diff --git a/Cargo.lock b/Cargo.lock index ea245d0154..80a7a3ebdb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -497,6 +497,7 @@ dependencies = [ "figma-api", "futures", "gl", + "grida-text-edit", "imgref", "json-patch", "math2", diff --git a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js index d675fb791f..16684bfa53 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -1,2 +1,2 @@ -var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Kg"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>3]=BigInt(id);HEAP64[dirp+pos+8>>3]=BigInt((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_pack,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_json,_pointer_move,_redraw,_resize_surface,_runtime_renderer_set_cache_tile,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_verbose,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Lg"];_add_font=Module["_add_font"]=wasmExports["Ng"];_add_image=Module["_add_image"]=wasmExports["Og"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Pg"];_allocate=Module["_allocate"]=wasmExports["Qg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Rg"];_command=Module["_command"]=wasmExports["Sg"];_deallocate=Module["_deallocate"]=wasmExports["Tg"];_destroy=Module["_destroy"]=wasmExports["Ug"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Vg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Wg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["Xg"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["Yg"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["Zg"];_export_node_as=Module["_export_node_as"]=wasmExports["_g"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["$g"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["ah"];_get_image_size=Module["_get_image_size"]=wasmExports["bh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["ch"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["dh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["eh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["fh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["gh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["hh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["ih"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["jh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["kh"];_grida_svg_pack=Module["_grida_svg_pack"]=wasmExports["lh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["mh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["nh"];_init=Module["_init"]=wasmExports["oh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["ph"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["qh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["rh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["sh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["th"];_load_scene_json=Module["_load_scene_json"]=wasmExports["uh"];_pointer_move=Module["_pointer_move"]=wasmExports["vh"];_redraw=Module["_redraw"]=wasmExports["wh"];_resize_surface=Module["_resize_surface"]=wasmExports["xh"];_runtime_renderer_set_cache_tile=Module["_runtime_renderer_set_cache_tile"]=wasmExports["yh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["zh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Ah"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Bh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Ch"];_set_debug=Module["_set_debug"]=wasmExports["Dh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Eh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Fh"];_set_verbose=Module["_set_verbose"]=wasmExports["Gh"];_tick=Module["_tick"]=wasmExports["Hh"];_to_vector_network=Module["_to_vector_network"]=wasmExports["Ih"];_toggle_debug=Module["_toggle_debug"]=wasmExports["Jh"];_main=Module["_main"]=wasmExports["Kh"];_emscripten_builtin_memalign=wasmExports["Lh"];_setThrew=wasmExports["Mh"];__emscripten_tempret_set=wasmExports["Nh"];__emscripten_stack_restore=wasmExports["Oh"];__emscripten_stack_alloc=wasmExports["Ph"];_emscripten_stack_get_current=wasmExports["Qh"];___cxa_decrement_exception_refcount=wasmExports["Rh"];___cxa_increment_exception_refcount=wasmExports["Sh"];___cxa_can_catch=wasmExports["Th"];___cxa_get_exception_ptr=wasmExports["Uh"];memory=wasmMemory=wasmExports["Jg"];__indirect_function_table=wasmTable=wasmExports["Mg"]}var wasmImports={F:___cxa_begin_catch,M:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,ba:___cxa_find_matching_catch_4,xa:___cxa_rethrow,G:___cxa_throw,cb:___cxa_uncaught_exceptions,e:___resumeException,Ba:___syscall_fcntl64,wb:___syscall_fstat64,sb:___syscall_getcwd,ib:___syscall_getdents64,xb:___syscall_ioctl,tb:___syscall_lstat64,ub:___syscall_newfstatat,Ca:___syscall_openat,hb:___syscall_readlinkat,vb:___syscall_stat64,Ab:__abort_js,eb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Bb:__tzset_js,zb:_clock_time_get,yb:_emscripten_date_now,gb:_emscripten_get_heap_max,Cf:_emscripten_glActiveTexture,Df:_emscripten_glAttachShader,fe:_emscripten_glBeginQuery,$d:_emscripten_glBeginQueryEXT,Gc:_emscripten_glBeginTransformFeedback,Ef:_emscripten_glBindAttribLocation,Ff:_emscripten_glBindBuffer,Dc:_emscripten_glBindBufferBase,Ec:_emscripten_glBindBufferRange,De:_emscripten_glBindFramebuffer,Ee:_emscripten_glBindRenderbuffer,le:_emscripten_glBindSampler,Gf:_emscripten_glBindTexture,Tb:_emscripten_glBindTransformFeedback,Ze:_emscripten_glBindVertexArray,af:_emscripten_glBindVertexArrayOES,Hf:_emscripten_glBlendColor,If:_emscripten_glBlendEquation,Ld:_emscripten_glBlendEquationSeparate,Jf:_emscripten_glBlendFunc,Kd:_emscripten_glBlendFuncSeparate,xe:_emscripten_glBlitFramebuffer,Kf:_emscripten_glBufferData,Lf:_emscripten_glBufferSubData,Fe:_emscripten_glCheckFramebufferStatus,Nf:_emscripten_glClear,gc:_emscripten_glClearBufferfi,hc:_emscripten_glClearBufferfv,jc:_emscripten_glClearBufferiv,ic:_emscripten_glClearBufferuiv,Of:_emscripten_glClearColor,Jd:_emscripten_glClearDepthf,Pf:_emscripten_glClearStencil,ue:_emscripten_glClientWaitSync,ad:_emscripten_glClipControlEXT,Qf:_emscripten_glColorMask,Rf:_emscripten_glCompileShader,Sf:_emscripten_glCompressedTexImage2D,Tc:_emscripten_glCompressedTexImage3D,Tf:_emscripten_glCompressedTexSubImage2D,Sc:_emscripten_glCompressedTexSubImage3D,we:_emscripten_glCopyBufferSubData,Id:_emscripten_glCopyTexImage2D,Uf:_emscripten_glCopyTexSubImage2D,Uc:_emscripten_glCopyTexSubImage3D,Vf:_emscripten_glCreateProgram,Wf:_emscripten_glCreateShader,Xf:_emscripten_glCullFace,Yf:_emscripten_glDeleteBuffers,Ge:_emscripten_glDeleteFramebuffers,Zf:_emscripten_glDeleteProgram,ge:_emscripten_glDeleteQueries,ae:_emscripten_glDeleteQueriesEXT,He:_emscripten_glDeleteRenderbuffers,me:_emscripten_glDeleteSamplers,_f:_emscripten_glDeleteShader,ve:_emscripten_glDeleteSync,$f:_emscripten_glDeleteTextures,Sb:_emscripten_glDeleteTransformFeedbacks,_e:_emscripten_glDeleteVertexArrays,bf:_emscripten_glDeleteVertexArraysOES,Hd:_emscripten_glDepthFunc,ag:_emscripten_glDepthMask,Gd:_emscripten_glDepthRangef,Fd:_emscripten_glDetachShader,bg:_emscripten_glDisable,cg:_emscripten_glDisableVertexAttribArray,dg:_emscripten_glDrawArrays,Xe:_emscripten_glDrawArraysInstanced,Od:_emscripten_glDrawArraysInstancedANGLE,Eb:_emscripten_glDrawArraysInstancedARB,Ue:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Zc:_emscripten_glDrawArraysInstancedEXT,Fb:_emscripten_glDrawArraysInstancedNV,Se:_emscripten_glDrawBuffers,Xc:_emscripten_glDrawBuffersEXT,Pd:_emscripten_glDrawBuffersWEBGL,eg:_emscripten_glDrawElements,Ye:_emscripten_glDrawElementsInstanced,Nd:_emscripten_glDrawElementsInstancedANGLE,Cb:_emscripten_glDrawElementsInstancedARB,Ve:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Db:_emscripten_glDrawElementsInstancedEXT,Yc:_emscripten_glDrawElementsInstancedNV,Me:_emscripten_glDrawRangeElements,fg:_emscripten_glEnable,gg:_emscripten_glEnableVertexAttribArray,he:_emscripten_glEndQuery,be:_emscripten_glEndQueryEXT,Fc:_emscripten_glEndTransformFeedback,re:_emscripten_glFenceSync,hg:_emscripten_glFinish,ig:_emscripten_glFlush,Ie:_emscripten_glFramebufferRenderbuffer,Je:_emscripten_glFramebufferTexture2D,Jc:_emscripten_glFramebufferTextureLayer,jg:_emscripten_glFrontFace,kg:_emscripten_glGenBuffers,Ke:_emscripten_glGenFramebuffers,ie:_emscripten_glGenQueries,ce:_emscripten_glGenQueriesEXT,Le:_emscripten_glGenRenderbuffers,ne:_emscripten_glGenSamplers,lg:_emscripten_glGenTextures,Rb:_emscripten_glGenTransformFeedbacks,We:_emscripten_glGenVertexArrays,cf:_emscripten_glGenVertexArraysOES,ze:_emscripten_glGenerateMipmap,Ed:_emscripten_glGetActiveAttrib,Dd:_emscripten_glGetActiveUniform,bc:_emscripten_glGetActiveUniformBlockName,cc:_emscripten_glGetActiveUniformBlockiv,ec:_emscripten_glGetActiveUniformsiv,Cd:_emscripten_glGetAttachedShaders,Bd:_emscripten_glGetAttribLocation,Ad:_emscripten_glGetBooleanv,Yb:_emscripten_glGetBufferParameteri64v,mg:_emscripten_glGetBufferParameteriv,ng:_emscripten_glGetError,og:_emscripten_glGetFloatv,tc:_emscripten_glGetFragDataLocation,Ae:_emscripten_glGetFramebufferAttachmentParameteriv,Zb:_emscripten_glGetInteger64i_v,$b:_emscripten_glGetInteger64v,Hc:_emscripten_glGetIntegeri_v,pg:_emscripten_glGetIntegerv,Ib:_emscripten_glGetInternalformativ,Mb:_emscripten_glGetProgramBinary,qg:_emscripten_glGetProgramInfoLog,rg:_emscripten_glGetProgramiv,Yd:_emscripten_glGetQueryObjecti64vEXT,Rd:_emscripten_glGetQueryObjectivEXT,Zd:_emscripten_glGetQueryObjectui64vEXT,je:_emscripten_glGetQueryObjectuiv,de:_emscripten_glGetQueryObjectuivEXT,ke:_emscripten_glGetQueryiv,ee:_emscripten_glGetQueryivEXT,Be:_emscripten_glGetRenderbufferParameteriv,Ub:_emscripten_glGetSamplerParameterfv,Vb:_emscripten_glGetSamplerParameteriv,sg:_emscripten_glGetShaderInfoLog,Vd:_emscripten_glGetShaderPrecisionFormat,zd:_emscripten_glGetShaderSource,tg:_emscripten_glGetShaderiv,ug:_emscripten_glGetString,$e:_emscripten_glGetStringi,_b:_emscripten_glGetSynciv,yd:_emscripten_glGetTexParameterfv,xd:_emscripten_glGetTexParameteriv,Bc:_emscripten_glGetTransformFeedbackVarying,dc:_emscripten_glGetUniformBlockIndex,fc:_emscripten_glGetUniformIndices,vg:_emscripten_glGetUniformLocation,wd:_emscripten_glGetUniformfv,vd:_emscripten_glGetUniformiv,uc:_emscripten_glGetUniformuiv,Ac:_emscripten_glGetVertexAttribIiv,zc:_emscripten_glGetVertexAttribIuiv,sd:_emscripten_glGetVertexAttribPointerv,ud:_emscripten_glGetVertexAttribfv,td:_emscripten_glGetVertexAttribiv,rd:_emscripten_glHint,Wd:_emscripten_glInvalidateFramebuffer,Xd:_emscripten_glInvalidateSubFramebuffer,qd:_emscripten_glIsBuffer,pd:_emscripten_glIsEnabled,od:_emscripten_glIsFramebuffer,nd:_emscripten_glIsProgram,Rc:_emscripten_glIsQuery,Sd:_emscripten_glIsQueryEXT,md:_emscripten_glIsRenderbuffer,Xb:_emscripten_glIsSampler,ld:_emscripten_glIsShader,se:_emscripten_glIsSync,wg:_emscripten_glIsTexture,Qb:_emscripten_glIsTransformFeedback,Ic:_emscripten_glIsVertexArray,Qd:_emscripten_glIsVertexArrayOES,xg:_emscripten_glLineWidth,yg:_emscripten_glLinkProgram,Qe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Re:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Pb:_emscripten_glPauseTransformFeedback,zg:_emscripten_glPixelStorei,$c:_emscripten_glPolygonModeWEBGL,kd:_emscripten_glPolygonOffset,bd:_emscripten_glPolygonOffsetClampEXT,Lb:_emscripten_glProgramBinary,Kb:_emscripten_glProgramParameteri,_d:_emscripten_glQueryCounterEXT,Te:_emscripten_glReadBuffer,Ag:_emscripten_glReadPixels,jd:_emscripten_glReleaseShaderCompiler,Ce:_emscripten_glRenderbufferStorage,ye:_emscripten_glRenderbufferStorageMultisample,Nb:_emscripten_glResumeTransformFeedback,id:_emscripten_glSampleCoverage,oe:_emscripten_glSamplerParameterf,Wb:_emscripten_glSamplerParameterfv,pe:_emscripten_glSamplerParameteri,qe:_emscripten_glSamplerParameteriv,Bg:_emscripten_glScissor,hd:_emscripten_glShaderBinary,Cg:_emscripten_glShaderSource,Dg:_emscripten_glStencilFunc,Eg:_emscripten_glStencilFuncSeparate,Fg:_emscripten_glStencilMask,Gg:_emscripten_glStencilMaskSeparate,Hg:_emscripten_glStencilOp,Ig:_emscripten_glStencilOpSeparate,Ha:_emscripten_glTexImage2D,Wc:_emscripten_glTexImage3D,Ia:_emscripten_glTexParameterf,Ja:_emscripten_glTexParameterfv,Ka:_emscripten_glTexParameteri,La:_emscripten_glTexParameteriv,Ne:_emscripten_glTexStorage2D,Jb:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Vc:_emscripten_glTexSubImage3D,Cc:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,yf:_emscripten_glUniform1i,zf:_emscripten_glUniform1iv,sc:_emscripten_glUniform1ui,nc:_emscripten_glUniform1uiv,Af:_emscripten_glUniform2f,Bf:_emscripten_glUniform2fv,xf:_emscripten_glUniform2i,wf:_emscripten_glUniform2iv,qc:_emscripten_glUniform2ui,mc:_emscripten_glUniform2uiv,vf:_emscripten_glUniform3f,uf:_emscripten_glUniform3fv,tf:_emscripten_glUniform3i,sf:_emscripten_glUniform3iv,pc:_emscripten_glUniform3ui,lc:_emscripten_glUniform3uiv,rf:_emscripten_glUniform4f,qf:_emscripten_glUniform4fv,df:_emscripten_glUniform4i,ef:_emscripten_glUniform4iv,oc:_emscripten_glUniform4ui,kc:_emscripten_glUniform4uiv,ac:_emscripten_glUniformBlockBinding,ff:_emscripten_glUniformMatrix2fv,Qc:_emscripten_glUniformMatrix2x3fv,Oc:_emscripten_glUniformMatrix2x4fv,gf:_emscripten_glUniformMatrix3fv,Pc:_emscripten_glUniformMatrix3x2fv,Lc:_emscripten_glUniformMatrix3x4fv,hf:_emscripten_glUniformMatrix4fv,Nc:_emscripten_glUniformMatrix4x2fv,Kc:_emscripten_glUniformMatrix4x3fv,jf:_emscripten_glUseProgram,gd:_emscripten_glValidateProgram,kf:_emscripten_glVertexAttrib1f,fd:_emscripten_glVertexAttrib1fv,ed:_emscripten_glVertexAttrib2f,lf:_emscripten_glVertexAttrib2fv,dd:_emscripten_glVertexAttrib3f,mf:_emscripten_glVertexAttrib3fv,cd:_emscripten_glVertexAttrib4f,nf:_emscripten_glVertexAttrib4fv,Oe:_emscripten_glVertexAttribDivisor,Md:_emscripten_glVertexAttribDivisorANGLE,Gb:_emscripten_glVertexAttribDivisorARB,_c:_emscripten_glVertexAttribDivisorEXT,Hb:_emscripten_glVertexAttribDivisorNV,yc:_emscripten_glVertexAttribI4i,wc:_emscripten_glVertexAttribI4iv,xc:_emscripten_glVertexAttribI4ui,vc:_emscripten_glVertexAttribI4uiv,Pe:_emscripten_glVertexAttribIPointer,of:_emscripten_glVertexAttribPointer,pf:_emscripten_glViewport,te:_emscripten_glWaitSync,Za:_emscripten_request_animation_frame_loop,fb:_emscripten_resize_heap,pb:_environ_get,qb:_environ_sizes_get,Ra:_exit,aa:_fd_close,jb:_fd_pread,za:_fd_read,nb:_fd_seek,fa:_fd_write,Pa:_glGetIntegerv,ka:_glGetString,Qa:_glGetStringi,Td:invoke_dd,Ud:invoke_dddd,va:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,wa:invoke_fiii,s:invoke_fiiidi,R:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,pa:invoke_iif,ob:invoke_iiffi,ma:invoke_iiffiii,h:invoke_iii,Fa:invoke_iiiffii,f:invoke_iiii,l:invoke_iiiii,bb:invoke_iiiiid,y:invoke_iiiiii,x:invoke_iiiiiii,D:invoke_iiiiiiii,q:invoke_iiiiiiiii,la:invoke_iiiiiiiiii,Z:invoke_iiiiiiiiiiii,ia:invoke_iiiiiiiiiiiifiii,oa:invoke_iijj,db:invoke_j,T:invoke_ji,W:invoke_jiii,_:invoke_jiiii,H:invoke_jjji,k:invoke_v,Mf:invoke_vff,b:invoke_vi,P:invoke_vid,O:invoke_vif,u:invoke_viff,B:invoke_viffff,V:invoke_vifffff,Va:invoke_viffffff,A:invoke_viffi,da:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,N:invoke_viif,E:invoke_viiff,ya:invoke_viififii,v:invoke_viifiiifi,d:invoke_viii,ja:invoke_viiif,z:invoke_viiiffi,I:invoke_viiiffiffii,Mc:invoke_viiifi,J:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ga:invoke_viiiif,Da:invoke_viiiiff,sa:invoke_viiiiffi,Aa:invoke_viiiifi,g:invoke_viiiii,Ob:invoke_viiiiif,Ea:invoke_viiiiiff,Wa:invoke_viiiiiffiii,_a:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,S:invoke_viiiiiiii,U:invoke_viiiiiiiii,L:invoke_viiiiiiiiii,na:invoke_viiiiiiiiiii,Y:invoke_viiiiiiiiiiiiiii,Ga:invoke_viiiiiji,$a:invoke_viiiijjiiiiff,K:invoke_viiij,w:invoke_viiijii,ca:invoke_viij,o:invoke_viiji,ea:invoke_viijiff,X:invoke_viijiiiif,qa:invoke_viijj,Q:invoke_vij,ua:invoke_vijff,ab:invoke_viji,C:invoke_vijii,ta:invoke_vijiifi,ra:invoke_vijiii,$:invoke_vijjjj,rc:invoke_vjii,ha:_llvm_eh_typeid_for,rb:_random_get};function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijjiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iijj(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Lg"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>3]=BigInt(id);HEAP64[dirp+pos+8>>3]=BigInt((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;HEAP8[dirp+pos+18]=type;stringToUTF8(name,dirp+pos+19,256);pos+=struct_size}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_readlinkat(dirfd,path,buf,bufsize){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(bufsize<=0)return-28;var ret=FS.readlink(path);var len=Math.min(bufsize,lengthBytesUTF8(ret));var endChar=HEAP8[buf+len];stringToUTF8(ret,buf,bufsize+1);HEAP8[buf+len]=endChar;return len}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_pack,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_json,_pointer_move,_redraw,_resize_surface,_runtime_renderer_set_cache_tile,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_verbose,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Mg"];_add_font=Module["_add_font"]=wasmExports["Og"];_add_image=Module["_add_image"]=wasmExports["Pg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Qg"];_allocate=Module["_allocate"]=wasmExports["Rg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Sg"];_command=Module["_command"]=wasmExports["Tg"];_deallocate=Module["_deallocate"]=wasmExports["Ug"];_destroy=Module["_destroy"]=wasmExports["Vg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Wg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Xg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["Yg"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["Zg"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["_g"];_export_node_as=Module["_export_node_as"]=wasmExports["$g"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["ah"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["bh"];_get_image_size=Module["_get_image_size"]=wasmExports["ch"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["dh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["eh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["fh"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["gh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["hh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["ih"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["jh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["kh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["lh"];_grida_svg_pack=Module["_grida_svg_pack"]=wasmExports["mh"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["nh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["oh"];_init=Module["_init"]=wasmExports["ph"];_init_with_backend=Module["_init_with_backend"]=wasmExports["qh"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["rh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["sh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["th"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["uh"];_load_scene_json=Module["_load_scene_json"]=wasmExports["vh"];_pointer_move=Module["_pointer_move"]=wasmExports["wh"];_redraw=Module["_redraw"]=wasmExports["xh"];_resize_surface=Module["_resize_surface"]=wasmExports["yh"];_runtime_renderer_set_cache_tile=Module["_runtime_renderer_set_cache_tile"]=wasmExports["zh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Ah"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Bh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Ch"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Dh"];_set_debug=Module["_set_debug"]=wasmExports["Eh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Fh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Gh"];_set_verbose=Module["_set_verbose"]=wasmExports["Hh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Ih"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Jh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Kh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["Lh"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["Mh"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["Nh"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["Oh"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["Ph"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["Qh"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["Rh"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["Sh"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["Th"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["Uh"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["Vh"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["Wh"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["Xh"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["Yh"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["Zh"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["_h"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["$h"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["ai"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["bi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ci"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["di"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["ei"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["fi"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["gi"];_tick=Module["_tick"]=wasmExports["hi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["ii"];_toggle_debug=Module["_toggle_debug"]=wasmExports["ji"];_main=Module["_main"]=wasmExports["ki"];_emscripten_builtin_memalign=wasmExports["li"];_setThrew=wasmExports["mi"];__emscripten_tempret_set=wasmExports["ni"];__emscripten_stack_restore=wasmExports["oi"];__emscripten_stack_alloc=wasmExports["pi"];_emscripten_stack_get_current=wasmExports["qi"];___cxa_decrement_exception_refcount=wasmExports["ri"];___cxa_increment_exception_refcount=wasmExports["si"];___cxa_can_catch=wasmExports["ti"];___cxa_get_exception_ptr=wasmExports["ui"];memory=wasmMemory=wasmExports["Kg"];__indirect_function_table=wasmTable=wasmExports["Ng"]}var wasmImports={G:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,da:___cxa_find_matching_catch_4,za:___cxa_rethrow,H:___cxa_throw,cb:___cxa_uncaught_exceptions,e:___resumeException,Ca:___syscall_fcntl64,wb:___syscall_fstat64,sb:___syscall_getcwd,jb:___syscall_getdents64,xb:___syscall_ioctl,tb:___syscall_lstat64,ub:___syscall_newfstatat,Da:___syscall_openat,ib:___syscall_readlinkat,vb:___syscall_stat64,Ab:__abort_js,eb:__emscripten_throw_longjmp,nb:__gmtime_js,lb:__mmap_js,mb:__munmap_js,Bb:__tzset_js,zb:_clock_time_get,yb:_emscripten_date_now,hb:_emscripten_get_heap_max,Cf:_emscripten_glActiveTexture,Df:_emscripten_glAttachShader,fe:_emscripten_glBeginQuery,$d:_emscripten_glBeginQueryEXT,Hc:_emscripten_glBeginTransformFeedback,Ef:_emscripten_glBindAttribLocation,Ff:_emscripten_glBindBuffer,Ec:_emscripten_glBindBufferBase,Fc:_emscripten_glBindBufferRange,De:_emscripten_glBindFramebuffer,Ee:_emscripten_glBindRenderbuffer,le:_emscripten_glBindSampler,Gf:_emscripten_glBindTexture,Tb:_emscripten_glBindTransformFeedback,Ze:_emscripten_glBindVertexArray,af:_emscripten_glBindVertexArrayOES,Hf:_emscripten_glBlendColor,If:_emscripten_glBlendEquation,Ld:_emscripten_glBlendEquationSeparate,Jf:_emscripten_glBlendFunc,Kd:_emscripten_glBlendFuncSeparate,xe:_emscripten_glBlitFramebuffer,Kf:_emscripten_glBufferData,Lf:_emscripten_glBufferSubData,Fe:_emscripten_glCheckFramebufferStatus,Mf:_emscripten_glClear,gc:_emscripten_glClearBufferfi,hc:_emscripten_glClearBufferfv,kc:_emscripten_glClearBufferiv,jc:_emscripten_glClearBufferuiv,Of:_emscripten_glClearColor,Jd:_emscripten_glClearDepthf,Pf:_emscripten_glClearStencil,ue:_emscripten_glClientWaitSync,ad:_emscripten_glClipControlEXT,Qf:_emscripten_glColorMask,Rf:_emscripten_glCompileShader,Sf:_emscripten_glCompressedTexImage2D,Tc:_emscripten_glCompressedTexImage3D,Tf:_emscripten_glCompressedTexSubImage2D,Sc:_emscripten_glCompressedTexSubImage3D,we:_emscripten_glCopyBufferSubData,Id:_emscripten_glCopyTexImage2D,Uf:_emscripten_glCopyTexSubImage2D,Uc:_emscripten_glCopyTexSubImage3D,Vf:_emscripten_glCreateProgram,Wf:_emscripten_glCreateShader,Xf:_emscripten_glCullFace,Yf:_emscripten_glDeleteBuffers,Ge:_emscripten_glDeleteFramebuffers,Zf:_emscripten_glDeleteProgram,ge:_emscripten_glDeleteQueries,ae:_emscripten_glDeleteQueriesEXT,He:_emscripten_glDeleteRenderbuffers,me:_emscripten_glDeleteSamplers,_f:_emscripten_glDeleteShader,ve:_emscripten_glDeleteSync,$f:_emscripten_glDeleteTextures,Sb:_emscripten_glDeleteTransformFeedbacks,_e:_emscripten_glDeleteVertexArrays,bf:_emscripten_glDeleteVertexArraysOES,Hd:_emscripten_glDepthFunc,ag:_emscripten_glDepthMask,Gd:_emscripten_glDepthRangef,Fd:_emscripten_glDetachShader,bg:_emscripten_glDisable,cg:_emscripten_glDisableVertexAttribArray,dg:_emscripten_glDrawArrays,Xe:_emscripten_glDrawArraysInstanced,Od:_emscripten_glDrawArraysInstancedANGLE,Eb:_emscripten_glDrawArraysInstancedARB,Ue:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Zc:_emscripten_glDrawArraysInstancedEXT,Gb:_emscripten_glDrawArraysInstancedNV,Se:_emscripten_glDrawBuffers,Xc:_emscripten_glDrawBuffersEXT,Pd:_emscripten_glDrawBuffersWEBGL,eg:_emscripten_glDrawElements,Ye:_emscripten_glDrawElementsInstanced,Nd:_emscripten_glDrawElementsInstancedANGLE,Cb:_emscripten_glDrawElementsInstancedARB,Ve:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Db:_emscripten_glDrawElementsInstancedEXT,Yc:_emscripten_glDrawElementsInstancedNV,Me:_emscripten_glDrawRangeElements,fg:_emscripten_glEnable,gg:_emscripten_glEnableVertexAttribArray,he:_emscripten_glEndQuery,be:_emscripten_glEndQueryEXT,Gc:_emscripten_glEndTransformFeedback,re:_emscripten_glFenceSync,hg:_emscripten_glFinish,ig:_emscripten_glFlush,Ie:_emscripten_glFramebufferRenderbuffer,Je:_emscripten_glFramebufferTexture2D,Kc:_emscripten_glFramebufferTextureLayer,jg:_emscripten_glFrontFace,kg:_emscripten_glGenBuffers,Ke:_emscripten_glGenFramebuffers,ie:_emscripten_glGenQueries,ce:_emscripten_glGenQueriesEXT,Le:_emscripten_glGenRenderbuffers,ne:_emscripten_glGenSamplers,lg:_emscripten_glGenTextures,Rb:_emscripten_glGenTransformFeedbacks,We:_emscripten_glGenVertexArrays,cf:_emscripten_glGenVertexArraysOES,ze:_emscripten_glGenerateMipmap,Ed:_emscripten_glGetActiveAttrib,Dd:_emscripten_glGetActiveUniform,bc:_emscripten_glGetActiveUniformBlockName,cc:_emscripten_glGetActiveUniformBlockiv,ec:_emscripten_glGetActiveUniformsiv,Cd:_emscripten_glGetAttachedShaders,Bd:_emscripten_glGetAttribLocation,Ad:_emscripten_glGetBooleanv,Yb:_emscripten_glGetBufferParameteri64v,mg:_emscripten_glGetBufferParameteriv,ng:_emscripten_glGetError,og:_emscripten_glGetFloatv,tc:_emscripten_glGetFragDataLocation,Ae:_emscripten_glGetFramebufferAttachmentParameteriv,Zb:_emscripten_glGetInteger64i_v,$b:_emscripten_glGetInteger64v,Ic:_emscripten_glGetIntegeri_v,pg:_emscripten_glGetIntegerv,Jb:_emscripten_glGetInternalformativ,Nb:_emscripten_glGetProgramBinary,qg:_emscripten_glGetProgramInfoLog,rg:_emscripten_glGetProgramiv,Yd:_emscripten_glGetQueryObjecti64vEXT,Rd:_emscripten_glGetQueryObjectivEXT,Zd:_emscripten_glGetQueryObjectui64vEXT,je:_emscripten_glGetQueryObjectuiv,de:_emscripten_glGetQueryObjectuivEXT,ke:_emscripten_glGetQueryiv,ee:_emscripten_glGetQueryivEXT,Be:_emscripten_glGetRenderbufferParameteriv,Ub:_emscripten_glGetSamplerParameterfv,Vb:_emscripten_glGetSamplerParameteriv,sg:_emscripten_glGetShaderInfoLog,Vd:_emscripten_glGetShaderPrecisionFormat,zd:_emscripten_glGetShaderSource,tg:_emscripten_glGetShaderiv,ug:_emscripten_glGetString,$e:_emscripten_glGetStringi,_b:_emscripten_glGetSynciv,yd:_emscripten_glGetTexParameterfv,xd:_emscripten_glGetTexParameteriv,Bc:_emscripten_glGetTransformFeedbackVarying,dc:_emscripten_glGetUniformBlockIndex,fc:_emscripten_glGetUniformIndices,vg:_emscripten_glGetUniformLocation,wd:_emscripten_glGetUniformfv,vd:_emscripten_glGetUniformiv,uc:_emscripten_glGetUniformuiv,Ac:_emscripten_glGetVertexAttribIiv,zc:_emscripten_glGetVertexAttribIuiv,sd:_emscripten_glGetVertexAttribPointerv,ud:_emscripten_glGetVertexAttribfv,td:_emscripten_glGetVertexAttribiv,rd:_emscripten_glHint,Wd:_emscripten_glInvalidateFramebuffer,Xd:_emscripten_glInvalidateSubFramebuffer,qd:_emscripten_glIsBuffer,pd:_emscripten_glIsEnabled,od:_emscripten_glIsFramebuffer,nd:_emscripten_glIsProgram,Rc:_emscripten_glIsQuery,Sd:_emscripten_glIsQueryEXT,md:_emscripten_glIsRenderbuffer,Xb:_emscripten_glIsSampler,ld:_emscripten_glIsShader,se:_emscripten_glIsSync,wg:_emscripten_glIsTexture,Qb:_emscripten_glIsTransformFeedback,Jc:_emscripten_glIsVertexArray,Qd:_emscripten_glIsVertexArrayOES,xg:_emscripten_glLineWidth,yg:_emscripten_glLinkProgram,Qe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Re:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Pb:_emscripten_glPauseTransformFeedback,zg:_emscripten_glPixelStorei,$c:_emscripten_glPolygonModeWEBGL,kd:_emscripten_glPolygonOffset,bd:_emscripten_glPolygonOffsetClampEXT,Mb:_emscripten_glProgramBinary,Lb:_emscripten_glProgramParameteri,_d:_emscripten_glQueryCounterEXT,Te:_emscripten_glReadBuffer,Ag:_emscripten_glReadPixels,jd:_emscripten_glReleaseShaderCompiler,Ce:_emscripten_glRenderbufferStorage,ye:_emscripten_glRenderbufferStorageMultisample,Ob:_emscripten_glResumeTransformFeedback,id:_emscripten_glSampleCoverage,oe:_emscripten_glSamplerParameterf,Wb:_emscripten_glSamplerParameterfv,pe:_emscripten_glSamplerParameteri,qe:_emscripten_glSamplerParameteriv,Bg:_emscripten_glScissor,hd:_emscripten_glShaderBinary,Cg:_emscripten_glShaderSource,Dg:_emscripten_glStencilFunc,Eg:_emscripten_glStencilFuncSeparate,Fg:_emscripten_glStencilMask,Gg:_emscripten_glStencilMaskSeparate,Hg:_emscripten_glStencilOp,Ig:_emscripten_glStencilOpSeparate,Jg:_emscripten_glTexImage2D,Wc:_emscripten_glTexImage3D,Ia:_emscripten_glTexParameterf,Ja:_emscripten_glTexParameterfv,Ka:_emscripten_glTexParameteri,La:_emscripten_glTexParameteriv,Ne:_emscripten_glTexStorage2D,Kb:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Vc:_emscripten_glTexSubImage3D,Cc:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,yf:_emscripten_glUniform1i,zf:_emscripten_glUniform1iv,sc:_emscripten_glUniform1ui,oc:_emscripten_glUniform1uiv,Af:_emscripten_glUniform2f,Bf:_emscripten_glUniform2fv,xf:_emscripten_glUniform2i,wf:_emscripten_glUniform2iv,rc:_emscripten_glUniform2ui,nc:_emscripten_glUniform2uiv,vf:_emscripten_glUniform3f,uf:_emscripten_glUniform3fv,tf:_emscripten_glUniform3i,sf:_emscripten_glUniform3iv,qc:_emscripten_glUniform3ui,mc:_emscripten_glUniform3uiv,rf:_emscripten_glUniform4f,qf:_emscripten_glUniform4fv,df:_emscripten_glUniform4i,ef:_emscripten_glUniform4iv,pc:_emscripten_glUniform4ui,lc:_emscripten_glUniform4uiv,ac:_emscripten_glUniformBlockBinding,ff:_emscripten_glUniformMatrix2fv,Qc:_emscripten_glUniformMatrix2x3fv,Oc:_emscripten_glUniformMatrix2x4fv,gf:_emscripten_glUniformMatrix3fv,Pc:_emscripten_glUniformMatrix3x2fv,Mc:_emscripten_glUniformMatrix3x4fv,hf:_emscripten_glUniformMatrix4fv,Nc:_emscripten_glUniformMatrix4x2fv,Lc:_emscripten_glUniformMatrix4x3fv,jf:_emscripten_glUseProgram,gd:_emscripten_glValidateProgram,kf:_emscripten_glVertexAttrib1f,fd:_emscripten_glVertexAttrib1fv,ed:_emscripten_glVertexAttrib2f,lf:_emscripten_glVertexAttrib2fv,dd:_emscripten_glVertexAttrib3f,mf:_emscripten_glVertexAttrib3fv,cd:_emscripten_glVertexAttrib4f,nf:_emscripten_glVertexAttrib4fv,Oe:_emscripten_glVertexAttribDivisor,Md:_emscripten_glVertexAttribDivisorANGLE,Hb:_emscripten_glVertexAttribDivisorARB,_c:_emscripten_glVertexAttribDivisorEXT,Ib:_emscripten_glVertexAttribDivisorNV,yc:_emscripten_glVertexAttribI4i,wc:_emscripten_glVertexAttribI4iv,xc:_emscripten_glVertexAttribI4ui,vc:_emscripten_glVertexAttribI4uiv,Pe:_emscripten_glVertexAttribIPointer,of:_emscripten_glVertexAttribPointer,pf:_emscripten_glViewport,te:_emscripten_glWaitSync,Za:_emscripten_request_animation_frame_loop,fb:_emscripten_resize_heap,pb:_environ_get,qb:_environ_sizes_get,Ra:_exit,ca:_fd_close,kb:_fd_pread,Ba:_fd_read,ob:_fd_seek,ha:_fd_write,Pa:_glGetIntegerv,la:_glGetString,Qa:_glGetStringi,Td:invoke_dd,Ud:invoke_dddd,xa:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,ya:invoke_fiii,s:invoke_fiiidi,T:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,qa:invoke_iif,gb:invoke_iiffi,na:invoke_iiffiii,h:invoke_iii,Ga:invoke_iiiffii,f:invoke_iiii,R:invoke_iiiiff,l:invoke_iiiii,bb:invoke_iiiiid,y:invoke_iiiiii,x:invoke_iiiiiii,D:invoke_iiiiiiii,q:invoke_iiiiiiiii,ma:invoke_iiiiiiiiii,$:invoke_iiiiiiiiiiii,ka:invoke_iiiiiiiiiiiifiii,pa:invoke_iijj,db:invoke_j,V:invoke_ji,Y:invoke_jiii,aa:invoke_jiiii,I:invoke_jjji,k:invoke_v,Nf:invoke_vff,b:invoke_vi,Q:invoke_vid,P:invoke_vif,u:invoke_viff,C:invoke_viffff,X:invoke_vifffff,Va:invoke_viffffff,B:invoke_viffi,fa:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,O:invoke_viif,F:invoke_viiff,wa:invoke_viififii,v:invoke_viifiiifi,d:invoke_viii,E:invoke_viiif,A:invoke_viiiffi,J:invoke_viiiffiffii,Dc:invoke_viiifi,K:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ia:invoke_viiiif,Ea:invoke_viiiiff,ta:invoke_viiiiffi,Aa:invoke_viiiifi,g:invoke_viiiii,Fb:invoke_viiiiif,Fa:invoke_viiiiiff,Wa:invoke_viiiiiffiii,_a:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,U:invoke_viiiiiiii,W:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,oa:invoke_viiiiiiiiiii,_:invoke_viiiiiiiiiiiiiii,Ha:invoke_viiiiiji,$a:invoke_viiiijjiiiiff,L:invoke_viiij,w:invoke_viiijii,ea:invoke_viij,o:invoke_viiji,ga:invoke_viijiff,Z:invoke_viijiiiif,ra:invoke_viijj,S:invoke_vij,va:invoke_vijff,ab:invoke_viji,z:invoke_vijii,ua:invoke_vijiifi,sa:invoke_vijiii,ba:invoke_vijjjj,ic:invoke_vjii,ja:_llvm_eh_typeid_for,rb:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijjiiiiff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iijj(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createGridaCanvas;module.exports.default=createGridaCanvas}else if(typeof define==="function"&&define["amd"])define([],()=>createGridaCanvas); diff --git a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm index 24628fa7d4..0842640475 100755 --- a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm +++ b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:63b78d73f5aa54808403a4a2794a7f28c43471d01cc4b3495777dab4eb10b6b7 -size 12841196 +oid sha256:19b7af5ced963125c83c96a4f61d0c66e43072ae703b9c82a1377969b637e2ee +size 13051987 diff --git a/crates/grida-canvas-wasm/lib/index.ts b/crates/grida-canvas-wasm/lib/index.ts index c408c6dbf5..882d516d1d 100644 --- a/crates/grida-canvas-wasm/lib/index.ts +++ b/crates/grida-canvas-wasm/lib/index.ts @@ -4,6 +4,7 @@ import { Scene, type CreateImageResourceResult, type AddImageWithIdResult, + type TextEditCommand, } from "./modules/canvas"; import { svgtypes } from "./modules/svg-bindings"; export { @@ -11,6 +12,7 @@ export { type svgtypes, type CreateImageResourceResult, type AddImageWithIdResult, + type TextEditCommand, }; export const version = _version; diff --git a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts index 1bd1dca308..f000e71acf 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts @@ -196,5 +196,86 @@ declare namespace canvas { state: GridaCanvasApplicationPtr, enable: boolean ): void; + + // ==================================================================================================== + // TEXT EDITING + // ==================================================================================================== + _text_edit_enter( + state: GridaCanvasApplicationPtr, + node_id_ptr: number, + node_id_len: number + ): boolean; + _text_edit_exit( + state: GridaCanvasApplicationPtr, + commit: boolean + ): Ptr; + _text_edit_is_active(state: GridaCanvasApplicationPtr): boolean; + _text_edit_get_text(state: GridaCanvasApplicationPtr): Ptr; + _text_edit_undo(state: GridaCanvasApplicationPtr): boolean; + _text_edit_redo(state: GridaCanvasApplicationPtr): boolean; + _text_edit_command( + state: GridaCanvasApplicationPtr, + json_ptr: number, + json_len: number + ): void; + _text_edit_pointer_down( + state: GridaCanvasApplicationPtr, + x: number, + y: number, + shift: boolean, + click_count: number + ): void; + _text_edit_pointer_move( + state: GridaCanvasApplicationPtr, + x: number, + y: number + ): void; + _text_edit_pointer_up(state: GridaCanvasApplicationPtr): void; + _text_edit_ime_set_preedit( + state: GridaCanvasApplicationPtr, + text_ptr: number, + text_len: number + ): void; + _text_edit_ime_commit( + state: GridaCanvasApplicationPtr, + text_ptr: number, + text_len: number + ): void; + _text_edit_ime_cancel(state: GridaCanvasApplicationPtr): void; + _text_edit_get_selected_text(state: GridaCanvasApplicationPtr): Ptr; + _text_edit_get_selected_html(state: GridaCanvasApplicationPtr): Ptr; + _text_edit_paste_text( + state: GridaCanvasApplicationPtr, + text_ptr: number, + text_len: number + ): void; + _text_edit_paste_html( + state: GridaCanvasApplicationPtr, + html_ptr: number, + html_len: number + ): void; + _text_edit_get_caret_rect(state: GridaCanvasApplicationPtr): Ptr; + _text_edit_get_selection_rects(state: GridaCanvasApplicationPtr): Ptr; + _text_edit_toggle_bold(state: GridaCanvasApplicationPtr): void; + _text_edit_toggle_italic(state: GridaCanvasApplicationPtr): void; + _text_edit_toggle_underline(state: GridaCanvasApplicationPtr): void; + _text_edit_toggle_strikethrough(state: GridaCanvasApplicationPtr): void; + _text_edit_set_font_size( + state: GridaCanvasApplicationPtr, + size: number + ): void; + _text_edit_set_font_family( + state: GridaCanvasApplicationPtr, + family_ptr: number, + family_len: number + ): void; + _text_edit_set_color( + state: GridaCanvasApplicationPtr, + r: number, + g: number, + b: number, + a: number + ): void; + _text_edit_tick(state: GridaCanvasApplicationPtr): boolean; } } diff --git a/crates/grida-canvas-wasm/lib/modules/canvas.ts b/crates/grida-canvas-wasm/lib/modules/canvas.ts index f547645dae..4cdb777230 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas.ts @@ -165,10 +165,7 @@ export class Scene { } } - addImageWithId( - data: Uint8Array, - rid: string - ): AddImageWithIdResult | false { + addImageWithId(data: Uint8Array, rid: string): AddImageWithIdResult | false { this._assertAlive(); const [dataPtr, dataLen] = ffi.allocBytes(this.module, data); const [ridPtr, ridLen] = this._alloc_string(rid); @@ -350,9 +347,7 @@ export class Scene { * network has straight segments and corner radius should be applied as * a rendering effect. When absent, curves are baked into the geometry. */ - toVectorNetwork( - id: string - ): types.FlattenResult | null { + toVectorNetwork(id: string): types.FlattenResult | null { this._assertAlive(); const [ptr, len] = this._alloc_string(id); const outptr = this.module._to_vector_network(this.appptr, ptr, len - 1); @@ -516,4 +511,286 @@ export class Scene { this._assertAlive(); this.module._devtools_rendering_set_show_ruler(this.appptr, show); } + + // ========================================================================== + // Text editing + // ========================================================================== + + /** + * Enter text editing mode for a node. + * + * The engine reads all text properties (font, size, width, alignment) + * directly from the scene node, so only the node ID is needed. + */ + textEditEnter(nodeId: string): boolean { + this._assertAlive(); + const [ptr, len] = this._alloc_string(nodeId); + const result = this.module._text_edit_enter(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + return !!result; + } + + /** + * Exit text editing mode. + * @param commit - If true, returns the final text. If false, cancels. + * @returns The committed text, or null if cancelled / no session. + */ + textEditExit(commit: boolean): string | null { + this._assertAlive(); + const outptr = this.module._text_edit_exit(this.appptr, commit) as number; + if (!outptr) return null; + return ffi.readLenPrefixedString(this.module, outptr); + } + + /** Check if a text editing session is active. */ + textEditIsActive(): boolean { + this._assertAlive(); + return !!this.module._text_edit_is_active(this.appptr); + } + + /** + * Returns the current text of the active editing session, or null if + * no session is active. + */ + textEditGetText(): string | null { + this._assertAlive(); + const outptr = this.module._text_edit_get_text(this.appptr) as number; + if (!outptr) return null; + return ffi.readLenPrefixedString(this.module, outptr); + } + + /** + * Undo within the text editing session. + * + * The session owns all undo during editing. Document-level undo is not + * involved until the session exits. + */ + textEditUndo(): boolean { + this._assertAlive(); + return !!this.module._text_edit_undo(this.appptr); + } + + /** + * Redo within the text editing session. + */ + textEditRedo(): boolean { + this._assertAlive(); + return !!this.module._text_edit_redo(this.appptr); + } + + /** + * Dispatch an editing command. + * @param cmd - The command object (JSON-serializable WasmEditCommand). + */ + textEditCommand(cmd: TextEditCommand) { + this._assertAlive(); + const json = JSON.stringify(cmd); + const [ptr, len] = this._alloc_string(json); + this.module._text_edit_command(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + } + + /** Pointer down in layout-local coordinates. */ + textEditPointerDown( + x: number, + y: number, + shift: boolean, + clickCount: number + ) { + this._assertAlive(); + this.module._text_edit_pointer_down(this.appptr, x, y, shift, clickCount); + } + + /** Pointer move in layout-local coordinates (during drag). */ + textEditPointerMove(x: number, y: number) { + this._assertAlive(); + this.module._text_edit_pointer_move(this.appptr, x, y); + } + + /** Pointer up. */ + textEditPointerUp() { + this._assertAlive(); + this.module._text_edit_pointer_up(this.appptr); + } + + /** Set IME preedit string. */ + textEditImeSetPreedit(text: string) { + this._assertAlive(); + const [ptr, len] = this._alloc_string(text); + this.module._text_edit_ime_set_preedit(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + } + + /** Commit IME composition. */ + textEditImeCommit(text: string) { + this._assertAlive(); + const [ptr, len] = this._alloc_string(text); + this.module._text_edit_ime_commit(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + } + + /** Cancel IME composition. */ + textEditImeCancel() { + this._assertAlive(); + this.module._text_edit_ime_cancel(this.appptr); + } + + /** Get selected text as plain text. */ + textEditGetSelectedText(): string | null { + this._assertAlive(); + const outptr = this.module._text_edit_get_selected_text( + this.appptr + ) as number; + if (!outptr) return null; + return ffi.readLenPrefixedString(this.module, outptr); + } + + /** Get selected text as HTML. */ + textEditGetSelectedHtml(): string | null { + this._assertAlive(); + const outptr = this.module._text_edit_get_selected_html( + this.appptr + ) as number; + if (!outptr) return null; + return ffi.readLenPrefixedString(this.module, outptr); + } + + /** Paste plain text. */ + textEditPasteText(text: string) { + this._assertAlive(); + const [ptr, len] = this._alloc_string(text); + this.module._text_edit_paste_text(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + } + + /** Paste HTML with formatting. */ + textEditPasteHtml(html: string) { + this._assertAlive(); + const [ptr, len] = this._alloc_string(html); + this.module._text_edit_paste_html(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + } + + /** + * Get the caret rectangle in layout-local coordinates. + * @returns {x, y, w, h} or null if no active session. + */ + textEditGetCaretRect(): { + x: number; + y: number; + w: number; + h: number; + } | null { + this._assertAlive(); + const outptr = this.module._text_edit_get_caret_rect(this.appptr) as number; + if (!outptr) return null; + const bytes = ffi.readLenPrefixedBytes(this.module, outptr); + if (bytes.length < 16) return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + return { + x: view.getFloat32(0, true), + y: view.getFloat32(4, true), + w: view.getFloat32(8, true), + h: view.getFloat32(12, true), + }; + } + + /** + * Get selection rectangles in layout-local coordinates. + * @returns Array of {x, y, w, h} or null if no selection. + */ + textEditGetSelectionRects(): Array<{ + x: number; + y: number; + w: number; + h: number; + }> | null { + this._assertAlive(); + const outptr = this.module._text_edit_get_selection_rects( + this.appptr + ) as number; + if (!outptr) return null; + const json = ffi.readLenPrefixedString(this.module, outptr); + try { + return JSON.parse(json); + } catch { + return null; + } + } + + // --- Style commands --- + + textEditToggleBold() { + this._assertAlive(); + this.module._text_edit_toggle_bold(this.appptr); + } + + textEditToggleItalic() { + this._assertAlive(); + this.module._text_edit_toggle_italic(this.appptr); + } + + textEditToggleUnderline() { + this._assertAlive(); + this.module._text_edit_toggle_underline(this.appptr); + } + + textEditToggleStrikethrough() { + this._assertAlive(); + this.module._text_edit_toggle_strikethrough(this.appptr); + } + + textEditSetFontSize(size: number) { + this._assertAlive(); + this.module._text_edit_set_font_size(this.appptr, size); + } + + textEditSetFontFamily(family: string) { + this._assertAlive(); + const [ptr, len] = this._alloc_string(family); + this.module._text_edit_set_font_family(this.appptr, ptr, len - 1); + this._free_string(ptr, len); + } + + textEditSetColor(r: number, g: number, b: number, a: number) { + this._assertAlive(); + this.module._text_edit_set_color(this.appptr, r, g, b, a); + } + + /** + * Tick the blink timer. Returns true if cursor visibility changed. + */ + textEditTick(): boolean { + this._assertAlive(); + return !!this.module._text_edit_tick(this.appptr); + } } + +// --------------------------------------------------------------------------- +// Text editing command types (mirrors Rust WasmEditCommand) +// --------------------------------------------------------------------------- + +export type TextEditCommand = + | { type: "Insert"; text: string } + | { type: "Backspace" } + | { type: "BackspaceWord" } + | { type: "BackspaceLine" } + | { type: "Delete" } + | { type: "DeleteWord" } + | { type: "DeleteLine" } + | { type: "DeleteByCut" } + | { type: "MoveLeft"; extend: boolean } + | { type: "MoveRight"; extend: boolean } + | { type: "MoveUp"; extend: boolean } + | { type: "MoveDown"; extend: boolean } + | { type: "MoveHome"; extend: boolean } + | { type: "MoveEnd"; extend: boolean } + | { type: "MoveDocStart"; extend: boolean } + | { type: "MoveDocEnd"; extend: boolean } + | { type: "MovePageUp"; extend: boolean } + | { type: "MovePageDown"; extend: boolean } + | { type: "MoveWordLeft"; extend: boolean } + | { type: "MoveWordRight"; extend: boolean } + | { type: "SelectAll" } + | { type: "Undo" } + | { type: "Redo" }; diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index eea7819d0c..beddd8e3e9 100644 --- a/crates/grida-canvas-wasm/package.json +++ b/crates/grida-canvas-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@grida/canvas-wasm", - "version": "0.90.0-canary.9", + "version": "0.90.0-canary.10", "private": false, "description": "WASM bindings for Grida Canvas", "keywords": [ diff --git a/crates/grida-canvas-wasm/src/main.rs b/crates/grida-canvas-wasm/src/main.rs index a2f841b82b..f537de61ab 100644 --- a/crates/grida-canvas-wasm/src/main.rs +++ b/crates/grida-canvas-wasm/src/main.rs @@ -5,6 +5,7 @@ mod wasm_application; mod wasm_fonts; mod wasm_markdown; mod wasm_svg; +mod wasm_text_edit; #[cfg(not(target_arch = "wasm32"))] fn main() {} diff --git a/crates/grida-canvas-wasm/src/wasm_text_edit.rs b/crates/grida-canvas-wasm/src/wasm_text_edit.rs new file mode 100644 index 0000000000..f61da89b9d --- /dev/null +++ b/crates/grida-canvas-wasm/src/wasm_text_edit.rs @@ -0,0 +1,409 @@ +//! WASM C ABI for text editing operations. +//! +//! Thin delegates to `UnknownTargetApplication` methods. All orchestration +//! logic (session creation, layout, decoration updates, frame queuing) +//! lives in `grida-canvas`'s Application — this file is purely the C ABI +//! boundary. + +use crate::_internal::*; +use cg::text_edit_session::DEFAULT_CARET_WIDTH; +use cg::window::application::UnknownTargetApplication; +use serde::Deserialize; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +fn alloc_len_prefixed(bytes: &[u8]) -> *const u8 { + let Ok(len_u32) = u32::try_from(bytes.len()) else { + return std::ptr::null(); + }; + let total = 4 + bytes.len(); + let out = allocate(total); + let len_bytes = len_u32.to_le_bytes(); + unsafe { + std::ptr::copy_nonoverlapping(len_bytes.as_ptr(), out, 4); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), out.add(4), bytes.len()); + } + out +} + +/// Encode `f32` values as little-endian bytes (safe, no alignment concerns). +fn f32_slice_to_le_bytes(floats: &[f32]) -> Vec { + let mut bytes = Vec::with_capacity(floats.len() * 4); + for &f in floats { + bytes.extend_from_slice(&f.to_le_bytes()); + } + bytes +} + +use cg::text_edit_session::EditCommand; + +/// JSON-serializable editing command from TS. +#[derive(Deserialize)] +#[serde(tag = "type")] +enum WasmEditCommand { + Insert { text: String }, + Backspace, + BackspaceWord, + BackspaceLine, + Delete, + DeleteWord, + DeleteLine, + DeleteByCut, + MoveLeft { extend: bool }, + MoveRight { extend: bool }, + MoveUp { extend: bool }, + MoveDown { extend: bool }, + MoveHome { extend: bool }, + MoveEnd { extend: bool }, + MoveDocStart { extend: bool }, + MoveDocEnd { extend: bool }, + MovePageUp { extend: bool }, + MovePageDown { extend: bool }, + MoveWordLeft { extend: bool }, + MoveWordRight { extend: bool }, + SelectAll, + Undo, + Redo, +} + +fn wasm_cmd_to_editing(cmd: WasmEditCommand) -> Option { + Some(match cmd { + WasmEditCommand::Insert { text } => EditCommand::Insert(text), + WasmEditCommand::Backspace => EditCommand::Backspace, + WasmEditCommand::BackspaceWord => EditCommand::BackspaceWord, + WasmEditCommand::BackspaceLine => EditCommand::BackspaceLine, + WasmEditCommand::Delete => EditCommand::Delete, + WasmEditCommand::DeleteWord => EditCommand::DeleteWord, + WasmEditCommand::DeleteLine => EditCommand::DeleteLine, + WasmEditCommand::DeleteByCut => EditCommand::DeleteByCut, + WasmEditCommand::MoveLeft { extend } => EditCommand::MoveLeft { extend }, + WasmEditCommand::MoveRight { extend } => EditCommand::MoveRight { extend }, + WasmEditCommand::MoveUp { extend } => EditCommand::MoveUp { extend }, + WasmEditCommand::MoveDown { extend } => EditCommand::MoveDown { extend }, + WasmEditCommand::MoveHome { extend } => EditCommand::MoveHome { extend }, + WasmEditCommand::MoveEnd { extend } => EditCommand::MoveEnd { extend }, + WasmEditCommand::MoveDocStart { extend } => EditCommand::MoveDocStart { extend }, + WasmEditCommand::MoveDocEnd { extend } => EditCommand::MoveDocEnd { extend }, + WasmEditCommand::MovePageUp { extend } => EditCommand::MovePageUp { extend }, + WasmEditCommand::MovePageDown { extend } => EditCommand::MovePageDown { extend }, + WasmEditCommand::MoveWordLeft { extend } => EditCommand::MoveWordLeft { extend }, + WasmEditCommand::MoveWordRight { extend } => EditCommand::MoveWordRight { extend }, + WasmEditCommand::SelectAll => EditCommand::SelectAll, + WasmEditCommand::Undo | WasmEditCommand::Redo => return None, + }) +} + +// --------------------------------------------------------------------------- +// Session lifecycle +// --------------------------------------------------------------------------- + +/// Enter text editing mode for a node. +/// +/// The Application reads all text properties directly from the scene node +/// to guarantee the editing layout matches the Painter exactly. +#[no_mangle] +pub unsafe extern "C" fn text_edit_enter( + app: *mut UnknownTargetApplication, + node_id_ptr: *const u8, + node_id_len: usize, +) -> bool { + let Some(app) = app.as_mut() else { return false }; + let Some(node_id) = __str_from_ptr_len(node_id_ptr, node_id_len) else { return false }; + app.text_edit_enter(&node_id) +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_exit( + app: *mut UnknownTargetApplication, + commit: bool, +) -> *const u8 { + let Some(app) = app.as_mut() else { return std::ptr::null() }; + match app.text_edit_exit(commit) { + Some(text) => alloc_len_prefixed(text.as_bytes()), + None => std::ptr::null(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_is_active( + app: *mut UnknownTargetApplication, +) -> bool { + app.as_ref().map(|a| a.text_edit_is_active()).unwrap_or(false) +} + +/// Returns the current text of the active editing session. +/// +/// Returns a length-prefixed UTF-8 string, or null if no session is active. +#[no_mangle] +pub unsafe extern "C" fn text_edit_get_text( + app: *mut UnknownTargetApplication, +) -> *const u8 { + let Some(app) = app.as_ref() else { return std::ptr::null() }; + match app.text_edit_get_text() { + Some(text) => alloc_len_prefixed(text.as_bytes()), + None => std::ptr::null(), + } +} + +// --------------------------------------------------------------------------- +// Command dispatch +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_command( + app: *mut UnknownTargetApplication, + json_ptr: *const u8, + json_len: usize, +) { + let Some(app) = app.as_mut() else { return }; + let Some(json) = __str_from_ptr_len(json_ptr, json_len) else { return }; + + let cmd: WasmEditCommand = match serde_json::from_str(&json) { + Ok(c) => c, + Err(e) => { eprintln!("text_edit_command: {e}"); return; } + }; + + match cmd { + WasmEditCommand::Undo => { app.text_edit_undo(); } + WasmEditCommand::Redo => { app.text_edit_redo(); } + other => { + if let Some(c) = wasm_cmd_to_editing(other) { + app.text_edit_command(c); + } + } + } +} + +// --------------------------------------------------------------------------- +// Undo / Redo +// --------------------------------------------------------------------------- + +/// Undo within the text editing session. +/// +/// The session owns all undo during editing. Document-level undo is not +/// involved until the session exits. +#[no_mangle] +pub unsafe extern "C" fn text_edit_undo( + app: *mut UnknownTargetApplication, +) -> bool { + let Some(app) = app.as_mut() else { return false }; + app.text_edit_undo() +} + +/// Redo within the text editing session. +#[no_mangle] +pub unsafe extern "C" fn text_edit_redo( + app: *mut UnknownTargetApplication, +) -> bool { + let Some(app) = app.as_mut() else { return false }; + app.text_edit_redo() +} + +// --------------------------------------------------------------------------- +// Pointer events +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_pointer_down( + app: *mut UnknownTargetApplication, + x: f32, y: f32, shift: bool, click_count: u32, +) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_pointer_down(x, y, shift, click_count); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_pointer_move( + app: *mut UnknownTargetApplication, + x: f32, y: f32, +) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_pointer_move(x, y); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_pointer_up( + app: *mut UnknownTargetApplication, +) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_pointer_up(); +} + +// --------------------------------------------------------------------------- +// IME +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_ime_set_preedit( + app: *mut UnknownTargetApplication, + text_ptr: *const u8, text_len: usize, +) { + let Some(app) = app.as_mut() else { return }; + let text = __str_from_ptr_len(text_ptr, text_len).unwrap_or_default(); + app.text_edit_ime_set_preedit(text); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_ime_commit( + app: *mut UnknownTargetApplication, + text_ptr: *const u8, text_len: usize, +) { + let Some(app) = app.as_mut() else { return }; + let text = __str_from_ptr_len(text_ptr, text_len).unwrap_or_default(); + app.text_edit_ime_commit(&text); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_ime_cancel( + app: *mut UnknownTargetApplication, +) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_ime_cancel(); +} + +// --------------------------------------------------------------------------- +// Clipboard +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_get_selected_text( + app: *mut UnknownTargetApplication, +) -> *const u8 { + let Some(app) = app.as_ref() else { return std::ptr::null() }; + match app.text_edit_get_selected_text() { + Some(text) => alloc_len_prefixed(text.as_bytes()), + None => std::ptr::null(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_get_selected_html( + app: *mut UnknownTargetApplication, +) -> *const u8 { + let Some(app) = app.as_ref() else { return std::ptr::null() }; + match app.text_edit_get_selected_html() { + Some(html) => alloc_len_prefixed(html.as_bytes()), + None => std::ptr::null(), + } +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_paste_text( + app: *mut UnknownTargetApplication, + text_ptr: *const u8, text_len: usize, +) { + let Some(app) = app.as_mut() else { return }; + let Some(text) = __str_from_ptr_len(text_ptr, text_len) else { return }; + app.text_edit_paste_text(&text); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_paste_html( + app: *mut UnknownTargetApplication, + html_ptr: *const u8, html_len: usize, +) { + let Some(app) = app.as_mut() else { return }; + let Some(html) = __str_from_ptr_len(html_ptr, html_len) else { return }; + app.text_edit_paste_html(&html); +} + +// --------------------------------------------------------------------------- +// Geometry queries +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_get_caret_rect( + app: *mut UnknownTargetApplication, +) -> *const u8 { + let Some(app) = app.as_mut() else { return std::ptr::null() }; + let Some(cr) = app.text_edit_get_caret_rect() else { return std::ptr::null() }; + let floats: [f32; 4] = [cr.x, cr.y, DEFAULT_CARET_WIDTH, cr.height]; + alloc_len_prefixed(&f32_slice_to_le_bytes(&floats)) +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_get_selection_rects( + app: *mut UnknownTargetApplication, +) -> *const u8 { + let Some(app) = app.as_mut() else { return std::ptr::null() }; + let Some(rects) = app.text_edit_get_selection_rects() else { return std::ptr::null() }; + + #[derive(serde::Serialize)] + struct R { x: f32, y: f32, w: f32, h: f32 } + + let json_rects: Vec = rects.iter().map(|r| R { + x: r.x, y: r.y, w: r.width, h: r.height, + }).collect(); + + match serde_json::to_string(&json_rects) { + Ok(json) => alloc_len_prefixed(json.as_bytes()), + Err(_) => std::ptr::null(), + } +} + +// --------------------------------------------------------------------------- +// Style commands +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_toggle_bold(app: *mut UnknownTargetApplication) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_toggle_bold(); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_toggle_italic(app: *mut UnknownTargetApplication) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_toggle_italic(); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_toggle_underline(app: *mut UnknownTargetApplication) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_toggle_underline(); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_toggle_strikethrough(app: *mut UnknownTargetApplication) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_toggle_strikethrough(); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_set_font_size(app: *mut UnknownTargetApplication, size: f32) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_set_font_size(size); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_set_font_family( + app: *mut UnknownTargetApplication, + family_ptr: *const u8, family_len: usize, +) { + let Some(app) = app.as_mut() else { return }; + let Some(family) = __str_from_ptr_len(family_ptr, family_len) else { return }; + app.text_edit_set_font_family(&family); +} + +#[no_mangle] +pub unsafe extern "C" fn text_edit_set_color( + app: *mut UnknownTargetApplication, + r: f32, g: f32, b: f32, a: f32, +) { + let Some(app) = app.as_mut() else { return }; + app.text_edit_set_color(r, g, b, a); +} + +// --------------------------------------------------------------------------- +// Blink tick +// --------------------------------------------------------------------------- + +#[no_mangle] +pub unsafe extern "C" fn text_edit_tick( + app: *mut UnknownTargetApplication, +) -> bool { + let Some(app) = app.as_mut() else { return false }; + app.text_edit_tick() +} diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index 875edc8073..cbd34a0584 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -8,7 +8,14 @@ crate-type = ["cdylib", "rlib"] [dependencies] # skia -skia-safe = { version = "0.91.0", features = ["gpu", "gl", "textlayout", "pdf", "svg", "webp"] } +skia-safe = { version = "0.91.0", features = [ + "gpu", + "gl", + "textlayout", + "pdf", + "svg", + "webp", +] } gl = "0.14.0" # io @@ -16,6 +23,9 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" json-patch = "4.1.0" +# text editing +grida-text-edit = { path = "../grida-text-edit" } + # geo math2 = { path = "../math2" } rstar = "0.12" @@ -26,7 +36,7 @@ seahash = "4.1.0" taffy = "0.9.1" # svg parsing # (+2mb wasm32-unknown-emscripten@opt-level=3) -usvg = { path = "../../third_party/usvg" } +usvg = { path = "../../third_party/usvg" } # markdown parsing # (+0.25mb wasm32-unknown-emscripten@opt-level=3) pulldown-cmark = "0.13.0" diff --git a/crates/grida-canvas/src/cache/paragraph.rs b/crates/grida-canvas/src/cache/paragraph.rs index 14ac50eb9b..6662dfda5b 100644 --- a/crates/grida-canvas/src/cache/paragraph.rs +++ b/crates/grida-canvas/src/cache/paragraph.rs @@ -480,6 +480,11 @@ impl ParagraphCache { self.entries_measurement_by_shapekey_unstable.clear(); } + /// Invalidate the cached paragraph for a single node. + pub fn invalidate_by_id(&mut self, id: NodeId) { + self.entries_measurement_by_id.remove(&id); + } + pub fn len(&self) -> usize { self.entries_measurement_by_id.len() + self.entries_measurement_by_shapekey_unstable.len() } diff --git a/crates/grida-canvas/src/cache/picture.rs b/crates/grida-canvas/src/cache/picture.rs index 6158e6db8d..6384202690 100644 --- a/crates/grida-canvas/src/cache/picture.rs +++ b/crates/grida-canvas/src/cache/picture.rs @@ -86,4 +86,15 @@ impl PictureCache { self.default_store.clear(); self.variant_store.clear(); } + + /// Invalidate cached pictures for a single node (all variants). + /// + /// Note: `variant_store.retain()` is O(n) over all entries. + /// Called on each keystroke during text editing. If profiling shows + /// this is hot, consider a `HashMap>` + /// to make per-node invalidation O(1). + pub fn invalidate_node(&mut self, id: NodeId) { + self.default_store.remove(&id); + self.variant_store.retain(|&(nid, _), _| nid != id); + } } diff --git a/crates/grida-canvas/src/devtools/mod.rs b/crates/grida-canvas/src/devtools/mod.rs index 887c1ae45a..4361f9d6bb 100644 --- a/crates/grida-canvas/src/devtools/mod.rs +++ b/crates/grida-canvas/src/devtools/mod.rs @@ -3,5 +3,6 @@ pub mod hit_overlay; pub mod ruler_overlay; pub mod stats_overlay; pub mod stroke_overlay; +pub mod text_edit_decoration_overlay; pub mod text_overlay; pub mod tile_overlay; diff --git a/crates/grida-canvas/src/devtools/text_edit_decoration_overlay.rs b/crates/grida-canvas/src/devtools/text_edit_decoration_overlay.rs new file mode 100644 index 0000000000..91c70545b8 --- /dev/null +++ b/crates/grida-canvas/src/devtools/text_edit_decoration_overlay.rs @@ -0,0 +1,150 @@ +//! Text editing decoration overlay (caret + selection highlights). +//! +//! Drawn in the **devtools overlay pass** — after the scene is flushed and +//! composited. This means the caret and selection highlights are: +//! +//! - **Not clipped** by parent containers (visible even when the text +//! overflows its bounds — matching standard OS text editor behavior). +//! - **Zoom-independent**: the caret width is fixed in screen pixels +//! (see [`DEFAULT_CARET_WIDTH`]), regardless of the canvas zoom level. +//! +//! The overlay transforms layout-local decoration geometry to screen space +//! using the node's world transform and the camera view matrix, following +//! the same pattern as [`super::stroke_overlay::StrokeOverlay`]. + +use crate::cache::scene::SceneCache; +use crate::node::schema::NodeId; +use crate::painter::layer::Layer; +use crate::runtime::camera::Camera2D; +use crate::sk; +use grida_text_edit::{CaretRect, SelectionRect, DEFAULT_CARET_WIDTH}; +use skia_safe::{Canvas, Color, Matrix, Paint, PaintStyle, Rect}; + +// --------------------------------------------------------------------------- +// Decoration data (computed per-frame by the editing session) +// --------------------------------------------------------------------------- + +/// Caret visual state. +#[derive(Clone, Debug)] +pub struct CaretDecoration { + /// Caret position and size in layout-local coordinates. + /// `x` is the logical caret position — the caret is drawn **centered** + /// on this x coordinate. + pub rect: CaretRect, + /// Whether the caret is currently visible (blink + selection state). + pub visible: bool, +} + +/// Visual decorations for the text node being edited. +/// +/// All geometry is in **layout-local** coordinates. The overlay applies +/// the node world transform + camera view matrix at draw time. +#[derive(Clone, Debug)] +pub struct TextEditingDecorations { + /// The node being edited. + pub node_id: NodeId, + /// Caret decoration (None before the first layout pass). + pub caret: Option, + /// Selection highlight rectangles. + pub selection_rects: Vec, + /// Vertical offset from text vertical alignment (top/center/bottom). + /// Applied as a Y translation before the node transform. + pub y_offset: f32, +} + +// --------------------------------------------------------------------------- +// Overlay renderer +// --------------------------------------------------------------------------- + +/// Selection highlight color: semi-transparent blue (matches OS selection). +const SELECTION_COLOR: Color = Color::from_argb(80, 66, 133, 244); + +/// Caret color: opaque black. +const CARET_COLOR: Color = Color::BLACK; + +/// Caret width in screen pixels (zoom-independent). +const CARET_SCREEN_WIDTH: f32 = DEFAULT_CARET_WIDTH; + +pub struct TextEditDecorationOverlay; + +impl TextEditDecorationOverlay { + /// Draw caret and selection decorations for the active text editing session. + /// + /// The canvas is in **screen space** (no camera transform applied). + /// The overlay computes the full transform chain itself: + /// + /// ```text + /// screen = view_matrix × node_transform × translate(0, y_offset) + /// ``` + pub fn draw( + canvas: &Canvas, + deco: &TextEditingDecorations, + camera: &Camera2D, + cache: &SceneCache, + ) { + // Look up the node's world transform from the layer list. + let entry = cache + .layers + .layers + .iter() + .find(|e| e.id == deco.node_id); + let Some(entry) = entry else { + return; + }; + + let node_matrix = sk::sk_matrix(entry.layer.transform().matrix); + let view_matrix = sk::sk_matrix(camera.view_matrix().matrix); + + // Combined transform: layout-local (with y_offset) → screen. + let y_offset_matrix = Matrix::translate((0.0, deco.y_offset)); + let combined = view_matrix * node_matrix * y_offset_matrix; + + // --- Selection highlights (drawn first, behind the caret) --- + if !deco.selection_rects.is_empty() { + canvas.save(); + canvas.concat(&combined); + + let mut paint = Paint::default(); + paint.set_anti_alias(true); + paint.set_color(SELECTION_COLOR); + paint.set_style(PaintStyle::Fill); + + for sr in &deco.selection_rects { + let rect = Rect::from_xywh(sr.x, sr.y, sr.width, sr.height); + canvas.draw_rect(rect, &paint); + } + + canvas.restore(); + } + + // --- Caret --- + if let Some(ref caret) = deco.caret { + if caret.visible { + // Transform the caret center-line to screen space, then + // draw a fixed-width rect in screen pixels. + let caret_top = skia_safe::Point::new(caret.rect.x, caret.rect.y); + let caret_bottom = + skia_safe::Point::new(caret.rect.x, caret.rect.y + caret.rect.height); + + let screen_top = combined.map_point(caret_top); + let screen_bottom = combined.map_point(caret_bottom); + + // The caret is a vertical line centered on the logical x. + let half_w = CARET_SCREEN_WIDTH / 2.0; + let rect = Rect::from_xywh( + screen_top.x - half_w, + screen_top.y, + CARET_SCREEN_WIDTH, + screen_bottom.y - screen_top.y, + ); + + let mut paint = Paint::default(); + paint.set_anti_alias(false); + paint.set_color(CARET_COLOR); + paint.set_style(PaintStyle::Fill); + + canvas.draw_rect(rect, &paint); + } + } + } +} diff --git a/crates/grida-canvas/src/lib.rs b/crates/grida-canvas/src/lib.rs index 47df42955c..1f7dd03480 100644 --- a/crates/grida-canvas/src/lib.rs +++ b/crates/grida-canvas/src/lib.rs @@ -19,5 +19,6 @@ pub mod sk_tiny; pub mod svg; pub mod sys; pub mod text; +pub mod text_edit_session; pub mod vectornetwork; pub mod window; diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index e46d12ac46..384547c1cf 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -313,6 +313,64 @@ impl Renderer { &self.scene_cache } + /// Update the text content for a node in the layer list and render + /// command tree. + /// + /// Used during text editing to keep the rendered text in sync with the + /// editing session without rebuilding the full layer list. Also + /// invalidates the paragraph cache and picture cache for this node so + /// the next draw uses the fresh text. + /// + /// **Fragility note**: This walks both the flat layer list and the + /// render command tree to patch text in-place. If new + /// `PainterRenderCommand` variants are added, the inner + /// `update_commands` function must be updated to traverse them. + pub fn update_layer_text(&mut self, node_id: NodeId, text: &str) { + use crate::painter::layer::{PainterPictureLayer, PainterRenderCommand}; + + // Update text in the flat layer entries. + for entry in &mut self.scene_cache.layers.layers { + if let PainterPictureLayer::Text(ref mut tl) = entry.layer { + if tl.base.id == node_id { + tl.text = text.to_owned(); + break; + } + } + } + + // Update text in the render command tree (this is what the Painter + // actually draws via draw_render_commands). + fn update_commands(commands: &mut [PainterRenderCommand], node_id: NodeId, text: &str) { + for cmd in commands.iter_mut() { + match cmd { + PainterRenderCommand::Draw(ref mut layer) => { + if let PainterPictureLayer::Text(ref mut tl) = layer { + if tl.base.id == node_id { + tl.text = text.to_owned(); + } + } + } + PainterRenderCommand::MaskGroup(ref mut group) => { + update_commands(&mut group.mask_commands, node_id, text); + update_commands(&mut group.content_commands, node_id, text); + } + } + } + } + update_commands(&mut self.scene_cache.layers.commands, node_id, text); + + // Invalidate the paragraph cache for this node so the Painter + // rebuilds the paragraph with the new text. + self.scene_cache + .paragraph + .borrow_mut() + .invalidate_by_id(node_id); + + // Invalidate the picture cache for this node so the Painter + // doesn't use a stale cached picture. + self.scene_cache.picture.invalidate_node(node_id); + } + pub fn canvas(&self) -> &Canvas { let surface = unsafe { &mut *self.backend.get_surface() }; surface.canvas() @@ -891,7 +949,7 @@ impl Renderer { // Apply camera transform canvas.concat(&sk::sk_matrix(self.camera.view_matrix().matrix)); - // Always use the command pipeline for export to ensure masks are applied + // Always use the command pipeline for export to ensure masks are applied. let painter = Painter::new_with_scene_cache( canvas, &self.fonts, diff --git a/crates/grida-canvas/src/text/attributed_text_conv.rs b/crates/grida-canvas/src/text/attributed_text_conv.rs new file mode 100644 index 0000000000..3fd0dd81f1 --- /dev/null +++ b/crates/grida-canvas/src/text/attributed_text_conv.rs @@ -0,0 +1,421 @@ +//! Bidirectional conversion between `cg` canvas types and +//! `grida_text_edit::attributed_text` types. +//! +//! The two type systems are structurally aligned by design (see +//! `docs/wg/feat-text-editing/attributed-text.md`). This module provides +//! lossless `From`/`Into` implementations so the canvas can construct +//! `AttributedText` from its `TextSpanNodeRec` and vice-versa. + +use crate::cg::types::{ + FontFeature as CgFontFeature, FontOpticalSizing as CgFontOpticalSizing, + FontVariation as CgFontVariation, FontWeight, TextDecorationLine as CgTextDecorationLine, + TextDecorationRec, TextDecorationStyle as CgTextDecorationStyle, + TextLetterSpacing, TextLineHeight, TextStyleRec, TextTransform as CgTextTransform, + TextWordSpacing, +}; + +use grida_text_edit::attributed_text::{ + FontFeature as AttrFontFeature, FontOpticalSizing as AttrFontOpticalSizing, + FontVariation as AttrFontVariation, TextDecorationLine as AttrTextDecorationLine, + TextDecorationStyle as AttrTextDecorationStyle, TextDimension, TextFill, TextStyle, + TextTransform as AttrTextTransform, RGBA, +}; + +// --------------------------------------------------------------------------- +// TextTransform +// --------------------------------------------------------------------------- + +impl From for AttrTextTransform { + fn from(v: CgTextTransform) -> Self { + match v { + CgTextTransform::None => AttrTextTransform::None, + CgTextTransform::Uppercase => AttrTextTransform::Uppercase, + CgTextTransform::Lowercase => AttrTextTransform::Lowercase, + CgTextTransform::Capitalize => AttrTextTransform::Capitalize, + } + } +} + +impl From for CgTextTransform { + fn from(v: AttrTextTransform) -> Self { + match v { + AttrTextTransform::None => CgTextTransform::None, + AttrTextTransform::Uppercase => CgTextTransform::Uppercase, + AttrTextTransform::Lowercase => CgTextTransform::Lowercase, + AttrTextTransform::Capitalize => CgTextTransform::Capitalize, + } + } +} + +// --------------------------------------------------------------------------- +// TextDecorationLine +// --------------------------------------------------------------------------- + +impl From for AttrTextDecorationLine { + fn from(v: CgTextDecorationLine) -> Self { + match v { + CgTextDecorationLine::None => AttrTextDecorationLine::None, + CgTextDecorationLine::Underline => AttrTextDecorationLine::Underline, + CgTextDecorationLine::Overline => AttrTextDecorationLine::Overline, + CgTextDecorationLine::LineThrough => AttrTextDecorationLine::LineThrough, + } + } +} + +impl From for CgTextDecorationLine { + fn from(v: AttrTextDecorationLine) -> Self { + match v { + AttrTextDecorationLine::None => CgTextDecorationLine::None, + AttrTextDecorationLine::Underline => CgTextDecorationLine::Underline, + AttrTextDecorationLine::Overline => CgTextDecorationLine::Overline, + AttrTextDecorationLine::LineThrough => CgTextDecorationLine::LineThrough, + } + } +} + +// --------------------------------------------------------------------------- +// TextDecorationStyle +// --------------------------------------------------------------------------- + +impl From for AttrTextDecorationStyle { + fn from(v: CgTextDecorationStyle) -> Self { + match v { + CgTextDecorationStyle::Solid => AttrTextDecorationStyle::Solid, + CgTextDecorationStyle::Double => AttrTextDecorationStyle::Double, + CgTextDecorationStyle::Dotted => AttrTextDecorationStyle::Dotted, + CgTextDecorationStyle::Dashed => AttrTextDecorationStyle::Dashed, + CgTextDecorationStyle::Wavy => AttrTextDecorationStyle::Wavy, + } + } +} + +impl From for CgTextDecorationStyle { + fn from(v: AttrTextDecorationStyle) -> Self { + match v { + AttrTextDecorationStyle::Solid => CgTextDecorationStyle::Solid, + AttrTextDecorationStyle::Double => CgTextDecorationStyle::Double, + AttrTextDecorationStyle::Dotted => CgTextDecorationStyle::Dotted, + AttrTextDecorationStyle::Dashed => CgTextDecorationStyle::Dashed, + AttrTextDecorationStyle::Wavy => CgTextDecorationStyle::Wavy, + } + } +} + +// --------------------------------------------------------------------------- +// FontOpticalSizing +// --------------------------------------------------------------------------- + +impl From for AttrFontOpticalSizing { + fn from(v: CgFontOpticalSizing) -> Self { + match v { + CgFontOpticalSizing::Auto => AttrFontOpticalSizing::Auto, + CgFontOpticalSizing::None => AttrFontOpticalSizing::None, + CgFontOpticalSizing::Fixed(f) => AttrFontOpticalSizing::Fixed(f), + } + } +} + +impl From for CgFontOpticalSizing { + fn from(v: AttrFontOpticalSizing) -> Self { + match v { + AttrFontOpticalSizing::Auto => CgFontOpticalSizing::Auto, + AttrFontOpticalSizing::None => CgFontOpticalSizing::None, + AttrFontOpticalSizing::Fixed(f) => CgFontOpticalSizing::Fixed(f), + } + } +} + +// --------------------------------------------------------------------------- +// FontFeature +// --------------------------------------------------------------------------- + +impl From for AttrFontFeature { + fn from(v: CgFontFeature) -> Self { + AttrFontFeature { + tag: v.tag, + value: v.value, + } + } +} + +impl From for CgFontFeature { + fn from(v: AttrFontFeature) -> Self { + CgFontFeature { + tag: v.tag, + value: v.value, + } + } +} + +// --------------------------------------------------------------------------- +// FontVariation +// --------------------------------------------------------------------------- + +impl From for AttrFontVariation { + fn from(v: CgFontVariation) -> Self { + AttrFontVariation { + axis: v.axis, + value: v.value, + } + } +} + +impl From for CgFontVariation { + fn from(v: AttrFontVariation) -> Self { + CgFontVariation { + axis: v.axis, + value: v.value, + } + } +} + +// --------------------------------------------------------------------------- +// Spacing / dimension types +// +// cg uses separate enums per dimension; attributed_text uses a unified +// `TextDimension` enum. The mapping is straightforward. +// --------------------------------------------------------------------------- + +impl From for TextDimension { + fn from(v: TextLetterSpacing) -> Self { + match v { + TextLetterSpacing::Fixed(f) => TextDimension::Fixed(f), + TextLetterSpacing::Factor(f) => TextDimension::Factor(f), + } + } +} + +impl From for TextDimension { + fn from(v: TextWordSpacing) -> Self { + match v { + TextWordSpacing::Fixed(f) => TextDimension::Fixed(f), + TextWordSpacing::Factor(f) => TextDimension::Factor(f), + } + } +} + +impl From for TextDimension { + fn from(v: TextLineHeight) -> Self { + match v { + TextLineHeight::Normal => TextDimension::Normal, + TextLineHeight::Fixed(f) => TextDimension::Fixed(f), + TextLineHeight::Factor(f) => TextDimension::Factor(f), + } + } +} + +/// Convert `TextDimension` back to `TextLetterSpacing`. +/// +/// `TextDimension::Normal` maps to `Fixed(0.0)` since `TextLetterSpacing` +/// has no `Normal` variant. +impl From for TextLetterSpacing { + fn from(v: TextDimension) -> Self { + match v { + TextDimension::Normal => TextLetterSpacing::Fixed(0.0), + TextDimension::Fixed(f) => TextLetterSpacing::Fixed(f), + TextDimension::Factor(f) => TextLetterSpacing::Factor(f), + } + } +} + +/// Convert `TextDimension` back to `TextWordSpacing`. +/// +/// `TextDimension::Normal` maps to `Fixed(0.0)`. +impl From for TextWordSpacing { + fn from(v: TextDimension) -> Self { + match v { + TextDimension::Normal => TextWordSpacing::Fixed(0.0), + TextDimension::Fixed(f) => TextWordSpacing::Fixed(f), + TextDimension::Factor(f) => TextWordSpacing::Factor(f), + } + } +} + +impl From for TextLineHeight { + fn from(v: TextDimension) -> Self { + match v { + TextDimension::Normal => TextLineHeight::Normal, + TextDimension::Fixed(f) => TextLineHeight::Fixed(f), + TextDimension::Factor(f) => TextLineHeight::Factor(f), + } + } +} + +// --------------------------------------------------------------------------- +// TextStyleRec <-> TextStyle (the main conversion) +// --------------------------------------------------------------------------- + +/// Convert a `TextStyleRec` (canvas uniform style) into an attributed +/// `TextStyle`. The `fill` field defaults to solid black since +/// `TextStyleRec` does not carry fill — fills live on the node. +/// +/// To provide a per-run fill, set the `fill` field on the returned +/// `TextStyle` after conversion, or use [`text_style_rec_to_attr_with_fill`]. +impl From<&TextStyleRec> for TextStyle { + fn from(rec: &TextStyleRec) -> Self { + let (deco_line, deco_style, deco_color, deco_skip_ink, deco_thickness) = + if let Some(ref d) = rec.text_decoration { + ( + d.text_decoration_line.into(), + d.text_decoration_style + .map(Into::into) + .unwrap_or(AttrTextDecorationStyle::Solid), + d.text_decoration_color.map(|c| RGBA { + r: c.r as f32 / 255.0, + g: c.g as f32 / 255.0, + b: c.b as f32 / 255.0, + a: c.a as f32 / 255.0, + }), + d.text_decoration_skip_ink.unwrap_or(true), + d.text_decoration_thinkness.unwrap_or(1.0), + ) + } else { + ( + AttrTextDecorationLine::None, + AttrTextDecorationStyle::Solid, + None, + true, + 1.0, + ) + }; + + TextStyle { + font_family: rec.font_family.clone(), + font_size: rec.font_size, + font_weight: rec.font_weight.0, + font_width: rec.font_width.unwrap_or(100.0), + font_style_italic: rec.font_style_italic, + font_kerning: rec.font_kerning, + font_optical_sizing: rec.font_optical_sizing.into(), + font_features: rec + .font_features + .as_ref() + .map(|v| v.iter().cloned().map(Into::into).collect()) + .unwrap_or_default(), + font_variations: rec + .font_variations + .as_ref() + .map(|v| v.iter().cloned().map(Into::into).collect()) + .unwrap_or_default(), + letter_spacing: rec.letter_spacing.into(), + word_spacing: rec.word_spacing.into(), + line_height: rec.line_height.clone().into(), + text_decoration_line: deco_line, + text_decoration_style: deco_style, + text_decoration_color: deco_color, + text_decoration_skip_ink: deco_skip_ink, + text_decoration_thickness: deco_thickness, + text_transform: rec.text_transform.into(), + fill: TextFill::default(), // caller must set from node fills + hyperlink: None, + } + } +} + +/// Convert an attributed `TextStyle` back into a canvas `TextStyleRec`. +/// +/// The `fill` and `hyperlink` fields are discarded since `TextStyleRec` +/// does not carry them (fill lives on the node; hyperlink is not yet +/// supported in the canvas schema). +impl From<&TextStyle> for TextStyleRec { + fn from(s: &TextStyle) -> Self { + use crate::cg::color::CGColor; + + let text_decoration = if s.text_decoration_line == AttrTextDecorationLine::None { + None + } else { + Some(TextDecorationRec { + text_decoration_line: s.text_decoration_line.into(), + text_decoration_style: Some(s.text_decoration_style.into()), + text_decoration_color: s.text_decoration_color.map(|c| { + CGColor::from_rgba( + (c.r * 255.0) as u8, + (c.g * 255.0) as u8, + (c.b * 255.0) as u8, + (c.a * 255.0) as u8, + ) + }), + text_decoration_skip_ink: Some(s.text_decoration_skip_ink), + text_decoration_thinkness: Some(s.text_decoration_thickness), + }) + }; + + TextStyleRec { + text_decoration, + font_family: s.font_family.clone(), + font_size: s.font_size, + font_weight: FontWeight(s.font_weight), + font_width: if (s.font_width - 100.0).abs() < f32::EPSILON { + None + } else { + Some(s.font_width) + }, + font_style_italic: s.font_style_italic, + font_kerning: s.font_kerning, + font_optical_sizing: s.font_optical_sizing.into(), + font_features: if s.font_features.is_empty() { + None + } else { + Some(s.font_features.iter().cloned().map(Into::into).collect()) + }, + font_variations: if s.font_variations.is_empty() { + None + } else { + Some(s.font_variations.iter().cloned().map(Into::into).collect()) + }, + letter_spacing: s.letter_spacing.into(), + word_spacing: s.word_spacing.into(), + line_height: s.line_height.into(), + text_transform: s.text_transform.into(), + } + } +} + +/// Convert a `TextStyleRec` to a `TextStyle` with an explicit fill color. +/// +/// This is the preferred conversion when the node's fill paint is known +/// (e.g. the first active solid fill from the node's paint stack). +pub fn text_style_rec_to_attr_with_fill(rec: &TextStyleRec, fill: TextFill) -> TextStyle { + let mut style: TextStyle = rec.into(); + style.fill = fill; + style +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn roundtrip_text_style_rec() { + let rec = TextStyleRec::from_font("Inter", 16.0); + let attr: TextStyle = (&rec).into(); + let rec2: TextStyleRec = (&attr).into(); + + assert_eq!(rec.font_family, rec2.font_family); + assert_eq!(rec.font_size, rec2.font_size); + assert_eq!(rec.font_weight.0, rec2.font_weight.0); + assert_eq!(rec.font_style_italic, rec2.font_style_italic); + } + + #[test] + fn text_dimension_roundtrip_letter_spacing() { + let ls = TextLetterSpacing::Fixed(1.5); + let dim: TextDimension = ls.into(); + let ls2: TextLetterSpacing = dim.into(); + match ls2 { + TextLetterSpacing::Fixed(f) => assert!((f - 1.5).abs() < f32::EPSILON), + _ => panic!("expected Fixed"), + } + } + + #[test] + fn text_dimension_line_height_normal_roundtrip() { + let lh = TextLineHeight::Normal; + let dim: TextDimension = lh.into(); + let lh2: TextLineHeight = dim.into(); + match lh2 { + TextLineHeight::Normal => {} + _ => panic!("expected Normal"), + } + } +} diff --git a/crates/grida-canvas/src/text/mod.rs b/crates/grida-canvas/src/text/mod.rs index 2e93757033..d715de30da 100644 --- a/crates/grida-canvas/src/text/mod.rs +++ b/crates/grida-canvas/src/text/mod.rs @@ -1,3 +1,5 @@ +pub mod attributed_text_conv; +pub mod paragraph_cache_layout; pub mod text_style; pub mod text_transform; diff --git a/crates/grida-canvas/src/text/paragraph_cache_layout.rs b/crates/grida-canvas/src/text/paragraph_cache_layout.rs new file mode 100644 index 0000000000..b47c607850 --- /dev/null +++ b/crates/grida-canvas/src/text/paragraph_cache_layout.rs @@ -0,0 +1,631 @@ +//! `ParagraphCacheLayout` — an adapter that implements +//! [`grida_text_edit::ManagedTextLayout`] using the same paragraph-building +//! code path as the scene's [`ParagraphCache`]. +//! +//! ## Why this exists +//! +//! The text editing session needs geometry queries (caret position, selection +//! rects, hit testing, word boundaries) that match the **exact** paragraph +//! the Painter renders. Previously, `SkiaLayoutEngine` (from `grida-text-edit`) +//! built its *own* paragraphs with its *own* font configuration — a completely +//! separate code path from `ParagraphCache`. This caused: +//! +//! - **Font fallback mismatch**: CJK text renders correctly (Painter has +//! fallbacks) but caret positions are wrong (SkiaLayoutEngine measures tofu). +//! - **Layout width divergence**: Painter uses `width: None` for auto-width +//! nodes (intrinsic sizing); SkiaLayoutEngine used a fixed width. +//! - **Style divergence**: Painter uses `textstyle()` + +//! `TextStyleRecBuildContext`; SkiaLayoutEngine used `TextConfig` + +//! `attr_style_to_skia()`. +//! +//! `ParagraphCacheLayout` eliminates all three by building paragraphs with +//! the **same** `textstyle()` function, `FontRepository::font_collection()`, +//! `TextStyleRecBuildContext` (with `user_fallback_fonts`), and intrinsic +//! width logic that `ParagraphCache::measure()` uses. +//! +//! ## Architecture +//! +//! The adapter holds: +//! - Node properties: `TextStyleRec`, `TextAlign`, `node_width`. +//! - A clone of the `FontCollection` from `FontRepository`. +//! - The user fallback font families from `FontRepository`. +//! - The current Skia `Paragraph` (built with the same code as the Painter). +//! +//! When `ensure_layout()` is called, if text or generation changed, the +//! adapter rebuilds the paragraph using `textstyle()` — identical to +//! `ParagraphCache::measure()`. All geometry queries then hit this paragraph. +//! +//! ## Note on `TextTransform` +//! +//! During editing, `text_transform` is **not** applied. The editing engine +//! works with raw text and raw byte offsets. `ParagraphCache::measure()` +//! applies `transform_text()`, but the editing adapter does not, because +//! transforms can change byte lengths (e.g. ß → SS) which would break +//! cursor offset mapping. When the user commits the edit, the scene re-renders +//! the node with `text_transform` applied as normal. + +use skia_safe; +use skia_safe::textlayout; + +use grida_text_edit::{ + layout::{CaretRect, LineMetrics, ManagedTextLayout, SelectionRect, TextLayoutEngine}, + prev_grapheme_boundary, snap_grapheme_boundary, utf16_to_utf8_offset, utf8_to_utf16_offset, +}; + +use crate::cg::prelude::*; +use crate::runtime::font_repository::FontRepository; +use crate::text::text_style::textstyle; + +// --------------------------------------------------------------------------- +// ParagraphCacheLayout +// --------------------------------------------------------------------------- + +/// Adapter that implements [`ManagedTextLayout`] by building paragraphs with +/// the same code path as `ParagraphCache::measure()` — same fonts, same +/// fallback chain, same `TextStyleRecBuildContext`. +/// +/// Created once per text editing session (one per `text_edit_enter` call). +pub struct ParagraphCacheLayout { + // -- Node properties (immutable for the session's lifetime) -- + text_style: TextStyleRec, + text_align: TextAlign, + /// `None` = auto-width (intrinsic sizing). + node_width: Option, + + // -- Font resources (cloned from FontRepository at session start) -- + font_collection: textlayout::FontCollection, + /// User fallback font families (e.g. CJK coverage fonts). + user_fallback_families: Vec, + + // -- Cached paragraph state -- + /// The Skia paragraph built with the same code as the Painter. + /// Owned exclusively by this adapter — no sharing needed. + paragraph: Option, + /// Text content at the time of the last build. + cached_text: String, + /// `AttributedText::generation()` seen by the last `ensure_layout`. + cached_generation: u64, + /// The layout width currently applied. + layout_width: f32, + /// Viewport height (for PageUp/PageDown). + layout_height: f32, + + // -- Cached line metrics -- + cached_line_metrics: Option>, +} + +impl ParagraphCacheLayout { + /// Create a new adapter for the given text node. + /// + /// The `FontRepository` is used to clone the font collection and extract + /// fallback families. The adapter does NOT hold a reference to the + /// repository — it takes a snapshot at construction time. + /// + /// # Arguments + /// + /// * `text_style` — The node's `TextStyleRec` (from the scene graph). + /// * `text_align` — The node's text alignment. + /// * `node_width` — The node's explicit width (`None` = auto-width). + /// * `layout_height` — Container/viewport height. + /// * `fonts` — The scene's font repository (borrowed for cloning). + pub fn new( + text_style: TextStyleRec, + text_align: TextAlign, + node_width: Option, + layout_height: f32, + fonts: &FontRepository, + ) -> Self { + let layout_width = node_width.unwrap_or(10000.0).max(1.0); + Self { + text_style, + text_align, + node_width, + font_collection: fonts.font_collection().clone(), + user_fallback_families: fonts.user_fallback_families(), + paragraph: None, + cached_text: String::new(), + cached_generation: 0, + layout_width, + layout_height: layout_height.max(1.0), + cached_line_metrics: None, + } + } + + /// Build a Skia paragraph using the **same** code path as + /// `ParagraphCache::measure()`, with **uniform** styling from the + /// node's `TextStyleRec`. + /// + /// Used as the fallback when no `AttributedText` is available (e.g. + /// for the plain-text `ensure_paragraph` path used by `TextLayoutEngine` + /// methods). + fn build_paragraph_uniform(&self, text: &str) -> textlayout::Paragraph { + let mut paragraph_style = textlayout::ParagraphStyle::new(); + paragraph_style.set_text_direction(textlayout::TextDirection::LTR); + paragraph_style.set_text_align(self.text_align.into()); + paragraph_style.set_apply_rounding_hack(false); + + let ctx = TextStyleRecBuildContext { + color: CGColor::TRANSPARENT, + user_fallback_fonts: self.user_fallback_families.clone(), + }; + let mut builder = + textlayout::ParagraphBuilder::new(¶graph_style, &self.font_collection); + let ts = textstyle(&self.text_style, &Some(ctx)); + builder.push_style(&ts); + builder.add_text(text); + let para = builder.build(); + builder.pop(); + para + } + + /// Build a Skia paragraph with **per-run** styling from `AttributedText`. + /// + /// Each run's `TextStyle` is converted to a canvas `TextStyleRec` via + /// the bidirectional conversion in `attributed_text_conv`, then passed + /// through the same `textstyle()` function the Painter uses. This + /// ensures bold, italic, font size, color, and other per-run properties + /// are visually rendered during editing. + /// + /// Falls back to uniform styling if no runs overlap the text range. + fn build_paragraph_attributed( + &self, + content: &grida_text_edit::attributed_text::AttributedText, + ) -> textlayout::Paragraph { + let text = content.text(); + let runs = content.runs(); + + let mut paragraph_style = textlayout::ParagraphStyle::new(); + paragraph_style.set_text_direction(textlayout::TextDirection::LTR); + paragraph_style.set_text_align(self.text_align.into()); + paragraph_style.set_apply_rounding_hack(false); + + let mut builder = + textlayout::ParagraphBuilder::new(¶graph_style, &self.font_collection); + + if runs.is_empty() || text.is_empty() { + // No runs — fall back to uniform node style. + let ctx = Some(TextStyleRecBuildContext { + color: CGColor::TRANSPARENT, + user_fallback_fonts: self.user_fallback_families.clone(), + }); + let ts = textstyle(&self.text_style, &ctx); + builder.push_style(&ts); + builder.add_text(text); + } else { + for run in runs { + let run_start = run.start as usize; + let run_end = run.end as usize; + if run_start >= run_end || run_end > text.len() { + continue; + } + // Convert attributed TextStyle → canvas TextStyleRec → Skia TextStyle. + let run_rec: TextStyleRec = (&run.style).into(); + let ctx = Some(TextStyleRecBuildContext { + color: CGColor::TRANSPARENT, + user_fallback_fonts: self.user_fallback_families.clone(), + }); + let mut ts = textstyle(&run_rec, &ctx); + // Apply the run's fill color (textstyle() doesn't handle fill). + match &run.style.fill { + grida_text_edit::attributed_text::TextFill::Solid(rgba) => { + ts.set_color(skia_safe::Color::from_argb( + (rgba.a * 255.0) as u8, + (rgba.r * 255.0) as u8, + (rgba.g * 255.0) as u8, + (rgba.b * 255.0) as u8, + )); + } + } + builder.push_style(&ts); + builder.add_text(&text[run_start..run_end]); + } + } + + let para = builder.build(); + para + } + + /// Build and layout the paragraph with **per-run** attributed styling, + /// handling intrinsic width for auto-width nodes (same logic as + /// `ParagraphCache::compute_measurements`). + fn rebuild_attributed( + &mut self, + content: &grida_text_edit::attributed_text::AttributedText, + ) { + let text = content.text(); + let mut para = self.build_paragraph_attributed(content); + + let layout_width = if let Some(width) = self.node_width { + width + } else { + para.layout(f32::INFINITY); + let intrinsic = para.max_intrinsic_width(); + if intrinsic < 1.0 { 1.0 } else { intrinsic } + }; + + para.layout(layout_width); + self.layout_width = layout_width.max(1.0); + self.paragraph = Some(para); + self.cached_text = text.to_owned(); + self.cached_line_metrics = None; + } + + /// Build and layout the paragraph with **uniform** node styling, + /// handling intrinsic width for auto-width nodes. + /// + /// Used by the plain-text `ensure_paragraph` path (for `TextLayoutEngine` + /// methods that receive `&str` instead of `AttributedText`). + fn rebuild_uniform(&mut self, text: &str) { + let mut para = self.build_paragraph_uniform(text); + + let layout_width = if let Some(width) = self.node_width { + width + } else { + para.layout(f32::INFINITY); + let intrinsic = para.max_intrinsic_width(); + if intrinsic < 1.0 { 1.0 } else { intrinsic } + }; + + para.layout(layout_width); + self.layout_width = layout_width.max(1.0); + self.paragraph = Some(para); + self.cached_text = text.to_owned(); + self.cached_line_metrics = None; + } + + /// Ensure the paragraph is built for the given text (uniform style). + fn ensure_paragraph(&mut self, text: &str) { + if self.paragraph.is_some() && self.cached_text == text { + return; + } + self.rebuild_uniform(text); + } + + /// Convert Skia's UTF-16 line metrics to UTF-8. + fn compute_line_metrics(&self, text: &str) -> Vec { + let para = match &self.paragraph { + Some(p) => p, + None => return vec![], + }; + let skia_metrics = para.get_line_metrics(); + + if skia_metrics.is_empty() { + return vec![LineMetrics { + start_index: 0, + end_index: 0, + baseline: self.text_style.font_size, + ascent: self.text_style.font_size, + descent: self.text_style.font_size * 0.2, + left: 0.0, + }]; + } + + let mut result = Vec::with_capacity(skia_metrics.len()); + let mut run_u16: usize = 0; + let mut run_byte: usize = 0; + let mut char_iter = text.char_indices().peekable(); + + for lm in &skia_metrics { + let start_u8 = incremental_u16_to_u8( + lm.start_index, + text, + &mut run_u16, + &mut run_byte, + &mut char_iter, + ); + let end_u8 = incremental_u16_to_u8( + lm.end_including_newline, + text, + &mut run_u16, + &mut run_byte, + &mut char_iter, + ) + .min(text.len()); + + result.push(LineMetrics { + start_index: start_u8, + end_index: end_u8, + baseline: lm.baseline as f32, + ascent: lm.ascent as f32, + descent: lm.descent as f32, + left: lm.left as f32, + }); + } + + // Trailing newline phantom line. + if text.ends_with('\n') && !text.is_empty() { + let needs_phantom = result + .last() + .map(|last| last.start_index < text.len()) + .unwrap_or(true); + if needs_phantom { + let (ascent, descent) = result + .last() + .map(|last| (last.ascent, last.descent)) + .unwrap_or((self.text_style.font_size, self.text_style.font_size * 0.2)); + let baseline = result + .last() + .map(|last| last.baseline + last.descent + ascent) + .unwrap_or(ascent); + result.push(LineMetrics { + start_index: text.len(), + end_index: text.len(), + baseline, + ascent, + descent, + left: empty_line_left(&self.text_align, self.layout_width), + }); + } + } + + result + } + + /// Height of the laid-out paragraph content (in layout-local pixels). + /// + /// This is the *content* height (from `Paragraph::height()`), NOT the + /// container height. Use this for vertical alignment offset calculations. + pub fn paragraph_height(&self) -> f32 { + self.paragraph + .as_ref() + .map(|p| p.height()) + .unwrap_or(self.text_style.font_size) + } + + /// Get or compute cached line metrics. + fn line_metrics_cached(&mut self, text: &str) -> Vec { + self.ensure_paragraph(text); + if let Some(ref cached) = self.cached_line_metrics { + return cached.clone(); + } + let metrics = self.compute_line_metrics(text); + self.cached_line_metrics = Some(metrics.clone()); + metrics + } +} + +// --------------------------------------------------------------------------- +// TextLayoutEngine +// --------------------------------------------------------------------------- + +impl TextLayoutEngine for ParagraphCacheLayout { + fn line_metrics(&mut self, text: &str) -> Vec { + self.line_metrics_cached(text) + } + + fn position_at_point(&mut self, text: &str, x: f32, y: f32) -> usize { + // Check empty lines first. + let metrics = self.line_metrics_cached(text); + for lm in &metrics { + let top = lm.baseline - lm.ascent; + let bot = lm.baseline + lm.descent; + if y >= top - 0.5 && y <= bot + 0.5 && lm.is_empty_line(text) { + return lm.start_index; + } + } + + let para = match &self.paragraph { + Some(p) => p, + None => return 0, + }; + let pwa = + para.get_glyph_position_at_coordinate(skia_safe::Point::new(x, y)); + let raw_u16 = pwa.position.max(0) as usize; + let raw_u8 = utf16_to_utf8_offset(text, raw_u16).min(text.len()); + snap_grapheme_boundary(text, raw_u8) + } + + fn caret_rect_at(&mut self, text: &str, offset: usize) -> CaretRect { + let metrics = self.line_metrics_cached(text); + if metrics.is_empty() { + return CaretRect { + x: 0.0, + y: 0.0, + height: self.text_style.font_size, + }; + } + + let idx = metrics + .iter() + .position(|lm| offset < lm.end_index) + .unwrap_or(metrics.len() - 1); + let lm = &metrics[idx]; + + let y = lm.baseline - lm.ascent; + let height = lm.ascent + lm.descent; + + let x = if offset <= lm.start_index { + lm.left + } else { + let para = match &self.paragraph { + Some(p) => p, + None => return CaretRect { x: lm.left, y, height }, + }; + + let u16_end = utf8_to_utf16_offset(text, offset); + let cluster_start = if offset > 0 { + prev_grapheme_boundary(text, offset) + } else { + 0 + }; + let u16_start = utf8_to_utf16_offset(text, cluster_start); + let rects = para.get_rects_for_range( + u16_start..u16_end, + textlayout::RectHeightStyle::Max, + textlayout::RectWidthStyle::Tight, + ); + rects + .iter() + .map(|tb| tb.rect.right()) + .fold(0.0_f32, f32::max) + }; + + CaretRect { x, y, height } + } + + fn word_boundary_at(&mut self, text: &str, offset: usize) -> (usize, usize) { + self.ensure_paragraph(text); + let para = match &self.paragraph { + Some(p) => p, + None => return (0, 0), + }; + let u16_pos = utf8_to_utf16_offset(text, offset) as u32; + let range = para.get_word_boundary(u16_pos); + let start = utf16_to_utf8_offset(text, range.start); + let end = utf16_to_utf8_offset(text, range.end); + (start, end) + } + + fn selection_rects_for_range( + &mut self, + text: &str, + start: usize, + end: usize, + ) -> Vec { + if start >= end { + return Vec::new(); + } + let metrics = self.line_metrics_cached(text); + if metrics.is_empty() { + return Vec::new(); + } + + self.ensure_paragraph(text); + let para = match &self.paragraph { + Some(p) => p, + None => return Vec::new(), + }; + + let u16_lo = utf8_to_utf16_offset(text, start); + let u16_hi = utf8_to_utf16_offset(text, end); + let raw = para.get_rects_for_range( + u16_lo..u16_hi, + textlayout::RectHeightStyle::Max, + textlayout::RectWidthStyle::Tight, + ); + + let mut rects: Vec = raw + .iter() + .map(|tb| SelectionRect { + x: tb.rect.left(), + y: tb.rect.top(), + width: (tb.rect.right() - tb.rect.left()).max(0.0), + height: (tb.rect.bottom() - tb.rect.top()).max(0.0), + }) + .collect(); + + // Empty-line invariant. + let first_line = grida_text_edit::line_index_for_offset_utf8(&metrics, start); + let last_line = grida_text_edit::line_index_for_offset_utf8( + &metrics, + end.saturating_sub(1).max(start), + ); + + for lm in metrics.iter().take(last_line + 1).skip(first_line) { + if !lm.is_empty_line(text) { + continue; + } + let mid_y = lm.baseline - lm.ascent * 0.5; + let already = rects + .iter() + .any(|r| r.y <= mid_y && mid_y <= r.y + r.height); + if !already { + rects.push(SelectionRect { + x: lm.left, + y: lm.baseline - lm.ascent, + width: self.text_style.font_size * 0.5, + height: lm.ascent + lm.descent, + }); + } + } + + rects + } + + fn viewport_height(&self) -> f32 { + self.layout_height + } +} + +// --------------------------------------------------------------------------- +// ManagedTextLayout +// --------------------------------------------------------------------------- + +impl ManagedTextLayout for ParagraphCacheLayout { + fn ensure_layout(&mut self, content: &grida_text_edit::attributed_text::AttributedText) { + let gen = content.generation(); + let text = content.text(); + if self.paragraph.is_some() + && self.cached_text == text + && self.cached_generation == gen + { + return; + } + self.cached_generation = gen; + self.rebuild_attributed(content); + } + + fn invalidate(&mut self) { + self.paragraph = None; + self.cached_text.clear(); + self.cached_generation = 0; + self.cached_line_metrics = None; + } + + fn layout_width(&self) -> f32 { + self.layout_width + } + + fn layout_height(&self) -> f32 { + self.layout_height + } + + fn set_layout_width(&mut self, width: f32) { + let new_w = width.max(1.0); + if (new_w - self.layout_width).abs() > 0.5 { + self.layout_width = new_w; + self.invalidate(); + } + } + + fn set_layout_height(&mut self, height: f32) { + let new_h = height.max(1.0); + if (new_h - self.layout_height).abs() > 0.5 { + self.layout_height = new_h; + } + } +} + +// --------------------------------------------------------------------------- +// Utility +// --------------------------------------------------------------------------- + +/// Advance a running UTF-16/UTF-8 cursor to the target UTF-16 offset. +fn incremental_u16_to_u8( + target_u16: usize, + text: &str, + run_u16: &mut usize, + run_byte: &mut usize, + iter: &mut std::iter::Peekable, +) -> usize { + while *run_u16 < target_u16 { + if let Some(&(byte_idx, ch)) = iter.peek() { + *run_u16 += ch.len_utf16(); + *run_byte = byte_idx + ch.len_utf8(); + iter.next(); + } else { + break; + } + } + (*run_byte).min(text.len()) +} + +/// X offset for an empty line given alignment and layout width. +fn empty_line_left(align: &TextAlign, layout_width: f32) -> f32 { + match align { + TextAlign::Left => 0.0, + TextAlign::Center => layout_width / 2.0, + TextAlign::Right => layout_width, + _ => 0.0, + } +} diff --git a/crates/grida-canvas/src/text_edit_session.rs b/crates/grida-canvas/src/text_edit_session.rs new file mode 100644 index 0000000000..4d8dce9c08 --- /dev/null +++ b/crates/grida-canvas/src/text_edit_session.rs @@ -0,0 +1,129 @@ +//! Text editing session for the canvas. +//! +//! This module provides the canvas-specific text editing integration by +//! re-using the generic [`grida_text_edit::TextEditSession`] with +//! [`ParagraphCacheLayout`] as the layout backend. +//! +//! The generic session already handles all editing concerns: +//! - Text buffer, cursor, selection, attributed text +//! - Undo/redo history with merge grouping +//! - Rich text style toggles (bold, italic, underline, etc.) +//! - Clipboard (copy/cut/paste with HTML formatting) +//! - IME composition (preedit rendering) +//! - Cursor blink timer +//! - Pointer events (click, drag, multi-click) +//! - Geometry queries (caret rect, selection rects) +//! - Scroll management +//! +//! This module adds only canvas-specific concerns: +//! - Constructing the session from a canvas text node's properties +//! - Capturing original text for commit/cancel semantics +//! - The `ActiveTextEdit` bundle that lives on the Application + +use crate::cg::types::TextStyleRec; +use crate::node::schema::NodeId; +use crate::text::attributed_text_conv::text_style_rec_to_attr_with_fill; +use crate::text::paragraph_cache_layout::ParagraphCacheLayout; + +use grida_text_edit::{ + attributed_text::{AttributedText, ParagraphStyle, TextFill}, + text_edit_session::TextEditSession, +}; + +// Re-export for WASM layer (so it doesn't depend on grida-text-edit directly) +pub use grida_text_edit::EditingCommand as EditCommand; +pub use grida_text_edit::DEFAULT_CARET_WIDTH; + +// Re-export the session type for convenience +pub type CanvasTextEditSession = TextEditSession; + +// --------------------------------------------------------------------------- +// ActiveTextEdit — session bundle with canvas-specific lifecycle +// --------------------------------------------------------------------------- + +/// An active text editing session bound to a canvas text node. +/// +/// Bundles the generic [`TextEditSession`] (which owns both the editing +/// state and the layout engine) with canvas-specific lifecycle data +/// (original text for commit/cancel, node ID). +/// +/// The generic session handles all editing, styling, history, blink, IME, +/// pointer events, and geometry queries internally. The canvas layer only +/// needs to: +/// 1. Forward events (commands, pointer, IME) to the session +/// 2. Read decoration data (caret rect, selection rects) for overlay rendering +/// 3. Commit/cancel the session on exit +pub struct ActiveTextEdit { + /// The generic text editing session with ParagraphCacheLayout. + pub session: CanvasTextEditSession, + + /// The canvas node being edited (internal ID, not exposed outside the crate). + pub(crate) node_id: NodeId, + + /// Original text at session start (for commit comparison). + original_text: String, +} + +impl ActiveTextEdit { + /// Create a new editing session from a text node's current state. + /// + /// # Arguments + /// + /// * `node_id` — The internal node ID being edited. + /// * `text` — The node's current plain text. + /// * `text_style_rec` — The node's uniform `TextStyleRec`. + /// * `fill` — The resolved text fill (from the node's paint stack). + /// * `paragraph_style` — Paragraph-level attributes. + /// * `layout` — The pre-configured `ParagraphCacheLayout`. + pub fn new( + node_id: NodeId, + text: &str, + text_style_rec: &TextStyleRec, + fill: TextFill, + paragraph_style: ParagraphStyle, + layout: ParagraphCacheLayout, + ) -> Self { + let attr_style = text_style_rec_to_attr_with_fill(text_style_rec, fill); + let mut content = AttributedText::new(text, attr_style); + *content.paragraph_style_mut() = paragraph_style; + + let mut session = TextEditSession::with_content(layout, content); + + // Select all text when entering edit mode so the user can + // immediately start typing to replace the existing content. + session.select_all(); + + Self { + session, + node_id, + original_text: text.to_owned(), + } + } + + /// The canvas node being edited. + pub fn node_id(&self) -> NodeId { + self.node_id + } + + /// Whether the text has been modified from its original state. + pub fn is_modified(&self) -> bool { + self.session.state.text != self.original_text + } + + /// Commit the session and return the final text (if modified). + /// + /// Returns `Some(text)` if the text was modified, `None` otherwise. + pub fn commit(self) -> Option { + let final_text = self.session.state.text.clone(); + if final_text != self.original_text { + Some(final_text) + } else { + None + } + } + + /// Cancel the session, discarding all changes. + pub fn cancel(self) -> String { + self.original_text + } +} diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index cca24e1028..d6f5101f3a 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -15,6 +15,8 @@ use crate::sys::scheduler; use crate::sys::timer::TimerMgr; use crate::text; use crate::vectornetwork::VectorNetwork; +use grida_text_edit::layout::ManagedTextLayout; +use grida_text_edit::TextLayoutEngine; use crate::window::command::ApplicationCommand; #[cfg(not(target_arch = "wasm32"))] use futures::channel::mpsc; @@ -188,6 +190,17 @@ pub struct UnknownTargetApplication { /// When `true`, this application is driven by a platform-managed tick loop /// and should be freed by that loop after `running` becomes `false`. auto_tick: bool, + + /// Active text editing session and its layout engine, if any. + /// + /// The session and layout are stored together so the overlay renderer + /// can access both during `draw_and_flush_devtools_overlay`. + pub text_edit: Option, + + /// Text editing decorations (caret + selection) for the overlay pass. + /// `None` when no text editing session is active. + text_edit_decorations: + Option, } impl ApplicationApi for UnknownTargetApplication { @@ -196,6 +209,13 @@ impl ApplicationApi for UnknownTargetApplication { fn tick(&mut self, time: f64) { self.clock.tick(time); self.timer.tick(self.clock.now()); + + // Drive the text-edit clock from the host's wall time. + // `time` is milliseconds (from performance.now()); the text-edit + // Instant uses microseconds. On native builds this is a no-op + // (native Instant uses std::time::Instant directly). + #[cfg(target_arch = "wasm32")] + grida_text_edit::time::Instant::set_micros((time * 1000.0) as u64); } /// Update backing resources after a window resize. @@ -616,6 +636,8 @@ impl UnknownTargetApplication { id_mapping_reverse: std::collections::HashMap::new(), running: true, auto_tick: false, + text_edit: None, + text_edit_decorations: None, } } @@ -692,7 +714,7 @@ impl UnknownTargetApplication { } /// Convert user string ID to internal u64 ID - fn user_id_to_internal(&self, user_id: &str) -> Option { + pub fn user_id_to_internal(&self, user_id: &str) -> Option { self.id_mapping.get(user_id).copied() } @@ -1013,6 +1035,17 @@ impl UnknownTargetApplication { self.highlight_stroke_style.as_ref(), ); } + // Text editing decorations (caret + selection) are rendered as + // an overlay — unclipped by parent containers and with a + // zoom-independent caret width. + if let Some(ref deco) = self.text_edit_decorations { + crate::devtools::text_edit_decoration_overlay::TextEditDecorationOverlay::draw( + &canvas, + deco, + &self.renderer.camera, + self.renderer.get_cache(), + ); + } if self.devtools_rendering_show_tiles { tile_overlay::TileOverlay::draw( &canvas, @@ -1100,4 +1133,421 @@ impl UnknownTargetApplication { pub fn get_image_size(&self, id: &str) -> Option<(u32, u32)> { self.renderer.get_image_size(id) } + + // ----------------------------------------------------------------------- + // Text editing — first-class engine feature + // ----------------------------------------------------------------------- + + /// Enter text editing mode for a node. + /// + /// Reads all text properties directly from the scene node to ensure + /// the editing layout engine uses exactly the same configuration as + /// the Painter. Returns `true` on success. + /// + /// The layout adapter (`ParagraphCacheLayout`) builds paragraphs with + /// the **same** `textstyle()`, `FontCollection`, and + /// `TextStyleRecBuildContext` that `ParagraphCache::measure()` uses, + /// eliminating font fallback mismatches and layout divergence. + pub fn text_edit_enter(&mut self, user_node_id: &str) -> bool { + use crate::node::schema::Node; + use crate::text::paragraph_cache_layout::ParagraphCacheLayout; + use crate::text_edit_session::ActiveTextEdit; + + let node_id = match self.user_id_to_internal(user_node_id) { + Some(id) => id, + None => return false, + }; + + // Look up the text node from the scene to get the authoritative + // properties — same data the Painter and ParagraphCache use. + let scene = match self.renderer.scene.as_ref() { + Some(s) => s, + None => return false, + }; + let node = match scene.graph.get_node(&node_id) { + Ok(n) => n, + Err(_) => return false, + }; + let tspan = match node { + Node::TextSpan(t) => t, + _ => return false, + }; + + let text = &tspan.text; + let text_style_rec = &tspan.text_style; + let text_align = &tspan.text_align; + let layout_height = tspan.height.unwrap_or(10000.0); + + let fill = grida_text_edit::attributed_text::TextFill::default(); + let paragraph_style = grida_text_edit::attributed_text::ParagraphStyle::default(); + + // Build the layout adapter using the same font collection and style + // code path as ParagraphCache::measure(). + let layout = ParagraphCacheLayout::new( + text_style_rec.clone(), + *text_align, + tspan.width, // None = auto-width (intrinsic sizing) + layout_height, + &self.renderer.fonts, + ); + + let te = ActiveTextEdit::new( + node_id, + text, + text_style_rec, + fill, + paragraph_style, + layout, + ); + + self.text_edit = Some(te); + self.text_edit_refresh_decorations(); + true + } + + /// Exit text editing mode. + /// + /// If `commit`, returns the final text (if modified). Otherwise cancels. + pub fn text_edit_exit(&mut self, commit: bool) -> Option { + let te = self.text_edit.take()?; + self.text_edit_decorations = None; + self.renderer.queue_unstable(); + + if commit { + te.commit() + } else { + te.cancel(); + None + } + } + + /// Whether a text editing session is active. + pub fn text_edit_is_active(&self) -> bool { + self.text_edit.is_some() + } + + /// Returns the current text of the active editing session, or `None` + /// if no session is active. + pub fn text_edit_get_text(&self) -> Option<&str> { + self.text_edit.as_ref().map(|te| te.session.state.text.as_str()) + } + + /// Dispatch an editing command. + pub fn text_edit_command(&mut self, cmd: grida_text_edit::EditingCommand) { + if let Some(te) = self.text_edit.as_mut() { + te.session.apply(cmd); + } + self.text_edit_refresh_decorations(); + } + + /// Undo within the text editing session. + /// + /// Returns `true` if the session had something to undo. + pub fn text_edit_undo(&mut self) -> bool { + let performed = self + .text_edit + .as_mut() + .is_some_and(|te| te.session.undo()); + self.text_edit_refresh_decorations(); + performed + } + + /// Redo within the text editing session. + /// + /// Returns `true` if the session had something to redo. + pub fn text_edit_redo(&mut self) -> bool { + let performed = self + .text_edit + .as_mut() + .is_some_and(|te| te.session.redo()); + self.text_edit_refresh_decorations(); + performed + } + + /// Pointer down in layout-local coordinates. + pub fn text_edit_pointer_down(&mut self, x: f32, y: f32, shift: bool, click_count: u32) { + if let Some(te) = self.text_edit.as_mut() { + te.session.handle_click(x, y, click_count, shift); + } + self.text_edit_refresh_decorations(); + } + + /// Pointer move during drag (layout-local coordinates). + pub fn text_edit_pointer_move(&mut self, x: f32, y: f32) { + if let Some(te) = self.text_edit.as_mut() { + te.session.on_pointer_move(x, y); + } + self.text_edit_refresh_decorations(); + } + + /// Pointer up. + pub fn text_edit_pointer_up(&mut self) { + if let Some(te) = self.text_edit.as_mut() { + te.session.on_pointer_up(); + } + } + + /// Set IME preedit string. + pub fn text_edit_ime_set_preedit(&mut self, text: String) { + if let Some(te) = self.text_edit.as_mut() { + te.session.update_preedit(text); + } + self.text_edit_sync_display_text(); + self.text_edit_refresh_decorations(); + } + + /// Commit IME composition. + pub fn text_edit_ime_commit(&mut self, text: &str) { + if let Some(te) = self.text_edit.as_mut() { + te.session.apply_with_kind( + grida_text_edit::EditingCommand::Insert(text.to_owned()), + grida_text_edit::EditKind::ImeCommit, + ); + te.session.cancel_preedit(); + } + self.text_edit_refresh_decorations(); + } + + /// Cancel IME composition. + pub fn text_edit_ime_cancel(&mut self) { + if let Some(te) = self.text_edit.as_mut() { + te.session.cancel_preedit(); + } + self.text_edit_sync_display_text(); + self.text_edit_refresh_decorations(); + } + + /// Get selected text (plain). + pub fn text_edit_get_selected_text(&self) -> Option { + let te = self.text_edit.as_ref()?; + te.session.selected_text().map(|s| s.to_owned()) + } + + /// Get selected text as HTML. + pub fn text_edit_get_selected_html(&self) -> Option { + let te = self.text_edit.as_ref()?; + te.session.selected_html() + } + + /// Paste plain text. + pub fn text_edit_paste_text(&mut self, text: &str) { + if text.is_empty() { return; } + if let Some(te) = self.text_edit.as_mut() { + te.session.apply_with_kind( + grida_text_edit::EditingCommand::Insert(text.to_owned()), + grida_text_edit::EditKind::Paste, + ); + } + self.text_edit_refresh_decorations(); + } + + /// Paste HTML with formatting. + pub fn text_edit_paste_html(&mut self, html: &str) { + if html.is_empty() { return; } + if let Some(te) = self.text_edit.as_mut() { + let base_style = te.session.content.default_style().clone(); + match grida_text_edit::attributed_text::html::html_to_attributed_text(html, base_style) { + Ok(pasted) if !pasted.is_empty() => { + te.session.paste_attributed(&pasted); + } + _ => {} // malformed HTML — ignore silently + } + } + self.text_edit_refresh_decorations(); + } + + /// Get caret rect in layout-local coordinates. + pub fn text_edit_get_caret_rect(&mut self) -> Option { + let te = self.text_edit.as_mut()?; + Some(te.session.caret_rect()) + } + + /// Get selection rects in layout-local coordinates. + pub fn text_edit_get_selection_rects(&mut self) -> Option> { + let te = self.text_edit.as_mut()?; + let (lo, hi) = te.session.selection_range()?; + let rects = te.session.layout.selection_rects_for_range(&te.session.state.text, lo, hi); + if rects.is_empty() { None } else { Some(rects) } + } + + /// Toggle bold on selection/caret. + pub fn text_edit_toggle_bold(&mut self) { + if let Some(te) = self.text_edit.as_mut() { te.session.toggle_bold(); } + self.text_edit_after_style_change(); + } + + /// Toggle italic on selection/caret. + pub fn text_edit_toggle_italic(&mut self) { + if let Some(te) = self.text_edit.as_mut() { te.session.toggle_italic(); } + self.text_edit_after_style_change(); + } + + /// Toggle underline on selection/caret. + pub fn text_edit_toggle_underline(&mut self) { + if let Some(te) = self.text_edit.as_mut() { te.session.toggle_underline(); } + self.text_edit_after_style_change(); + } + + /// Toggle strikethrough on selection/caret. + pub fn text_edit_toggle_strikethrough(&mut self) { + if let Some(te) = self.text_edit.as_mut() { te.session.toggle_strikethrough(); } + self.text_edit_after_style_change(); + } + + /// Set font size on selection/caret. + pub fn text_edit_set_font_size(&mut self, size: f32) { + if let Some(te) = self.text_edit.as_mut() { te.session.set_font_size(size); } + self.text_edit_after_style_change(); + } + + /// Set font family on selection/caret. + pub fn text_edit_set_font_family(&mut self, family: &str) { + if let Some(te) = self.text_edit.as_mut() { te.session.set_font_family(family); } + self.text_edit_after_style_change(); + } + + /// Set fill color on selection/caret. + pub fn text_edit_set_color(&mut self, r: f32, g: f32, b: f32, a: f32) { + use grida_text_edit::attributed_text::RGBA; + if let Some(te) = self.text_edit.as_mut() { + te.session.set_color(RGBA { r, g, b, a }); + } + self.text_edit_after_style_change(); + } + + /// Tick the blink timer. Returns `true` if visibility changed. + pub fn text_edit_tick(&mut self) -> bool { + let changed = self + .text_edit + .as_mut() + .map(|te| te.session.tick_blink()) + .unwrap_or(false); + if changed { + if let Some(te) = self.text_edit.as_ref() { + let visible = te.session.should_show_caret(); + if let Some(ref mut deco) = self.text_edit_decorations { + if let Some(ref mut caret) = deco.caret { + caret.visible = visible; + } + } + } + self.renderer.queue_unstable(); + } + changed + } + + // -- Internal helpers -- + + /// Build the display text (committed text + preedit at cursor) and + /// sync it to the layer. Called during IME composition so the user + /// sees each intermediate syllable. + fn text_edit_sync_display_text(&mut self) { + if let Some(te) = self.text_edit.as_ref() { + let node_id = te.node_id(); + let display_text = match te.session.preedit() { + Some(preedit) if !preedit.is_empty() => { + let committed = &te.session.state.text; + let cursor = te.session.state.cursor; + let mut buf = String::with_capacity(committed.len() + preedit.len()); + buf.push_str(&committed[..cursor]); + buf.push_str(preedit); + buf.push_str(&committed[cursor..]); + buf + } + _ => te.session.state.text.clone(), + }; + self.renderer.update_layer_text(node_id, &display_text); + } + self.renderer.queue_unstable(); + } + + fn text_edit_after_style_change(&mut self) { + if let Some(te) = self.text_edit.as_mut() { + te.session.layout.invalidate(); + te.session.layout.ensure_layout(&te.session.content); + } + self.text_edit_refresh_decorations(); + } + + fn text_edit_refresh_decorations(&mut self) { + // The generic session's apply() already calls ensure_layout internally, + // but we still need to compute decoration data for the overlay. + + // Split borrows: extract data from `text_edit`, then access `renderer`. + let deco_data = self.text_edit.as_mut().map(|te| { + // Ensure layout is up to date. + te.session.layout.ensure_layout(&te.session.content); + let node_id = te.node_id(); + let paragraph_height = te.session.layout.paragraph_height(); + // Use display text (committed + preedit) so intermediate IME + // syllables remain visible when decorations are refreshed. + let display_text = match te.session.preedit() { + Some(preedit) if !preedit.is_empty() => { + let committed = &te.session.state.text; + let cursor = te.session.state.cursor; + let mut buf = String::with_capacity(committed.len() + preedit.len()); + buf.push_str(&committed[..cursor]); + buf.push_str(preedit); + buf.push_str(&committed[cursor..]); + buf + } + _ => te.session.state.text.clone(), + }; + let caret = te.session.caret_rect(); + let visible = te.session.should_show_caret(); + let selection_rects = te.session.selection_range().map(|(lo, hi)| { + te.session.layout.selection_rects_for_range(&te.session.state.text, lo, hi) + }).unwrap_or_default(); + (node_id, paragraph_height, display_text, caret, visible, selection_rects) + }); + + if let Some((node_id, paragraph_height, display_text, caret, visible, selection_rects)) = + deco_data + { + self.renderer.update_layer_text(node_id, &display_text); + + let y_offset = + Self::compute_text_y_offset(&self.renderer, node_id, paragraph_height); + + use crate::devtools::text_edit_decoration_overlay::{ + CaretDecoration, TextEditingDecorations, + }; + + self.text_edit_decorations = Some(TextEditingDecorations { + node_id, + caret: Some(CaretDecoration { rect: caret, visible }), + selection_rects, + y_offset, + }); + } + self.renderer.queue_unstable(); + } + + /// Compute the text vertical alignment offset for a text node. + fn compute_text_y_offset( + renderer: &Renderer, + node_id: NodeId, + paragraph_height: f32, + ) -> f32 { + use crate::node::schema::Node; + let scene = match renderer.scene.as_ref() { + Some(s) => s, + None => return 0.0, + }; + let node = match scene.graph.get_node(&node_id) { + Ok(n) => n, + Err(_) => return 0.0, + }; + match node { + Node::TextSpan(t) => match t.height { + Some(h) => match t.text_align_vertical { + TextAlignVertical::Top => 0.0, + TextAlignVertical::Center => (h - paragraph_height) / 2.0, + TextAlignVertical::Bottom => h - paragraph_height, + }, + None => 0.0, + }, + _ => 0.0, + } + } } diff --git a/crates/grida-text-edit/Cargo.toml b/crates/grida-text-edit/Cargo.toml index 6abf8e17f4..0d8c29014c 100644 --- a/crates/grida-text-edit/Cargo.toml +++ b/crates/grida-text-edit/Cargo.toml @@ -10,7 +10,11 @@ description = "Platform-agnostic text editing engine." unicode-segmentation = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -skia-safe = { version = "0.91.0", features = ["textlayout"] } +skia-safe = { version = "0.91.0", features = ["textlayout"], optional = true } + +[features] +default = ["skia"] +skia = ["dep:skia-safe"] [dev-dependencies] arboard = "3" diff --git a/crates/grida-text-edit/examples/wd_text_editor.rs b/crates/grida-text-edit/examples/wd_text_editor.rs index 1f445c2499..830a725bf8 100644 --- a/crates/grida-text-edit/examples/wd_text_editor.rs +++ b/crates/grida-text-edit/examples/wd_text_editor.rs @@ -34,7 +34,6 @@ use glutin_winit::DisplayBuilder; use raw_window_handle::HasRawWindowHandle; use skia_safe::{ gpu::{self, backend_render_targets, gl::FramebufferInfo, surfaces::wrap_backend_render_target}, - textlayout::{RectHeightStyle, RectWidthStyle}, Color, ColorType, Paint, Point, Rect, Surface, }; use winit::{ @@ -47,10 +46,8 @@ use winit::{ }; use grida_text_edit::{ - utf8_to_utf16_offset, TextLayoutEngine, - selection_rects::{ - EmptyLineSelectionPolicy, selection_rects_with_policy, skia_line_index_for_u16_offset, - }, + TextLayoutEngine, SkiaLayoutEngine, + selection_rects::EmptyLineSelectionPolicy, text_edit_session::{ClickTracker, KeyAction, KeyName, TextEditSession}, attributed_text::{ AttributedText, TextStyle as AttrTextStyle, RGBA, @@ -66,7 +63,7 @@ const WINDOW_W: u32 = 800; const WINDOW_H: u32 = 600; const PADDING: f32 = 24.0; const FONT_SIZE: f32 = 18.0; -const CURSOR_WIDTH: f32 = 2.0; +const CURSOR_WIDTH: f32 = 1.0; // --------------------------------------------------------------------------- // GL + Skia surface helpers (purely windowing boilerplate) @@ -115,22 +112,11 @@ impl GlSkiaSurface { fn draw_session( canvas: &skia_safe::Canvas, - session: &mut TextEditSession, - policy: EmptyLineSelectionPolicy, + session: &mut TextEditSession, + _policy: EmptyLineSelectionPolicy, ) { canvas.clear(Color::WHITE); - canvas.save(); - canvas.clip_rect( - Rect::from_xywh( - PADDING, - PADDING, - session.layout.layout_width, - session.layout.layout_height, - ), - None, - None, - ); let origin = Point::new(PADDING, PADDING - session.scroll_y()); let preedit = session @@ -139,40 +125,33 @@ fn draw_session( .map(str::to_owned); if let Some(ref p) = preedit { - // ---- preedit mode ---- - let (display_text, dp, preedit_range) = - session.layout.build_preedit_paragraph( + // ---- preedit mode (per-block, same path as normal) ---- + // Build per-block layout with preedit text spliced in and + // underlined, producing identical metrics to the normal path. + let (display_text, preedit_range) = + session.layout.rebuild_blocks_with_preedit( &session.content, session.state.cursor, p, ); - let preedit_end_u16 = utf8_to_utf16_offset(&display_text, preedit_range.end); - - // Selection + // Selection (using the display_text which includes preedit) if let Some((lo, hi)) = session.selection_range() { if lo < hi { - let u16_lo = utf8_to_utf16_offset(&display_text, lo); - let u16_hi = utf8_to_utf16_offset(&display_text, hi); - let rects = selection_rects_with_policy( - &dp, - &display_text, - u16_lo, - u16_hi, - session.layout.layout_width, - FONT_SIZE, - policy, - ); + let sel_rects = + session + .layout + .selection_rects_for_range(&display_text, lo, hi); let mut sp = Paint::default(); sp.set_color(Color::from_argb(80, 66, 133, 244)); sp.set_anti_alias(true); - for r in &rects { + for r in &sel_rects { canvas.draw_rect( Rect::from_ltrb( - r.left + origin.x, - r.top + origin.y, - r.right + origin.x, - r.bottom + origin.y, + r.x + origin.x, + r.y + origin.y, + r.x + r.width + origin.x, + r.y + r.height + origin.y, ), &sp, ); @@ -180,41 +159,24 @@ fn draw_session( } } - dp.paint(canvas, origin); - - // Cursor at end of preedit - let preedit_start_u16 = utf8_to_utf16_offset(&display_text, preedit_range.start); - let cx = if preedit_start_u16 < preedit_end_u16 { - let rects = dp.get_rects_for_range( - (preedit_end_u16 - 1)..preedit_end_u16, - RectHeightStyle::Max, - RectWidthStyle::Tight, - ); - rects.last().map(|tb| tb.rect.right()).unwrap_or(0.0) - } else { - 0.0 - }; - let cy = { - let skia_metrics = dp.get_line_metrics(); - if skia_metrics.is_empty() { - FONT_SIZE - } else { - let idx = skia_line_index_for_u16_offset(&skia_metrics, preedit_end_u16); - skia_metrics[idx].baseline as f32 - } - }; + // Text (per-block paint, same as normal mode) + session + .layout + .paint_paragraph_at(canvas, &display_text, origin); + + // Cursor at end of preedit (use caret_rect_at with display text) + let preedit_end = preedit_range.end; + let cr = session.layout.caret_rect_at(&display_text, preedit_end); + let cursor_rect = Rect::from_xywh( + cr.x + origin.x - CURSOR_WIDTH / 2.0, + cr.y + origin.y, + CURSOR_WIDTH, + cr.height, + ); let mut cp = Paint::default(); cp.set_color(Color::BLACK); cp.set_anti_alias(false); - canvas.draw_rect( - Rect::from_xywh( - cx + origin.x - CURSOR_WIDTH / 2.0, - cy - FONT_SIZE + origin.y, - CURSOR_WIDTH, - FONT_SIZE * 1.2, - ), - &cp, - ); + canvas.draw_rect(cursor_rect, &cp); } else { // ---- normal mode (rich text, per-block layout) ---- session @@ -251,7 +213,7 @@ fn draw_session( .paint_paragraph_at(canvas, &session.state.text, origin); // Cursor - if session.cursor_visible() && !session.has_selection() { + if session.should_show_caret() { let cr = session.caret_rect(); let cursor_rect = Rect::from_xywh( cr.x + origin.x - CURSOR_WIDTH / 2.0, @@ -265,8 +227,6 @@ fn draw_session( canvas.draw_rect(cursor_rect, &cp); } } - - canvas.restore(); } // --------------------------------------------------------------------------- @@ -285,7 +245,7 @@ struct TextEditorApp { struct AppInner { window: Window, gl_skia: GlSkiaSurface, - session: TextEditSession, + session: TextEditSession, } impl TextEditorApp { @@ -437,7 +397,8 @@ impl ApplicationHandler for TextEditorApp { let layout_w = (w as f32) - PADDING * 2.0; let layout_h = (h as f32) - PADDING * 2.0; - let mut session = TextEditSession::new(layout_w, layout_h, default_style.clone()); + let layout = SkiaLayoutEngine::new(layout_w, layout_h); + let mut session = TextEditSession::new(layout, default_style.clone()); // Load fonts let inter_upright = include_bytes!( @@ -563,6 +524,23 @@ impl ApplicationHandler for TextEditorApp { self.modifiers = m.state(); } + // --------------------------------------------------------- + // IME (Input Method Editor) composition events + // --------------------------------------------------------- + // On macOS, winit 0.30 suppresses KeyboardInput during active + // IME composition at the platform level (view.rs key_down). + // We only receive Preedit / Commit here; no duplicate + // KeyboardInput for composed characters. + // + // Known issue (winit 0.30, macOS): when a non-composable + // character (e.g. ".", "?", symbols) finalizes an active + // composition, winit's insertText handler commits the + // composed text but silently drops the finalizing character. + // Neither Ime::Commit nor KeyboardInput is emitted for it, + // so it is lost. The user must press the key a second time. + // This is a winit bug — our handler is correct. + // --------------------------------------------------------- + WindowEvent::Ime(Ime::Preedit(text, _cursor_range)) => { if inner.session.handle_key_action(KeyAction::ImePreedit(text)) { inner.window.request_redraw(); @@ -574,9 +552,13 @@ impl ApplicationHandler for TextEditorApp { } } WindowEvent::Ime(Ime::Enabled) => { - inner.session.cancel_preedit(); + // No-op. On macOS, Ime::Enabled fires when the first + // preedit begins — cancelling preedit here would race + // with the Preedit event and drop initial characters. } WindowEvent::Ime(Ime::Disabled) => { + // Input source switched away from CJK. Clear any + // in-progress composition. inner.session.cancel_preedit(); inner.window.request_redraw(); } @@ -584,6 +566,11 @@ impl ApplicationHandler for TextEditorApp { WindowEvent::KeyboardInput { event: ke, .. } if ke.state == ElementState::Pressed => { + // Drain the empty-preedit sentinel *before* processing + // keys so that the session's preedit.is_some() guard + // doesn't suppress the next insertion. + inner.session.drain_empty_preedit(); + let meta = self.modifiers.super_key(); let alt = self.modifiers.alt_key(); let ctrl = self.modifiers.control_key(); @@ -607,10 +594,8 @@ impl ApplicationHandler for TextEditorApp { let plain = inner.session.selected_text().unwrap_or("").to_string(); let _ = self.clipboard.set_html(&html, Some(&plain)); } - if inner.session.has_selection() { - inner.session.apply(EditingCommand::Delete); - inner.window.request_redraw(); - } + inner.session.apply(EditingCommand::DeleteByCut); + inner.window.request_redraw(); handled = true; } PhysicalKey::Code(KeyCode::KeyV) => { @@ -657,8 +642,6 @@ impl ApplicationHandler for TextEditorApp { } } } - - inner.session.drain_empty_preedit(); } WindowEvent::MouseWheel { delta, .. } => { diff --git a/crates/grida-text-edit/src/history.rs b/crates/grida-text-edit/src/history.rs index 11673a6789..2d2dadcb59 100644 --- a/crates/grida-text-edit/src/history.rs +++ b/crates/grida-text-edit/src/history.rs @@ -17,6 +17,9 @@ pub enum EditKind { Paste, ImeCommit, Newline, + /// Cut (deleteByCut) — selection deleted via clipboard cut. + /// Never merges with adjacent edits. + Cut, /// A style-only change (bold, italic, font size, etc.). /// Never merges with text-editing kinds. Style, diff --git a/crates/grida-text-edit/src/layout.rs b/crates/grida-text-edit/src/layout.rs index 7270cfd584..46230ad087 100644 --- a/crates/grida-text-edit/src/layout.rs +++ b/crates/grida-text-edit/src/layout.rs @@ -43,6 +43,12 @@ impl LineMetrics { } } +/// Default caret width in screen pixels. +/// +/// Renderers (Skia overlay, WASM FFI, etc.) should use this constant as +/// the default caret width unless the caller overrides it. +pub const DEFAULT_CARET_WIDTH: f32 = 2.0; + /// Caret geometry returned by [`TextLayoutEngine::caret_rect_at`]. /// /// All coordinates are in **layout-local space** (origin at top-left of the @@ -118,6 +124,45 @@ pub trait TextLayoutEngine { fn viewport_height(&self) -> f32; } +// --------------------------------------------------------------------------- +// ManagedTextLayout — layout lifecycle trait +// --------------------------------------------------------------------------- + +/// Extended layout engine trait that adds lifecycle management. +/// +/// `TextLayoutEngine` provides read-only geometry queries (caret position, +/// selection rects, etc.). `ManagedTextLayout` extends it with layout +/// invalidation, rebuild, and sizing — everything `TextEditSession` needs +/// to orchestrate the full editing loop. +/// +/// Implementors: +/// - `SimpleLayoutEngine` — trivial no-ops (test-only, monospace). +/// - `SkiaLayoutEngine` — Skia Paragraph per-block layout (behind `skia` feature). +/// - Canvas-side adapters — delegate to the scene's paragraph cache. +pub trait ManagedTextLayout: TextLayoutEngine { + /// Ensure layout is up-to-date for the given attributed text. + /// + /// Implementations may cache and skip rebuild if content hasn't changed + /// (e.g. by checking `AttributedText::generation()`). + fn ensure_layout(&mut self, content: &crate::attributed_text::AttributedText); + + /// Invalidate all cached layout. The next `ensure_layout` call will + /// rebuild from scratch. + fn invalidate(&mut self); + + /// The current layout width (the wrap boundary). + fn layout_width(&self) -> f32; + + /// The current layout height (viewport/container height). + fn layout_height(&self) -> f32; + + /// Update layout width. Implementations should invalidate if changed. + fn set_layout_width(&mut self, width: f32); + + /// Update layout height. + fn set_layout_height(&mut self, height: f32); +} + // --------------------------------------------------------------------------- // Utility: find the line index for a UTF-8 offset // --------------------------------------------------------------------------- diff --git a/crates/grida-text-edit/src/lib.rs b/crates/grida-text-edit/src/lib.rs index fe0b40cfca..fe6d48b7ad 100644 --- a/crates/grida-text-edit/src/lib.rs +++ b/crates/grida-text-edit/src/lib.rs @@ -1,24 +1,30 @@ pub mod attributed_text; pub mod history; pub mod layout; -pub mod selection_rects; pub mod simple_layout; -pub mod skia_layout; pub mod text_edit_session; pub mod time; +#[cfg(feature = "skia")] +pub mod selection_rects; +#[cfg(feature = "skia")] +pub mod skia_layout; + #[cfg(test)] mod tests; #[cfg(test)] mod session_tests; -pub use history::{EditHistory, EditKind, GenericEditHistory}; -pub use layout::{line_index_for_offset, CaretRect, LineMetrics, SelectionRect, TextLayoutEngine}; -pub use selection_rects::{EmptyLineSelectionPolicy, selection_rects_with_policy, skia_line_index_for_u16_offset}; +pub use history::EditKind; +pub use layout::{line_index_for_offset, CaretRect, LineMetrics, ManagedTextLayout, SelectionRect, TextLayoutEngine, DEFAULT_CARET_WIDTH}; pub use simple_layout::SimpleLayoutEngine; -pub use skia_layout::SkiaLayoutEngine; pub use text_edit_session::{ClickTracker, KeyAction, KeyName, TextEditSession}; +#[cfg(feature = "skia")] +pub use selection_rects::{EmptyLineSelectionPolicy, selection_rects_with_policy, skia_line_index_for_u16_offset}; +#[cfg(feature = "skia")] +pub use skia_layout::SkiaLayoutEngine; + use unicode_segmentation::UnicodeSegmentation; // --------------------------------------------------------------------------- @@ -262,6 +268,23 @@ impl TextEditorState { self.anchor.map_or(false, |a| a != self.cursor) } + /// Whether the caret should be shown. + /// + /// Returns `true` when there is **no** active selection (caret mode). + /// When the user has selected text, the caret is hidden — only the + /// selection highlight is visible. This is the standard behaviour of + /// every major OS text editor (macOS, Windows, Linux). + /// + /// Consumers should combine this with the blink state to decide whether + /// to actually paint the caret: + /// + /// ```text + /// let paint = state.should_show_caret() && blink_visible; + /// ``` + pub fn should_show_caret(&self) -> bool { + !self.has_selection() + } + pub fn selection_range(&self) -> Option<(usize, usize)> { self.anchor.map(|a| { let lo = a.min(self.cursor); @@ -292,6 +315,11 @@ pub enum EditingCommand { BackspaceWord, Delete, DeleteWord, + /// Delete the current selection (cut semantics per W3C Input Events L2 + /// `deleteByCut`). When the selection is collapsed this is a no-op — + /// the caller is responsible for having already copied text to the + /// clipboard before dispatching this command. + DeleteByCut, MoveLeft { extend: bool }, MoveRight { extend: bool }, MoveDocStart { extend: bool }, @@ -349,6 +377,7 @@ impl EditingCommand { Self::Delete | Self::DeleteWord | Self::DeleteLine => { Some(EditKind::Delete) } + Self::DeleteByCut => Some(EditKind::Cut), _ => None, } } @@ -480,6 +509,20 @@ pub fn apply_command_mut( } } + // deleteByCut (W3C Input Events L2): only acts when a + // non-collapsed selection exists; otherwise it's a no-op. + EditingCommand::DeleteByCut => { + if s.has_selection() { + let (lo, hi) = s.selection_range().unwrap(); + let removed = hi - lo; + s.cursor = delete_selection_in_place(s); + s.anchor = None; + Some(EditDelta { offset: lo, old_len: removed, new_len: 0 }) + } else { + None + } + } + EditingCommand::MoveLeft { extend } => { if !extend && s.has_selection() { let lo = s.selection_range().map_or(s.cursor, |(lo, _)| lo); diff --git a/crates/grida-text-edit/src/session_tests.rs b/crates/grida-text-edit/src/session_tests.rs index 259a1a26f5..c406817a26 100644 --- a/crates/grida-text-edit/src/session_tests.rs +++ b/crates/grida-text-edit/src/session_tests.rs @@ -21,6 +21,7 @@ use crate::attributed_text::{ AttributedText, TextDecorationLine, TextFill, TextStyle as AttrTextStyle, RGBA, }; use crate::layout::TextLayoutEngine; +use crate::simple_layout::SimpleLayoutEngine; use crate::text_edit_session::{ClickTracker, KeyAction, KeyName, TextEditSession}; // --------------------------------------------------------------------------- @@ -36,9 +37,10 @@ fn default_style() -> AttrTextStyle { } /// Create a session with some text, cursor at end. -fn session(text: &str) -> TextEditSession { +fn session(text: &str) -> TextEditSession { let style = default_style(); - let mut s = TextEditSession::new(400.0, 300.0, style.clone()); + let layout = SimpleLayoutEngine::new(300.0, 24.0, 10.0); + let mut s = TextEditSession::new(layout, style.clone()); if !text.is_empty() { s.content = AttributedText::new(text, style); s.state.text = text.to_string(); @@ -48,7 +50,7 @@ fn session(text: &str) -> TextEditSession { } /// Assert the critical invariant: content.text() == state.text. -fn assert_content_synced(s: &TextEditSession) { +fn assert_content_synced(s: &TextEditSession) { assert_eq!( s.content.text(), &s.state.text, @@ -702,6 +704,88 @@ fn drain_empty_preedit_only_clears_empty() { assert_eq!(s.preedit(), Some("ko")); } +// =========================================================================== +// IME caret positioning — preedit-aware caret_rect +// =========================================================================== + +#[test] +fn preedit_caret_advances_past_composed_text() { + let mut s = session("abc"); + // Cursor at end → offset 3 + assert_eq!(s.state.cursor, 3); + let cr_before = s.caret_rect(); + + // Simulate Korean IME: preedit "한" (3 UTF-8 bytes) + s.update_preedit("\u{D55C}".into()); + let cr_preedit = s.caret_rect(); + + // Caret must advance past the preedit text. + assert!( + cr_preedit.x > cr_before.x, + "caret should advance past preedit: before.x={}, preedit.x={}", + cr_before.x, + cr_preedit.x, + ); + + // Same line — y should not change. + assert_eq!(cr_preedit.y, cr_before.y); +} + +#[test] +fn preedit_caret_mid_text() { + let mut s = session("abcd"); + // Place cursor between 'b' and 'c' → offset 2 + s.state.cursor = 2; + let cr_before = s.caret_rect(); + + // Preedit inserted at cursor position + s.update_preedit("XY".into()); + let cr_preedit = s.caret_rect(); + + // Caret should be further right (past the preedit). + assert!( + cr_preedit.x > cr_before.x, + "mid-text preedit caret should advance: before.x={}, preedit.x={}", + cr_before.x, + cr_preedit.x, + ); +} + +#[test] +fn preedit_cancel_restores_caret() { + let mut s = session("abc"); + let cr_before = s.caret_rect(); + + s.update_preedit("XY".into()); + let cr_preedit = s.caret_rect(); + assert!(cr_preedit.x > cr_before.x); + + // Cancel preedit → caret returns to the committed cursor position. + s.cancel_preedit(); + let cr_after = s.caret_rect(); + assert_eq!(cr_after.x, cr_before.x); + assert_eq!(cr_after.y, cr_before.y); +} + +#[test] +fn preedit_caret_cache_invalidated_on_update() { + let mut s = session("abc"); + let _cr1 = s.caret_rect(); // populate cache + + s.update_preedit("X".into()); + let cr2 = s.caret_rect(); + + // Update preedit with longer text → cache must be invalidated. + s.update_preedit("XYZ".into()); + let cr3 = s.caret_rect(); + assert!( + cr3.x > cr2.x, + "caret should advance with longer preedit: cr2.x={}, cr3.x={}", + cr2.x, + cr3.x, + ); +} + // =========================================================================== // Scroll management // =========================================================================== diff --git a/crates/grida-text-edit/src/simple_layout.rs b/crates/grida-text-edit/src/simple_layout.rs index d480fc6577..09e702ce96 100644 --- a/crates/grida-text-edit/src/simple_layout.rs +++ b/crates/grida-text-edit/src/simple_layout.rs @@ -10,7 +10,7 @@ //! and wrong for real rendering — its only purpose is to produce deterministic, //! inspectable results for unit tests. -use crate::layout::{CaretRect, LineMetrics, SelectionRect, TextLayoutEngine}; +use crate::layout::{CaretRect, LineMetrics, ManagedTextLayout, SelectionRect, TextLayoutEngine}; use unicode_segmentation::UnicodeSegmentation; @@ -221,6 +221,34 @@ impl TextLayoutEngine for SimpleLayoutEngine { } } +impl ManagedTextLayout for SimpleLayoutEngine { + fn ensure_layout(&mut self, _content: &crate::attributed_text::AttributedText) { + // No-op: monospace layout needs no rebuild. + } + + fn invalidate(&mut self) { + // No-op: stateless. + } + + fn layout_width(&self) -> f32 { + // SimpleLayoutEngine has no wrapping, but we provide a nominal width. + // This is only used for scroll calculations. + f32::MAX + } + + fn layout_height(&self) -> f32 { + self.viewport_height + } + + fn set_layout_width(&mut self, _width: f32) { + // No-op: monospace layout doesn't wrap. + } + + fn set_layout_height(&mut self, height: f32) { + self.viewport_height = height; + } +} + /// Helper: test-friendly constructor with typical values (18px font, 8px char). impl SimpleLayoutEngine { pub fn default_test() -> Self { diff --git a/crates/grida-text-edit/src/skia_layout.rs b/crates/grida-text-edit/src/skia_layout.rs index d2c95965a7..1bb81df5fe 100644 --- a/crates/grida-text-edit/src/skia_layout.rs +++ b/crates/grida-text-edit/src/skia_layout.rs @@ -355,7 +355,128 @@ impl SkiaLayoutEngine { self.rebuild_blocks(text); } - /// Full rebuild: split `text` on `\n` and lay out each block. + // ------------------------------------------------------------------- + // Shared block-building helpers + // ------------------------------------------------------------------- + + /// Finalize a built paragraph into a `ParaBlock`. + /// + /// Converts Skia line metrics, synthesizes empty-line metrics if needed, + /// adjusts `end_index` to include trailing `\n`, and computes block height. + /// This is the single source of truth for the per-block post-processing + /// that was previously duplicated across `rebuild_blocks`, + /// `rebuild_blocks_attributed`, and `notify_edit`. + fn finalize_block( + &self, + para: Paragraph, + text: &str, + start: usize, + end: usize, + has_newline: bool, + y_offset: f32, + ) -> ParaBlock { + let content_end = if has_newline { end - 1 } else { end }; + let content_slice = &text[start..content_end]; + let mut stored_lines = self.convert_block_line_metrics(¶, content_slice, start); + + // Empty content (e.g. line between two `\n`s): Skia may return 0 + // lines for "". Synthesize one so the block has vertical extent. + if stored_lines.is_empty() { + let skia_metrics = para.get_line_metrics(); + let (ascent, descent, baseline) = if let Some(lm) = skia_metrics.first() { + (lm.ascent as f32, lm.descent as f32, lm.baseline as f32) + } else { + (self.font_size, self.font_size * 0.2, self.font_size) + }; + stored_lines.push(LineMetrics { + start_index: start, + end_index: start, + baseline, + ascent, + descent, + left: self.config.text_align.empty_line_left(self.layout_width), + }); + } + + // For the flattened line_metrics view, the line that owns the `\n` + // must have its end_index include the `\n` byte. + if has_newline { + if let Some(last) = stored_lines.last_mut() { + last.end_index = end; + } + } + + let height: f32 = if let Some(last) = stored_lines.last() { + last.baseline + last.descent + } else { + self.font_size * 1.2 + }; + + ParaBlock { + byte_start: start, + byte_end: end, + paragraph: para, + y_offset, + height, + line_metrics: stored_lines, + } + } + + /// Append a phantom empty block for trailing `\n`, so the cursor can + /// sit on the blank line after the last newline. + /// + /// Returns the new y_offset after the phantom block (if appended). + fn append_phantom_trailing_block(&mut self, text: &str, y_offset: f32) -> f32 { + if !text.ends_with('\n') || text.is_empty() { + return y_offset; + } + if let Some(last_block) = self.blocks.last() { + let last_lm = last_block.line_metrics.last(); + let (ascent, descent) = last_lm + .map(|lm| (lm.ascent, lm.descent)) + .unwrap_or((self.font_size, self.font_size * 0.2)); + let phantom = LineMetrics { + start_index: text.len(), + end_index: text.len(), + baseline: ascent, + ascent, + descent, + left: self.config.text_align.empty_line_left(self.layout_width), + }; + let phantom_height = ascent + descent; + self.blocks.push(ParaBlock { + byte_start: text.len(), + byte_end: text.len(), + paragraph: self.build_paragraph_for_slice(""), + y_offset, + height: phantom_height, + line_metrics: vec![phantom], + }); + return y_offset + phantom_height; + } + y_offset + } + + /// Remove a phantom trailing block if one exists at the end of the + /// cached text. Returns `true` if one was removed. + fn remove_phantom_trailing_block(&mut self) -> bool { + if let Some(last) = self.blocks.last() { + if last.byte_start == last.byte_end + && last.byte_start == self.cached_text.len() + && self.cached_text.ends_with('\n') + { + self.blocks.pop(); + return true; + } + } + false + } + + // ------------------------------------------------------------------- + // Full rebuilds + // ------------------------------------------------------------------- + + /// Full rebuild: split `text` on `\n` and lay out each block (uniform style). fn rebuild_blocks(&mut self, text: &str) { self.blocks.clear(); self.cached_line_metrics = None; @@ -364,106 +485,29 @@ impl SkiaLayoutEngine { let mut y_offset: f32 = 0.0; let mut start = 0usize; - // Split on hard line breaks. Each block's byte_end includes the `\n`, - // but we pass only the content (without the trailing `\n`) to Skia - // so it doesn't generate phantom empty lines for newlines. We handle - // inter-block spacing ourselves. loop { let has_newline; let end = if let Some(pos) = text[start..].find('\n') { has_newline = true; - start + pos + 1 // byte_end includes the `\n` + start + pos + 1 } else { has_newline = false; text.len() }; - // The content slice fed to Skia excludes the trailing `\n`. let content_end = if has_newline { end - 1 } else { end }; - let content_slice = &text[start..content_end]; - let para = self.build_paragraph_for_slice(content_slice); - - // Line metrics use block-local baselines. Byte offsets are global. - let mut stored_lines = self.convert_block_line_metrics(¶, content_slice, start); - - // Empty content (e.g. line between two `\n`s): Skia may return 0 - // lines for "". Synthesize one so the block has vertical extent. - if stored_lines.is_empty() { - let skia_metrics = para.get_line_metrics(); - let (ascent, descent, baseline) = if let Some(lm) = skia_metrics.first() { - (lm.ascent as f32, lm.descent as f32, lm.baseline as f32) - } else { - (self.font_size, self.font_size * 0.2, self.font_size) - }; - stored_lines.push(LineMetrics { - start_index: start, - end_index: start, - baseline, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }); - } - - // For the flattened line_metrics view, the line that owns the `\n` - // must have its end_index include the `\n` byte. - if has_newline { - if let Some(last) = stored_lines.last_mut() { - last.end_index = end; - } - } - - let height: f32 = if let Some(last) = stored_lines.last() { - last.baseline + last.descent - } else { - self.font_size * 1.2 - }; + let para = self.build_paragraph_for_slice(&text[start..content_end]); + let block = self.finalize_block(para, text, start, end, has_newline, y_offset); + y_offset += block.height; + self.blocks.push(block); - self.blocks.push(ParaBlock { - byte_start: start, - byte_end: end, - paragraph: para, - y_offset, - height, - line_metrics: stored_lines, - }); - - y_offset += height; start = end; - if start >= text.len() { break; } } - // Handle trailing `\n`: add a phantom empty block so the cursor can - // sit on the blank line after the last newline. - if text.ends_with('\n') && !text.is_empty() { - if let Some(last_block) = self.blocks.last() { - let last_lm = last_block.line_metrics.last(); - let (ascent, descent) = last_lm - .map(|lm| (lm.ascent, lm.descent)) - .unwrap_or((self.font_size, self.font_size * 0.2)); - let phantom = LineMetrics { - start_index: text.len(), - end_index: text.len(), - baseline: ascent, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }; - let phantom_height = ascent + descent; - self.blocks.push(ParaBlock { - byte_start: text.len(), - byte_end: text.len(), - paragraph: self.build_paragraph_for_slice(""), - y_offset, - height: phantom_height, - line_metrics: vec![phantom], - }); - } - } - + self.append_phantom_trailing_block(text, y_offset); self.cached_text = text.to_owned(); } @@ -555,14 +599,7 @@ impl SkiaLayoutEngine { } // Remove old phantom trailing block if present. - if let Some(last) = self.blocks.last() { - if last.byte_start == last.byte_end - && last.byte_start == self.cached_text.len() - && self.cached_text.ends_with('\n') - { - self.blocks.pop(); - } - } + self.remove_phantom_trailing_block(); // Determine the byte range in the NEW text that we need to // re-parse into blocks. @@ -579,7 +616,7 @@ impl SkiaLayoutEngine { .map(|b| b.y_offset) .unwrap_or(0.0); - // Build new blocks for the affected region. + // Build new blocks for the affected region using finalize_block. let mut new_blocks = Vec::new(); let mut y_offset = y_start; let mut start = rebuild_start; @@ -605,48 +642,10 @@ impl SkiaLayoutEngine { let content_end = if has_newline { end - 1 } else { end }; let content_slice = &text[start..content_end]; let para = self.build_paragraph_for_slice(content_slice); + let block = self.finalize_block(para, text, start, end, has_newline, y_offset); + y_offset += block.height; + new_blocks.push(block); - let mut stored_lines = self.convert_block_line_metrics(¶, content_slice, start); - - if stored_lines.is_empty() { - let skia_metrics = para.get_line_metrics(); - let (ascent, descent, baseline) = if let Some(lm) = skia_metrics.first() { - (lm.ascent as f32, lm.descent as f32, lm.baseline as f32) - } else { - (self.font_size, self.font_size * 0.2, self.font_size) - }; - stored_lines.push(LineMetrics { - start_index: start, - end_index: start, - baseline, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }); - } - - if has_newline { - if let Some(last) = stored_lines.last_mut() { - last.end_index = end; - } - } - - let height: f32 = if let Some(last) = stored_lines.last() { - last.baseline + last.descent - } else { - self.font_size * 1.2 - }; - - new_blocks.push(ParaBlock { - byte_start: start, - byte_end: end, - paragraph: para, - y_offset, - height, - line_metrics: stored_lines, - }); - - y_offset += height; cursor += chunk_len; start = end; @@ -661,12 +660,10 @@ impl SkiaLayoutEngine { let old_y_after = if tail_start < self.blocks.len() { self.blocks[tail_start].y_offset } else { - // No tail blocks. - y_offset // doesn't matter, no shifting needed + y_offset }; // Shift tail blocks: only byte offsets and y_offset change. - // Per-block lm.baseline is block-local (from Skia) and stays the same. let y_delta = y_offset - old_y_after; for block in &mut self.blocks[tail_start..] { block.byte_start = (block.byte_start as isize + delta) as usize; @@ -683,36 +680,16 @@ impl SkiaLayoutEngine { // Re-add phantom trailing block if needed. if text.ends_with('\n') && !text.is_empty() { - // Remove old phantom if present. + // Remove old phantom if present (may have been shifted). if let Some(last) = self.blocks.last() { if last.byte_start == last.byte_end && last.byte_start == text.len() { self.blocks.pop(); } } - if let Some(last_block) = self.blocks.last() { - let last_lm = last_block.line_metrics.last(); - let (ascent, descent) = last_lm - .map(|lm| (lm.ascent, lm.descent)) - .unwrap_or((self.font_size, self.font_size * 0.2)); - let phantom_y = last_block.y_offset + last_block.height; - let phantom = LineMetrics { - start_index: text.len(), - end_index: text.len(), - baseline: ascent, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }; - let phantom_height = ascent + descent; - self.blocks.push(ParaBlock { - byte_start: text.len(), - byte_end: text.len(), - paragraph: self.build_paragraph_for_slice(""), - y_offset: phantom_y, - height: phantom_height, - line_metrics: vec![phantom], - }); - } + let final_y = self.blocks.last() + .map(|b| b.y_offset + b.height) + .unwrap_or(0.0); + self.append_phantom_trailing_block(text, final_y); } else { // Remove phantom if text no longer ends with \n. if let Some(last) = self.blocks.last() { @@ -723,80 +700,32 @@ impl SkiaLayoutEngine { } } else { // No paragraph structure change — only one block is affected. - // Find it and rebuild just that block. let block_idx = self.blocks .iter() .position(|b| edit_offset < b.byte_end || (b.byte_start == b.byte_end && edit_offset == b.byte_start)) .unwrap_or(self.blocks.len().saturating_sub(1)); // Remove old phantom trailing block before modifying. - let had_phantom = if let Some(last) = self.blocks.last() { - last.byte_start == last.byte_end - && last.byte_start == self.cached_text.len() - && self.cached_text.ends_with('\n') - } else { - false - }; - if had_phantom { - self.blocks.pop(); - } + let had_phantom = self.remove_phantom_trailing_block(); + let _ = had_phantom; - // Rebuild the affected block. + // Rebuild the affected block using finalize_block. if block_idx < self.blocks.len() { - let old_block = &self.blocks[block_idx]; - let old_height = old_block.height; - let new_start = old_block.byte_start; - let new_end = (old_block.byte_end as isize + delta) as usize; - let y_off = old_block.y_offset; + let old_height = self.blocks[block_idx].height; + let new_start = self.blocks[block_idx].byte_start; + let new_end = (self.blocks[block_idx].byte_end as isize + delta) as usize; + let y_off = self.blocks[block_idx].y_offset; let has_newline = new_end > 0 && new_end <= text.len() && text.as_bytes().get(new_end - 1) == Some(&b'\n'); let content_end = if has_newline { new_end - 1 } else { new_end }; let content_slice = &text[new_start..content_end]; let para = self.build_paragraph_for_slice(content_slice); - - let mut stored_lines = self.convert_block_line_metrics(¶, content_slice, new_start); - - if stored_lines.is_empty() { - let skia_metrics = para.get_line_metrics(); - let (ascent, descent, baseline) = if let Some(lm) = skia_metrics.first() { - (lm.ascent as f32, lm.descent as f32, lm.baseline as f32) - } else { - (self.font_size, self.font_size * 0.2, self.font_size) - }; - stored_lines.push(LineMetrics { - start_index: new_start, - end_index: new_start, - baseline, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }); - } - - if has_newline { - if let Some(last) = stored_lines.last_mut() { - last.end_index = new_end; - } - } - - let new_height: f32 = if let Some(last) = stored_lines.last() { - last.baseline + last.descent - } else { - self.font_size * 1.2 - }; - - self.blocks[block_idx] = ParaBlock { - byte_start: new_start, - byte_end: new_end, - paragraph: para, - y_offset: y_off, - height: new_height, - line_metrics: stored_lines, - }; + let block = self.finalize_block(para, text, new_start, new_end, has_newline, y_off); + let new_height = block.height; + self.blocks[block_idx] = block; // Shift all subsequent blocks: byte offsets and y_offset. - // Per-block lm.baseline is block-local and stays the same. let y_delta = new_height - old_height; for block in &mut self.blocks[block_idx + 1..] { block.byte_start = (block.byte_start as isize + delta) as usize; @@ -810,32 +739,10 @@ impl SkiaLayoutEngine { } // Re-add phantom trailing block if needed. - if text.ends_with('\n') && !text.is_empty() { - if let Some(last_block) = self.blocks.last() { - let last_lm = last_block.line_metrics.last(); - let (ascent, descent) = last_lm - .map(|lm| (lm.ascent, lm.descent)) - .unwrap_or((self.font_size, self.font_size * 0.2)); - let phantom_y = last_block.y_offset + last_block.height; - let phantom = LineMetrics { - start_index: text.len(), - end_index: text.len(), - baseline: ascent, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }; - let phantom_height = ascent + descent; - self.blocks.push(ParaBlock { - byte_start: text.len(), - byte_end: text.len(), - paragraph: self.build_paragraph_for_slice(""), - y_offset: phantom_y, - height: phantom_height, - line_metrics: vec![phantom], - }); - } - } + let final_y = self.blocks.last() + .map(|b| b.y_offset + b.height) + .unwrap_or(0.0); + self.append_phantom_trailing_block(text, final_y); } self.cached_text = text.to_owned(); @@ -1024,6 +931,24 @@ impl SkiaLayoutEngine { para_style.set_apply_rounding_hack(false); para_style.set_text_align(self.config.text_align.to_skia()); + // Use a strut style based on the first run's font in this block + // (or the config defaults). This prevents font-fallback characters + // (e.g. CJK glyphs rendered by a system font) from changing the + // line height, which causes a visual "jump" during IME preedit. + { + let (strut_size, strut_family) = if let Some(run) = runs.first() { + (run.style.font_size, run.style.font_family.as_str()) + } else { + (self.config.font_size, self.config.font_families.first().map(|s| s.as_str()).unwrap_or("sans-serif")) + }; + let mut strut = skia_safe::textlayout::StrutStyle::new(); + strut.set_strut_enabled(true); + strut.set_force_strut_height(true); + strut.set_font_size(strut_size); + strut.set_font_families(&[strut_family]); + para_style.set_strut_style(strut); + } + let mut builder = ParagraphBuilder::new(¶_style, &self.font_collection); let fallback_families: Vec<&str> = self.config.font_families.iter().map(|s| s.as_str()).collect(); @@ -1080,87 +1005,19 @@ impl SkiaLayoutEngine { }; let content_end = if has_newline { end - 1 } else { end }; - - // Get runs overlapping this block. let runs = at.runs_in_range(start as u32, content_end as u32); let para = self.build_paragraph_for_attributed_slice(text, start, content_end, runs); + let block = self.finalize_block(para, text, start, end, has_newline, y_offset); + y_offset += block.height; + self.blocks.push(block); - let content_slice = &text[start..content_end]; - let mut stored_lines = self.convert_block_line_metrics(¶, content_slice, start); - - if stored_lines.is_empty() { - let skia_metrics = para.get_line_metrics(); - let (ascent, descent, baseline) = if let Some(lm) = skia_metrics.first() { - (lm.ascent as f32, lm.descent as f32, lm.baseline as f32) - } else { - (self.font_size, self.font_size * 0.2, self.font_size) - }; - stored_lines.push(LineMetrics { - start_index: start, - end_index: start, - baseline, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }); - } - - if has_newline { - if let Some(last) = stored_lines.last_mut() { - last.end_index = end; - } - } - - let height: f32 = if let Some(last) = stored_lines.last() { - last.baseline + last.descent - } else { - self.font_size * 1.2 - }; - - self.blocks.push(ParaBlock { - byte_start: start, - byte_end: end, - paragraph: para, - y_offset, - height, - line_metrics: stored_lines, - }); - - y_offset += height; start = end; - if start >= text.len() { break; } } - // Trailing newline phantom block. - if text.ends_with('\n') && !text.is_empty() { - if let Some(last_block) = self.blocks.last() { - let last_lm = last_block.line_metrics.last(); - let (ascent, descent) = last_lm - .map(|lm| (lm.ascent, lm.descent)) - .unwrap_or((self.font_size, self.font_size * 0.2)); - let phantom = LineMetrics { - start_index: text.len(), - end_index: text.len(), - baseline: ascent, - ascent, - descent, - left: self.config.text_align.empty_line_left(self.layout_width), - }; - let phantom_height = ascent + descent; - self.blocks.push(ParaBlock { - byte_start: text.len(), - byte_end: text.len(), - paragraph: self.build_paragraph_for_slice(""), - y_offset, - height: phantom_height, - line_metrics: vec![phantom], - }); - } - } - + self.append_phantom_trailing_block(text, y_offset); self.cached_text = text.to_owned(); } @@ -1227,6 +1084,38 @@ impl SkiaLayoutEngine { // Preedit (IME composition) support // ------------------------------------------------------------------- + /// Rebuild per-block layout with the preedit text spliced in at `cursor`. + /// + /// This uses the same per-block approach as + /// [`ensure_layout_attributed`](Self::ensure_layout_attributed) so the + /// resulting layout metrics are identical — avoiding the visual "jump" + /// that occurs when switching between a single-paragraph preedit layout + /// and a per-block normal layout. + /// + /// Returns `(display_text, preedit_byte_range)` so the caller can + /// position the cursor and draw selection rects using the display text. + pub fn rebuild_blocks_with_preedit( + &mut self, + content: &crate::attributed_text::AttributedText, + cursor: usize, + preedit: &str, + ) -> (String, std::ops::Range) { + use crate::attributed_text::TextDecorationLine; + + let mut display_content = content.clone(); + let mut preedit_style = content.caret_style_at(cursor as u32).clone(); + preedit_style.text_decoration_line = TextDecorationLine::Underline; + display_content.insert_with_style(cursor, preedit, preedit_style); + + let preedit_range = cursor..(cursor + preedit.len()); + + // Full per-block rebuild using the spliced content. + self.rebuild_blocks_attributed(&display_content); + + let display_text = display_content.text().to_owned(); + (display_text, preedit_range) + } + /// Build a display string and a laid-out `Paragraph` with the preedit /// text spliced in at `cursor` and styled with an underline. /// @@ -1538,3 +1427,33 @@ impl TextLayoutEngine for SkiaLayoutEngine { self.layout_height } } + +impl crate::layout::ManagedTextLayout for SkiaLayoutEngine { + fn ensure_layout(&mut self, content: &crate::attributed_text::AttributedText) { + self.ensure_layout_attributed(content); + } + + fn invalidate(&mut self) { + // Delegate to the existing invalidate method. + self.paragraph = None; + self.blocks.clear(); + self.cached_text.clear(); + self.cached_line_metrics = None; + } + + fn layout_width(&self) -> f32 { + self.layout_width + } + + fn layout_height(&self) -> f32 { + self.layout_height + } + + fn set_layout_width(&mut self, w: f32) { + SkiaLayoutEngine::set_layout_width(self, w); + } + + fn set_layout_height(&mut self, h: f32) { + SkiaLayoutEngine::set_layout_height(self, h); + } +} diff --git a/crates/grida-text-edit/src/tests.rs b/crates/grida-text-edit/src/tests.rs index 24c08206b9..558d89940c 100644 --- a/crates/grida-text-edit/src/tests.rs +++ b/crates/grida-text-edit/src/tests.rs @@ -3,7 +3,7 @@ //! All tests use `SimpleLayoutEngine` — no Skia, no winit. The layout is //! monospace / no-wrap, so every assertion is exact. -use crate::{apply_command, floor_char_boundary, ceil_char_boundary, layout::{CaretRect, TextLayoutEngine}, line_index_for_offset_utf8, snap_grapheme_boundary, word_segment_at, EditHistory, EditKind, EditingCommand, SimpleLayoutEngine, TextEditorState}; +use crate::{apply_command, floor_char_boundary, ceil_char_boundary, layout::{CaretRect, TextLayoutEngine}, line_index_for_offset_utf8, snap_grapheme_boundary, word_segment_at, history::EditHistory, EditKind, EditingCommand, SimpleLayoutEngine, TextEditorState}; fn layout() -> SimpleLayoutEngine { SimpleLayoutEngine::default_test() diff --git a/crates/grida-text-edit/src/text_edit_session.rs b/crates/grida-text-edit/src/text_edit_session.rs index 8f4e5f3ad2..64282e4d9f 100644 --- a/crates/grida-text-edit/src/text_edit_session.rs +++ b/crates/grida-text-edit/src/text_edit_session.rs @@ -1,10 +1,18 @@ //! Rich text editing session. //! //! [`TextEditSession`] bundles the pure editing state ([`TextEditorState`]), -//! a Skia-backed layout engine ([`SkiaLayoutEngine`]), and an +//! a layout engine implementing [`ManagedTextLayout`], and an //! [`AttributedText`] content model into a single, self-contained editing //! session. //! +//! The session is generic over the layout backend `L: ManagedTextLayout`. +//! Built-in implementations: +//! - `SkiaLayoutEngine` — real shaping via Skia Paragraph (behind `skia` feature). +//! - `SimpleLayoutEngine` — monospace, no wrapping; for deterministic tests. +//! +//! External consumers (e.g. `grida-canvas`) can provide their own implementation +//! that delegates to the host's paragraph cache. +//! //! This is the primary integration point that hosts (WASM canvas, winit //! desktop, headless tests) consume. The session does **not** own any //! rendering surface or window — drawing is the host's responsibility. @@ -101,9 +109,8 @@ use crate::{ AttributedText, TextDecorationLine, TextFill, TextStyle as AttrTextStyle, RGBA, }, history::{EditKind, GenericEditHistory}, - layout::{line_index_for_offset, CaretRect}, - skia_layout::SkiaLayoutEngine, - EditDelta, EditingCommand, TextEditorState, TextLayoutEngine, + layout::{line_index_for_offset, CaretRect, ManagedTextLayout}, + EditDelta, EditingCommand, TextEditorState, }; // --------------------------------------------------------------------------- @@ -397,14 +404,18 @@ struct ScrollAnchor { /// Bundles editing state, layout engine, attributed content, history, /// cursor blink, pointer tracking, IME composition, and scroll. /// +/// Generic over the layout backend `L`. Built-in options: +/// - `SkiaLayoutEngine` (behind `skia` feature) — real shaping. +/// - `SimpleLayoutEngine` — monospace, for tests. +/// /// Hosts create a session, feed events into it, and use the exposed /// layout engine + geometry queries to render. -pub struct TextEditSession { +pub struct TextEditSession { /// Pure editing state: text, cursor, anchor. pub state: TextEditorState, - /// Skia-backed layout engine. - pub layout: SkiaLayoutEngine, + /// Layout engine (generic — could be Skia, simple, or host-provided). + pub layout: L, /// Attributed text model (runs of styled text). pub content: AttributedText, @@ -436,16 +447,13 @@ pub struct TextEditSession { cached_caret_rect: Option, } -impl TextEditSession { - /// Create a new empty editing session with the given default style. - pub fn new( - layout_width: f32, - layout_height: f32, - default_style: AttrTextStyle, - ) -> Self { +impl TextEditSession { + /// Create a new empty editing session with the given layout engine and + /// default style. + pub fn new(layout: L, default_style: AttrTextStyle) -> Self { Self { state: TextEditorState::with_cursor(String::new(), 0), - layout: SkiaLayoutEngine::new(layout_width, layout_height), + layout, content: AttributedText::empty(default_style), caret_style_override: None, cursor_visible: true, @@ -460,16 +468,12 @@ impl TextEditSession { } /// Create a session pre-loaded with text content. - pub fn with_content( - layout_width: f32, - layout_height: f32, - content: AttributedText, - ) -> Self { + pub fn with_content(layout: L, content: AttributedText) -> Self { let text = content.text().to_owned(); let cursor = text.len(); Self { state: TextEditorState::with_cursor(text, cursor), - layout: SkiaLayoutEngine::new(layout_width, layout_height), + layout, content, caret_style_override: None, cursor_visible: true, @@ -483,6 +487,26 @@ impl TextEditSession { } } + // ----------------------------------------------------------------------- + // Invariant: state.text == content.text() + // ----------------------------------------------------------------------- + + /// Assert that the two text copies (`state.text` and `content.text()`) + /// are identical. + /// + /// This is a **debug-only** check (compiled away in release builds). + /// The dual-source-of-truth design means mutations update one copy + /// and then patch the other. If any code path forgets to synchronize, + /// this assertion will catch it immediately. + #[inline] + fn assert_text_synced(&self) { + debug_assert_eq!( + self.state.text, + self.content.text(), + "BUG: TextEditSession state.text and content.text() diverged" + ); + } + // ----------------------------------------------------------------------- // Accessors // ----------------------------------------------------------------------- @@ -497,11 +521,25 @@ impl TextEditSession { self.caret_style_override = style; } - /// Whether the cursor is currently visible (for blink rendering). + /// Whether the cursor is currently in its visible blink phase. + /// + /// **Prefer [`should_show_caret`](Self::should_show_caret)** for + /// rendering decisions — it combines blink state with selection state. pub fn cursor_visible(&self) -> bool { self.cursor_visible } + /// Whether the caret should be painted this frame. + /// + /// Combines the blink phase (`cursor_visible`) with the selection + /// state: when text is selected, the caret is hidden regardless of + /// the blink timer. This is the single authority for "should I draw + /// the caret?" — callers should not need to check `has_selection()` + /// separately. + pub fn should_show_caret(&self) -> bool { + self.state.should_show_caret() && self.cursor_visible + } + /// Whether a mouse drag is in progress. pub fn is_mouse_down(&self) -> bool { self.mouse_down @@ -540,11 +578,28 @@ impl TextEditSession { // ----------------------------------------------------------------------- /// Return the caret rectangle, using a per-frame cache. + /// + /// When an IME preedit is active, the caret is positioned at the + /// **end** of the preedit text (not the committed cursor offset). + /// This is computed by laying out the display text (committed text + /// with preedit spliced in) and querying the caret at + /// `cursor + preedit.len()`. pub fn caret_rect(&mut self) -> CaretRect { if let Some(ref cr) = self.cached_caret_rect { return cr.clone(); } - let cr = self.layout.caret_rect_at(&self.state.text, self.state.cursor); + let cr = match self.preedit.as_deref() { + Some(preedit) if !preedit.is_empty() => { + let cursor = self.state.cursor; + let text = &self.state.text; + let mut display = String::with_capacity(text.len() + preedit.len()); + display.push_str(&text[..cursor]); + display.push_str(preedit); + display.push_str(&text[cursor..]); + self.layout.caret_rect_at(&display, cursor + preedit.len()) + } + _ => self.layout.caret_rect_at(&self.state.text, self.state.cursor), + }; self.cached_caret_rect = Some(cr.clone()); cr } @@ -570,10 +625,11 @@ impl TextEditSession { fn restore(&mut self, snap: RichTextSnapshot) { self.state = snap.state; self.content = snap.content; + self.assert_text_synced(); self.caret_style_override = None; self.cached_caret_rect = None; self.layout.invalidate(); - self.layout.ensure_layout_attributed(&self.content); + self.layout.ensure_layout(&self.content); self.ensure_cursor_visible(); } @@ -606,8 +662,9 @@ impl TextEditSession { } else if self.state.cursor != old_cursor { self.caret_style_override = None; } + self.assert_text_synced(); self.reset_blink(); - self.layout.ensure_layout_attributed(&self.content); + self.layout.ensure_layout(&self.content); self.ensure_cursor_visible(); } @@ -632,8 +689,9 @@ impl TextEditSession { } else if self.state.cursor != old_cursor { self.caret_style_override = None; } + self.assert_text_synced(); self.reset_blink(); - self.layout.ensure_layout_attributed(&self.content); + self.layout.ensure_layout(&self.content); self.ensure_cursor_visible(); } @@ -909,8 +967,12 @@ impl TextEditSession { self.state.cursor = pos + pasted.text().len(); self.state.anchor = None; self.caret_style_override = None; + self.assert_text_synced(); + self.invalidate_caret_cache(); self.layout.invalidate(); self.reset_blink(); + self.layout.ensure_layout(&self.content); + self.ensure_cursor_visible(); } // ----------------------------------------------------------------------- @@ -1182,7 +1244,7 @@ impl TextEditSession { // 2. Rebuild layout at the new width. self.layout.set_layout_width(w.max(1.0)); - self.layout.ensure_layout_attributed(&self.content); + self.layout.ensure_layout(&self.content); self.cached_caret_rect = None; // 3. Restore scroll position from anchor, then clamp. @@ -1211,7 +1273,7 @@ impl TextEditSession { /// Maximum scroll offset (content may be shorter than viewport). pub fn max_scroll_y(&mut self) -> f32 { - (self.content_height() - self.layout.layout_height).max(0.0) + (self.content_height() - self.layout.layout_height()).max(0.0) } /// Clamp scroll_y to valid range. @@ -1278,11 +1340,11 @@ impl TextEditSession { /// Adjust scroll so the caret is within the visible viewport. /// - /// **Ordering constraint:** `ensure_layout_attributed` must be called + /// **Ordering constraint:** `ensure_layout` must be called /// before this method when editing rich text. pub fn ensure_cursor_visible(&mut self) { let cr = self.caret_rect(); - let viewport_height = self.layout.layout_height; + let viewport_height = self.layout.layout_height(); let margin = cr.height; if cr.y < self.scroll_y + margin { @@ -1308,7 +1370,25 @@ impl TextEditSession { } /// Advance the blink timer. Returns `true` if visibility changed. + /// + /// When a selection is active the caret is unconditionally hidden, so + /// the timer is not toggled — this avoids unnecessary redraws and + /// ensures the caret appears immediately when the selection is + /// collapsed. pub fn tick_blink(&mut self) -> bool { + // No blinking while text is selected — the caret is not shown. + if self.state.has_selection() { + // Keep the phase "visible" so that collapsing the selection + // will show the caret immediately without waiting for a full + // blink interval. + if !self.cursor_visible { + self.cursor_visible = true; + self.last_blink = Instant::now(); + return true; + } + return false; + } + if self.last_blink.elapsed() >= BLINK_INTERVAL { self.cursor_visible = !self.cursor_visible; self.last_blink = Instant::now(); @@ -1330,12 +1410,14 @@ impl TextEditSession { /// Update the IME preedit string. pub fn update_preedit(&mut self, text: String) { self.preedit = Some(text); + self.cached_caret_rect = None; self.reset_blink(); } /// Cancel/clear the IME preedit. pub fn cancel_preedit(&mut self) { self.preedit = None; + self.cached_caret_rect = None; self.reset_blink(); } @@ -1362,6 +1444,7 @@ impl TextEditSession { self.state.cursor = 0; self.state.anchor = None; self.caret_style_override = None; + self.assert_text_synced(); self.cached_caret_rect = None; self.layout.invalidate(); self.scroll_y = 0.0; @@ -1375,6 +1458,7 @@ impl TextEditSession { self.state.cursor = 0; self.state.anchor = None; self.caret_style_override = None; + self.assert_text_synced(); self.cached_caret_rect = None; self.layout.invalidate(); self.scroll_y = 0.0; diff --git a/docs/wg/feat-text-editing/richtext-persistency.md b/docs/wg/feat-text-editing/richtext-persistency.md new file mode 100644 index 0000000000..60e6c9d7cf --- /dev/null +++ b/docs/wg/feat-text-editing/richtext-persistency.md @@ -0,0 +1,783 @@ +--- +id: richtext-persistency +title: "Rich Text Persistency: Research & Comparative Reference" +--- + +## Purpose + +This document is a **research artifact and comparative reference** for rich text persistency models. It surveys how major systems represent styled text on disk and on the wire, catalogs the full property universe, analyzes structural trade-offs, and records Grida's current state as context. + +This document is **not** a finalized proposal or committed architecture decision. It is intended to ground future schema design work with factual analysis and prior art. Any candidate directions sketched here are illustrative, not normative. + +### Design constraints under consideration + +The following constraints have been identified by stakeholders as desirable for a future Grida rich text persistency model. They are recorded here as research inputs, not as finalized requirements: + +1. **CRDT-compatible**: the model should not structurally prevent layering CRDT/OT collaboration. Per-character or per-run multiplayer should be feasible without rewriting the core model. +2. **Non-nested, tabular**: preference for flat structures (e.g., FlatBuffers tables with run arrays) over recursive trees. +3. **Per-range styling**: fills, fonts, decorations, OpenType features, and typographic properties should be stylable per character range. +4. **Storage-compact**: avoid per-character overhead for uniformly-styled text. Delta-encoding against a default style is preferred. +5. **Evolution-friendly**: FlatBuffers schema evolution (new optional fields with stable IDs). +6. **Pragmatic**: avoid over-engineering. Provide extension points without front-loading complexity. + +--- + +## 1. Comparative survey: how systems model rich text + +### 1.1 SVG `` / `` (W3C SVG 2) + +**Source**: [W3C SVG 2 Text chapter](https://www.w3.org/TR/SVG2/text.html) + +SVG models text as a tree: `` is the block element, `` children carry per-range style overrides. Properties cascade from parent to child via CSS inheritance. + +```xml + + Hello world! + +``` + +**Key properties** (on `` and ``): + +| Category | Properties | +| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Font | `font-family`, `font-size`, `font-weight`, `font-style`, `font-stretch`, `font-variant`, `font-feature-settings`, `font-variation-settings`, `font-kerning`, `font-optical-sizing` | +| Spacing | `letter-spacing`, `word-spacing`, `line-height` | +| Alignment | `text-anchor` (start/middle/end), `dominant-baseline`, `alignment-baseline`, `baseline-shift` | +| Decoration | `text-decoration` (line, style, color, thickness, skip-ink) | +| Transform | `text-transform` | +| Direction | `writing-mode`, `direction`, `unicode-bidi` | +| Fill/Stroke | `fill`, `stroke`, `fill-opacity`, `stroke-opacity`, etc. | +| Positioning | `x`, `y`, `dx`, `dy`, `rotate` (per-glyph repositioning) | + +**Observations**: + +- (+) CSS property inheritance is well understood and widely implemented. +- (+) Per-glyph positioning (`dx`, `dy`, `rotate`) is a capability not found in most other models. +- (-) **Tree-nested**: `` nests inside ``. Overlapping styles require resolving the tree. +- (-) Not designed for in-place editing (DOM tree is typically treated as immutable for rendering). +- (-) No native concept of "default style + overrides." +- **Summary**: Useful as a property reference. The nested tree structure is generally considered unsuitable for design-tool editing contexts. + +### 1.2 CSS text properties (W3C CSS Text / CSS Fonts) + +CSS defines the property vocabulary that SVG, Flutter, Skia, and browsers converge toward: + +| CSS Module | Key properties | +| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **CSS Fonts 4** | `font-family`, `font-weight` (1-1000), `font-style` (normal/italic/oblique), `font-stretch` (50%-200%), `font-size`, `font-optical-sizing`, `font-kerning`, `font-feature-settings`, `font-variation-settings`, `font-synthesis` | +| **CSS Text 3** | `letter-spacing`, `word-spacing`, `line-height`, `text-transform` (none/uppercase/lowercase/capitalize), `text-align`, `text-indent`, `word-break`, `overflow-wrap` | +| **CSS Text Decoration 4** | `text-decoration-line` (none/underline/overline/line-through), `text-decoration-style` (solid/double/dotted/dashed/wavy), `text-decoration-color`, `text-decoration-thickness`, `text-underline-offset`, `text-decoration-skip-ink` | +| **CSS Writing Modes 4** | `writing-mode`, `direction`, `unicode-bidi` | +| **CSS Inline 3** | `vertical-align`, `dominant-baseline`, `alignment-baseline`, `baseline-shift` | + +**Observation**: CSS property names and semantics function as a de facto lingua franca across the systems surveyed here. Figma, Flutter, Skia, and browsers all map to or from CSS properties. A rich text model that adopts CSS-compatible naming and semantics is likely to have the least impedance mismatch with downstream consumers. + +### 1.3 Flutter `TextSpan` / `TextStyle` (tree-based, immutable) + +**Source**: [Flutter TextSpan](https://api.flutter.dev/flutter/painting/TextSpan-class.html), [Flutter TextStyle](https://api.flutter.dev/flutter/painting/TextStyle-class.html) + +Flutter uses an **immutable recursive tree** of `TextSpan` nodes. Each has optional `text`, `style`, and `children`. Styles cascade: child `TextStyle` fields override parent fields when non-null. + +At layout time, `TextSpan.build(ParagraphBuilder)` does a DFS, calling `pushStyle` / `addText` / `pop` -- flattening the tree into a run-based sequence that Skia `ParagraphBuilder` consumes. + +**Flutter `TextStyle` fields** (per-run capable): + +``` +fontFamily, fontFamilyFallback, fontSize, fontWeight, fontStyle, +letterSpacing, wordSpacing, height (line-height multiplier), +foreground (Paint), background (Paint), shadows, fontFeatures, +fontVariations, decoration, decorationColor, decorationStyle, +decorationThickness, locale, leadingDistribution +``` + +**Observations**: + +- (+) Ergonomic API for building styled text programmatically. +- (+) Natural CSS-like cascading via non-null field override. +- (-) **Not designed for in-place editing**: `TextEditingValue` is plain text + selection. The styled tree is rebuilt on each frame. +- (-) Tree rebuild per mutation makes it impractical for editing-oriented use cases. +- **Summary**: Useful as a property reference. Demonstrates that tree structures are resolved to flat runs before reaching the layout engine. + +### 1.4 Figma text model (Kiwi `.fig` schema + REST API) + +Figma uses a distinctive architecture: a **per-character style ID array** with an **override table**. + +#### 1.4.1 Kiwi (`.fig` internal format) + +**Source**: `fig.kiwi` schema ([`.ref/figma/fig.kiwi`](/.ref/figma/fig.kiwi)), [`docs/wg/feat-fig/glossary/fig.kiwi.md`](/docs/wg/feat-fig/glossary/fig.kiwi.md) + +``` +TextData { + characters: string // flat plain text + characterStyleIDs: uint[] // per-character style ID + styleOverrideTable: NodeChange[] // sparse override table + lines: TextLineData[] // per-line metadata + fontMetaData: FontMetaData[] // font weight/italic data + ... +} +``` + +**Three-layer architecture**: + +| Layer | Content | Purpose | +| --------------------- | ------------------------------------------------------------------- | -------------------------------------------------------------------------------- | +| **Base style** | `NodeChange` fields (`fontSize`, `fontName`, `letterSpacing`, etc.) | Default for all characters (style ID 0) | +| **Per-char ID array** | `characterStyleIDs[i]` | Maps each character to an override ID. `0` = use base. Trailing zeros truncated. | +| **Override table** | `styleOverrideTable[id]` = `NodeChange` (sparse diff) | Only overridden fields are set. Unset fields inherit from base. | + +**Authoring vs layout cache separation**: + +- `TextData` = authoring data (characters, style IDs, override table, line metadata). Flows through multiplayer sync. +- `DerivedTextData` = layout cache (glyphs, baselines, decorations, layout size). Computed locally. + +**Collaborative text support** (CRDT-like structures present in schema): + +``` +CollaborativePlainText { + historyOpsWithIds: CollaborativeTextStrippedOpRunWithIDs[] + historyOpsWithLoc: CollaborativeTextStrippedOpRunWithLoc[] + historyStringContentBuffer: byte[] + changesToAppend: CollaborativeTextOpRun[] +} +``` + +**Per-character overridable properties** (from `NodeChange` fields): + +| Category | Fields | +| ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Font | `fontSize`, `fontName` (family/style/postscript), `fontVariations[]`, `fontVersion` | +| Font variant | `fontVariantCommonLigatures`, `fontVariantContextualLigatures`, `fontVariantDiscretionaryLigatures`, `fontVariantHistoricalLigatures`, `fontVariantOrdinal`, `fontVariantSlashedZero`, `fontVariantNumericFigure`, `fontVariantNumericSpacing`, `fontVariantNumericFraction`, `fontVariantCaps`, `fontVariantPosition` | +| OpenType | `toggledOnOTFeatures[]`, `toggledOffOTFeatures[]` (229 feature enum values) | +| Spacing | `letterSpacing` (Number), `lineHeight` (Number), `textTracking` | +| Transform | `textCase` (ORIGINAL/UPPER/LOWER/TITLE/SMALL_CAPS/SMALL_CAPS_FORCED) | +| Decoration | `textDecoration` (NONE/UNDERLINE/STRIKETHROUGH), `textDecorationStyle` (SOLID/DOTTED/WAVY), `textDecorationFillPaints[]`, `textDecorationSkipInk`, `textDecorationThickness` (Number), `textUnderlineOffset` (Number) | +| Color | (inherits from node `fillPaints[]`) | +| Links | `hyperlink` (url, guid, openInNewTab, cmsTarget) | +| Mentions | `mention` (id, userId) | +| Semantic | `semanticWeight` (NORMAL/BOLD), `semanticItalic` (NORMAL/ITALIC), `isOverrideOverTextStyle` | +| Leading | `leadingTrim` (NONE/CAP_HEIGHT), `hangingPunctuation`, `hangingList` | + +**Paragraph-level properties** (node-level, not per-character): + +| Field | Type | +| --------------------- | ---------------------------- | +| `textAlignHorizontal` | LEFT/CENTER/RIGHT/JUSTIFIED | +| `textAlignVertical` | TOP/CENTER/BOTTOM | +| `textAutoResize` | NONE/WIDTH_AND_HEIGHT/HEIGHT | +| `textTruncation` | DISABLED/ENDING | +| `maxLines` | int | +| `paragraphIndent` | float | +| `paragraphSpacing` | float | + +**Line-level properties** (from `TextLineData`): + +| Field | Type | +| ---------------------- | --------------------------------------------------- | +| `lineType` | PLAIN/ORDERED_LIST/UNORDERED_LIST/BLOCKQUOTE/HEADER | +| `indentationLevel` | int | +| `sourceDirectionality` | AUTO/LTR/RTL | +| `directionality` | LTR/RTL (resolved) | + +#### 1.4.2 Figma REST API + +**Source**: `@figma/rest-api-spec@0.36.0` + +The REST API translates Kiwi's per-character model to a simpler surface: + +```typescript +TextNode = { + characters: string + style: TypeStyle // base style (ID 0) + characterStyleOverrides: number[] // per-char override IDs + styleOverrideTable: { [id]: TypeStyle } // override diffs + lineTypes: ('NONE' | 'ORDERED' | 'UNORDERED')[] + lineIndentations: number[] +} +``` + +**`TypeStyle` fields** (flattened `BaseTypeStyle` + own): + +| Field | Type | Notes | +| --------------------------- | ------------------------------------- | ------------------------------------------------------- | +| `fontFamily` | string | | +| `fontPostScriptName` | string \| null | | +| `fontStyle` | string | Human-readable ("Bold Italic") | +| `italic` | boolean | | +| `fontWeight` | number | 1-1000 | +| `fontSize` | number | px | +| `textCase` | enum | ORIGINAL/UPPER/LOWER/TITLE/SMALL_CAPS/SMALL_CAPS_FORCED | +| `textAlignHorizontal` | enum | LEFT/RIGHT/CENTER/JUSTIFIED | +| `textAlignVertical` | enum | TOP/CENTER/BOTTOM | +| `letterSpacing` | number | px | +| `fills` | Paint[] | **Per-run fills** | +| `hyperlink` | Hyperlink | URL or NODE | +| `opentypeFlags` | `{ [tag]: 0\|1 }` | | +| `semanticWeight` | BOLD/NORMAL | | +| `semanticItalic` | ITALIC/NORMAL | | +| `paragraphSpacing` | number | px | +| `paragraphIndent` | number | px | +| `listSpacing` | number | px | +| `textDecoration` | NONE/STRIKETHROUGH/UNDERLINE | | +| `textAutoResize` | NONE/WIDTH_AND_HEIGHT/HEIGHT/TRUNCATE | | +| `textTruncation` | DISABLED/ENDING | | +| `maxLines` | number | | +| `lineHeightPx` | number | | +| `lineHeightPercentFontSize` | number | | +| `lineHeightUnit` | PIXELS/FONT*SIZE*%/INTRINSIC\_% | | +| `boundVariables` | `{ field: VariableAlias }` | Variable bindings | + +**Observations**: + +- (+) **O(1) per-character style lookup** (direct array index + table). +- (+) **Strong CRDT/multiplayer characteristics** -- per-character ID arrays splice trivially; immutable override table avoids run-boundary merge conflicts. +- (+) Two-level inheritance (base + override) is simple and sufficient for observed use cases. +- (+) Clean authoring/layout-cache separation. +- (-) Memory overhead: one `uint` per character even for uniform text (mitigated by trailing-zero truncation). +- (-) Requires a resolution step before layout (RLE the per-char array into runs for the paragraph builder). +- **Summary**: Among the systems surveyed, Figma's model appears to have the strongest multiplayer characteristics. The per-character ID approach trades storage compactness for CRDT compatibility. + +### 1.5 ProseMirror (schema-driven, tree of nodes + marks) + +**Source**: [ProseMirror Guide](https://prosemirror.net/docs/guide/), [ProseMirror Reference](https://prosemirror.net/docs/ref/) + +ProseMirror is a widely adopted web-based rich-text editing toolkit. It is included here because it represents a major **editor-native** model in the web ecosystem -- i.e., a model designed from the ground up around the editing lifecycle (selection, input, undo, commands) rather than being primarily a rendering or persistence format. + +#### Document model + +A ProseMirror document is a **tree of typed nodes**. Each node type is declared in a user-defined **schema** that specifies allowed children, attributes, and how the node maps to/from DOM. Inline content is represented as flat sequences of text and inline nodes under a parent block node; within that sequence, text ranges carry **marks** (bold, italic, link, etc.) rather than being nested elements. + +``` +doc + └─ paragraph + ├─ text "Hello " + ├─ text "world" [mark: bold] + └─ text "!" +``` + +- **Nodes** have a `type`, optional `attrs` (attribute dict), and an ordered list of child nodes. +- **Marks** are annotations on inline text ranges. A mark has a `type` (from the schema) and optional `attrs`. Marks are order-independent and set-like per character position. +- The schema constrains which node types may appear where and which marks are valid on which nodes. This gives the model a degree of structural validation absent from most other systems surveyed here. + +#### Editing model (transactions and transforms) + +Mutations are expressed as **steps** (atomic operations such as `ReplaceStep`, `AddMarkStep`, `RemoveMarkStep`). Steps compose into **transforms**, and a transform combined with a selection update forms a **transaction**. All document changes flow through this pipeline: + +1. Build a `Transaction` from the current `EditorState`. +2. Each step produces a new immutable `Document`. +3. The transaction is dispatched to produce a new `EditorState`. + +This explicit transform pipeline enables undo/redo (invert steps), collaborative editing (rebase steps), and change tracking. ProseMirror ships a collaboration module (`prosemirror-collab`) that implements an OT-style rebase against a central authority, though it is not a CRDT. + +#### Inline styling: marks vs. attributed-string runs + +ProseMirror's marks are structurally similar to attributed-string runs in some respects but differ in important ways: + +| Aspect | ProseMirror marks | Attributed-string runs (NSAttributedString-style) | +| ----------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Granularity | Per-character mark set | Per-run attribute dictionary | +| Overlap | Freely overlapping (bold + italic + link coexist as independent marks) | No overlap; each position belongs to exactly one run with a merged dict | +| Storage | Adjacent text nodes with identical mark sets are joined; a single paragraph's inline content is a flat `Fragment` of `TextNode` slices | Adjacent runs with identical attributes coalesce | +| Inheritance | No cascading; marks are explicit per text range | Varies (Apple: none; Flutter: tree cascade) | + +#### Relevance to this survey + +ProseMirror is included because: + +1. It is a dominant model for **web-based structured editing** (used by or influencing editors such as Tiptap, Atlassian Editor, the New York Times, and others). +2. Its schema-driven approach demonstrates a middle ground between pure free-form HTML editing and the compact flat-run models used in design tools. +3. Its transform/step pipeline offers an explicit representation of mutations, which is relevant to collaboration -- though it uses OT-style rebasing rather than CRDTs. + +**Observations**: + +- (+) Schema-driven: the document model is validated by a grammar, reducing invalid states. +- (+) Explicit transform pipeline enables undo, collaboration (OT-based), and change tracking. +- (+) Marks as a set per character position handle overlapping styles naturally. +- (+) Battle-tested at scale in production web editors. +- (-) **Tree-structured**: the document is a node tree, not a flat array. This is well-suited to document editing (paragraphs, lists, headings) but adds structural complexity compared to flat run arrays in a design-tool context. +- (-) Marks carry only lightweight attrs (typically a few fields). Rich per-run style records (dozens of typographic fields like in Figma or the Grida `TextStyleRec`) would require either many fine-grained mark types or mark attrs with complex objects -- neither of which ProseMirror's mark model is primarily optimized for. +- (-) Collaboration is OT-based (central authority rebasing), not CRDT-native. Real-time peer-to-peer sync requires additional infrastructure. +- (-) Serialization format is JSON-based by default; no built-in compact binary format. Not directly FlatBuffers-compatible without a custom mapping layer. +- **Summary**: ProseMirror is the most mature editor-native model in the web ecosystem. Its strengths (schema validation, transform pipeline, mark-based inline styling) are most valuable for document-oriented editors. For a design-tool text model that prioritizes compact flat-run persistence, per-character-level CRDT compatibility, and rich typographic style records, ProseMirror's tree structure and mark granularity represent a different set of trade-offs than the flat run-based or per-character-ID models also surveyed here. + +### 1.6 Apple `NSAttributedString` (run-based / RLE) + +**Source**: [Apple NSAttributedString](https://developer.apple.com/documentation/foundation/nsattributedstring) + +Apple's model: flat `CFString` + **run-length encoded attribute dictionaries**. Each run is `(length, NSDictionary*)`. Runs are non-overlapping, contiguous, covering, and automatically coalesced. + +- No inheritance. Each run stores a complete attribute dictionary. +- `NSParagraphStyle` on the first character of a paragraph determines paragraph-level attributes. +- O(log n) lookup via binary search over run boundaries. +- Insert inherits from adjacent character. Delete shortens/removes runs and coalesces. + +**Observations**: The longest-running attributed text model in wide production use (in use since the early 1990s in NeXTSTEP, ~30+ years). Direct layout mapping (run = `pushStyle` + `addText`). However, run splits at boundaries create merge conflicts in collaborative editing scenarios. + +### 1.7 Android `SpannableStringBuilder` (span-based / interval set) + +**Source**: [Android SpannableStringBuilder](https://developer.android.com/reference/android/text/SpannableStringBuilder) + +Android stores spans as independent `(start, end, flags, object)` tuples over a gap buffer. Spans **can overlap freely**. Span flags (`INCLUSIVE_EXCLUSIVE`, `EXCLUSIVE_INCLUSIVE`, etc.) control boundary insertion behavior. + +**Observations**: Provides maximum flexibility (overlapping spans, per-span insertion policy). Typically has the weakest query performance among the models surveyed (O(n) linear scan, though newer implementations use interval trees). No automatic coalescing. The overlapping span model is generally considered difficult to use in collaborative editing contexts. + +### 1.8 Peritext (CRDT for rich text collaboration) + +**Source**: [Ink & Switch - Peritext](https://www.inkandswitch.com/peritext/) (CSCW 2022) + +Peritext is a CRDT algorithm specifically designed for rich-text collaboration, published as a peer-reviewed paper at CSCW 2022. + +**Core idea**: Instead of storing formatting as characters or control codes in the text sequence, Peritext stores formatting as **mark operations** anchored to character IDs: + +``` +addMark(opId, start: {type, charId}, end: {type, charId}, markType, ...) +removeMark(opId, start: {type, charId}, end: {type, charId}, markType) +``` + +**Anchor semantics** -- the key innovation of this approach: + +- Each character has **two anchor points**: "before" and "after". +- `type: "before"` means the mark starts/ends in the gap _before_ the character. +- `type: "after"` means the mark starts/ends in the gap _after_ the character. +- This controls whether concurrent insertions at boundaries extend the mark or not. + +**Mark behavior classification**: +| Mark type | Start anchor | End anchor | Boundary behavior | +|---|---|---|---| +| Bold, italic, etc. | `before` first char | `before` char after last | Grows at end, not at start | +| Link, comment | `before` first char | `after` last char | Does not grow at either end | + +**Conflict resolution**: + +- **Independent marks** (bold + italic): both apply. No conflict. +- **Exclusive marks** (red vs blue color): last-write-wins using Lamport timestamps. +- **Overlapping same-type**: merged (union of ranges). + +**Key properties**: + +- Built on any plain text CRDT (RGA, Causal Trees, YATA). +- Operations are **commutative** -- applying in any order converges. +- Tombstones preserve anchor points for deleted characters. +- Per-character metadata: `markOpsBefore` and `markOpsAfter` sets. + +**Relevance to a design-tool context**: + +- A system that does not require real-time collaboration as a first-class constraint does not need Peritext directly. +- However, any persistent model that aims for future CRDT compatibility should be structurally convertible to Peritext-like anchor semantics. +- Concretely, a run `[start, end)` with style S can be expressed as `addMark(before(char[start]), before(char[end]), S)`, suggesting that run-based models are not inherently incompatible with Peritext if a conversion layer is provided. + +--- + +## 2. Grida current state (implementation context) + +> **Note**: This section documents Grida's current implementation as of the time of writing. It is included as context for the research, not as a normative reference. The implementation is under active development and details here may become outdated. + +### 2.1 `grida.fbs` -- `TextSpanNode` (current schema) + +The current text node in `grida.fbs` is **uniform-style** -- a single style applies to the entire text block. + +```fbs +table TextSpanNodeProperties { + stroke_geometry: StrokeGeometryTrait; + fill_paints: [PaintStackItem]; // node-level fills (one set for all text) + stroke_paints: [PaintStackItem]; + text: string; + text_style: TextStyleRec; // SINGLE style for entire text + text_align: TextAlign; + text_align_vertical: TextAlignVertical; + max_lines: uint; + ellipsis: string; +} +``` + +`TextStyleRec` fields (as currently defined): + +| Field | ID | Type | +| --------------------- | --- | ------------------------------- | +| `text_decoration` | 0 | `TextDecorationRec` | +| `font_family` | 1 | `string` (required) | +| `font_size` | 2 | `float` = 14.0 | +| `font_weight` | 3 | `FontWeight` (struct, required) | +| `font_width` | 4 | `float` = 0.0 | +| `font_style_italic` | 5 | `bool` = false | +| `font_kerning` | 6 | `bool` = true | +| `font_optical_sizing` | 7 | `FontOpticalSizing` (struct) | +| `font_features` | 8 | `[FontFeature]` | +| `font_variations` | 9 | `[FontVariation]` | +| `letter_spacing` | 10 | `TextDimension` | +| `word_spacing` | 11 | `TextDimension` | +| `line_height` | 12 | `TextDimension` | +| `text_transform` | 13 | `TextTransform` | + +**Notable gap in current schema**: `TextStyleRec` does not include `fill` (text color). Fills live on `TextSpanNodeProperties.fill_paints` at the node level. The in-memory `grida-text-edit` crate extends this with per-run `fill: TextFill` and `hyperlink: Option`, but these extensions are not reflected in the persistent schema. + +### 2.2 Attributed text model (in-memory, not persisted) + +The `docs/wg/feat-text-editing/attributed-text.md` spec defines an in-memory model: + +``` +AttributedText = (text: String, default_style: TextStyle, paragraph_style: ParagraphStyle, runs: Vec) +StyledRun = { start: u32, end: u32, style: TextStyle } +``` + +With 7 strict invariants (non-empty, coverage, contiguity, non-degenerate, maximality, boundary alignment, monotonicity). + +The `TextStyle` in this in-memory model extends `TextStyleRec` with: + +- `fill: TextFill` (solid color; with future gradient/pattern noted as possible) +- `hyperlink: Option` (url, open_in_new_tab) + +**Current state of integration**: This rich attributed text model exists in Rust memory during WASM-based editing but collapses to a uniform style on commit back to the TypeScript document model. Per-run styling is not currently persisted. + +### 2.3 TypeScript document model (current) + +```typescript +type TextSpanNode = { + type: "tspan"; + text: string; + // ... ITextNodeStyle (single uniform style) ... + // ... ITextStroke ... + max_lines?: number; +}; +``` + +No per-range styling in the current persisted TypeScript model. + +--- + +## 3. Property universe (superset across surveyed systems) + +This section catalogs the **union** of all per-run-capable properties observed across the systems surveyed in Section 1. It serves as a reference for what a comprehensive rich text model could represent. + +### 3.1 Run-level properties (per character range) + +The "Grida (current)" column reflects the state documented in Section 2 and may change independently. + +| Property | Grida (current) | Figma | CSS | SVG | Flutter | Notes | +| ----------------------------- | ---------------------------------- | -------------------------------------------- | --------------------------- | ------------------------- | --------------------- | ------------------------------------ | +| **font_family** | `TextStyleRec.font_family` | `fontName.family` | `font-family` | `font-family` | `fontFamily` | Required in all systems | +| **font_size** | `TextStyleRec.font_size` | `fontSize` | `font-size` | `font-size` | `fontSize` | px | +| **font_weight** | `TextStyleRec.font_weight` | `fontWeight` (int) | `font-weight` (1-1000) | `font-weight` | `fontWeight` | CSS-compatible numeric | +| **font_width** | `TextStyleRec.font_width` | via `fontVariations` | `font-stretch` | `font-stretch` | via `fontVariations` | CSS font-stretch % | +| **font_style_italic** | `TextStyleRec.font_style_italic` | `italic` (bool) | `font-style` | `font-style` | `fontStyle` | bool or enum | +| **font_kerning** | `TextStyleRec.font_kerning` | implicit | `font-kerning` | `kerning` | N/A | bool | +| **font_optical_sizing** | `TextStyleRec.font_optical_sizing` | implicit | `font-optical-sizing` | N/A | N/A | Auto/None/Fixed | +| **font_features** | `TextStyleRec.font_features` | `toggledOnOTFeatures`/`toggledOffOTFeatures` | `font-feature-settings` | `font-feature-settings` | `fontFeatures` | OpenType tag + value | +| **font_variations** | `TextStyleRec.font_variations` | `fontVariations` | `font-variation-settings` | `font-variation-settings` | `fontVariations` | axis + value | +| **letter_spacing** | `TextStyleRec.letter_spacing` | `letterSpacing` | `letter-spacing` | `letter-spacing` | `letterSpacing` | px or factor | +| **word_spacing** | `TextStyleRec.word_spacing` | N/A | `word-spacing` | `word-spacing` | `wordSpacing` | px or factor | +| **line_height** | `TextStyleRec.line_height` | `lineHeight` (Number) | `line-height` | `line-height` | `height` (multiplier) | Normal/Fixed/Factor | +| **text_decoration_line** | `TextDecorationRec` | `textDecoration` | `text-decoration-line` | `text-decoration` | `decoration` | none/underline/overline/line-through | +| **text_decoration_style** | `TextDecorationRec` | `textDecorationStyle` | `text-decoration-style` | N/A | `decorationStyle` | solid/double/dotted/dashed/wavy | +| **text_decoration_color** | `TextDecorationRec` | `textDecorationFillPaints` | `text-decoration-color` | `text-decoration-fill` | `decorationColor` | RGBA or Paint | +| **text_decoration_thickness** | `TextDecorationRec` | `textDecorationThickness` | `text-decoration-thickness` | N/A | `decorationThickness` | px or % | +| **text_decoration_skip_ink** | `TextDecorationRec` | `textDecorationSkipInk` | `text-decoration-skip-ink` | N/A | N/A | bool | +| **text_transform** | `TextStyleRec.text_transform` | `textCase` | `text-transform` | `text-transform` | N/A | none/upper/lower/capitalize | +| **fill** | Not in schema (node-level only) | `fillPaints` (per-char via override) | `color` | `fill` | `foreground` (Paint) | Per-run text color | +| **hyperlink** | Not in persistent schema | `hyperlink` | N/A (DOM ``) | N/A | N/A | url + open_in_new_tab | + +**Additional properties observed in Figma but not currently in Grida's schema**: + +| Property | Figma field | CSS equivalent | Notes | +| ---------------------------- | ---------------------------------- | --------------------------- | --------------------------------------- | +| `text_underline_offset` | `textUnderlineOffset` | `text-underline-offset` | Decoration refinement | +| `leading_trim` | `leadingTrim` | `text-box-trim` (CSS draft) | Leading control | +| `font_variant_*` (11 fields) | `fontVariantCommonLigatures`, etc. | `font-variant-*` | Fine-grained OpenType control | +| `semantic_weight` | `semanticWeight` | N/A | Editor hint for style override tracking | +| `semantic_italic` | `semanticItalic` | N/A | Editor hint for style override tracking | + +### 3.2 Paragraph-level properties (per text block) + +| Property | Grida (current) | Figma | CSS | +| --------------------- | -------------------------------------------- | ---------------------- | ------------------------ | +| `text_align` | `TextSpanNodeProperties.text_align` | `textAlignHorizontal` | `text-align` | +| `text_align_vertical` | `TextSpanNodeProperties.text_align_vertical` | `textAlignVertical` | N/A (layout) | +| `max_lines` | `TextSpanNodeProperties.max_lines` | `maxLines` | `-webkit-line-clamp` | +| `ellipsis` | `TextSpanNodeProperties.ellipsis` | `textTruncation` | `text-overflow` | +| `paragraph_spacing` | Not in current schema | `paragraphSpacing` | `margin-bottom` on `

` | +| `paragraph_indent` | Not in current schema | `paragraphIndent` | `text-indent` | +| `paragraph_direction` | Not in current schema | `sourceDirectionality` | `direction` | + +### 3.3 Line-level properties (observed in Figma) + +| Property | Figma | Notes | +| ------------------- | ------------------------------------------------------------- | ------------------------ | +| `line_type` | `PLAIN`/`ORDERED_LIST`/`UNORDERED_LIST`/`BLOCKQUOTE`/`HEADER` | Block-level structure | +| `indentation_level` | int | List nesting | +| `list_spacing` | `listSpacing` | Space between list items | + +These represent block-level structure. Whether and how to support them is a separate design question; they are recorded here for completeness. + +--- + +## 4. Architecture comparison matrix + +This matrix compares the structural approaches from Section 1 across key dimensions. The "Run-based candidate" column reflects the direction that appears most aligned with the constraints in the Purpose section, but is included for comparison, not as a committed choice. + +| Dimension | Apple (runs) | Android (spans) | Flutter (tree) | ProseMirror (schema tree + marks) | Figma (per-char IDs) | Peritext (CRDT marks) | Run-based candidate | +| ------------------------ | ------------------------ | -------------------- | ---------------------- | --------------------------------- | --------------------------- | ----------------------- | ---------------------------------------------------------------------- | +| **Topology** | Flat run array | Flat interval set | Immutable tree | Schema-constrained node tree | Flat char array + table | Mark ops on char IDs | Flat run array (storage), convertible to per-char IDs at sync boundary | +| **Overlap** | No | Yes | No (hierarchy) | Yes (marks are independent sets) | No | Yes (marks) | No (resolved runs) | +| **Style lookup** | O(log k) | O(n) or O(log n+k) | O(depth) | O(marks at position) | O(1) | O(marks at position) | O(log k) in-memory | +| **Insert** | Inherits adjacent | Controlled by flags | Rebuild tree | Schema-dependent (storedMarks) | Inherits base (ID 0) | Anchor semantics | Inherits upstream (caret style) | +| **Coalescing** | Automatic | None | N/A | Adjacent same-mark texts joined | N/A (no runs) | N/A | Automatic | +| **Inheritance** | None | None | Tree merge | None (marks explicit per range) | Two-level (base + override) | None (mark union) | Two-level (default + run override) | +| **Multiplayer** | Poor | Poor | Poor | Moderate (OT-based collab) | Excellent | Excellent | Dependent on sync layer design | +| **Memory (uniform)** | 1 run | 0 spans | 1 node | 1 text node | n integers | 0 marks | 0 runs (delta encoded = empty) | +| **Memory (k styles)** | k runs | k spans | tree nodes | k text node slices + marks | n integers + k entries | 2k mark ops | k runs | +| **Layout mapping** | Direct (run = push+text) | Resolve then push | DFS produces push+text | Walk fragment, push mark ranges | Resolve per-char then RLE | Resolve marks then push | Direct (run = push+text) | +| **FlatBuffers friendly** | Yes (table array) | Possible but complex | No (recursive) | No (tree + JSON default) | Yes (vector + table array) | Complex (op log) | Yes (table array) | + +--- + +## 5. CRDT compatibility analysis + +### 5.1 Trade-offs of run-based models for CRDT + +The `attributed-text.md` spec documents several arguments favoring a run-based in-memory model: + +1. Direct layout mapping (run = `pushStyle` + `addText`). +2. Minimal memory for typical design text (run count proportional to style transitions, not character count). +3. Automatic normalization via coalescing invariant. +4. Well-understood editing algebra (split, shift, coalesce). + +However, run-based models are not natively CRDT-friendly: run splits at boundaries create merge conflicts when two users concurrently modify adjacent ranges. This is a known limitation of `NSAttributedString`-style models in collaborative contexts. + +### 5.2 Strategies for CRDT compatibility + +Three strategies for bridging a run-based model to collaborative editing have been identified: + +**Strategy A: Convert at sync boundary** + +Store resolved runs on disk and in memory. At the sync layer, convert to a per-character representation for collaboration: + +``` +// Write path (local -> sync) +for each run in runs: + for each char in run.start..run.end: + sync_char_style[char] = run.style_id + +// Read path (sync -> local) +runs = RLE(sync_char_styles) +``` + +- (+) Storage-compact for non-collaborative use. +- (+) Direct layout mapping without resolution step. +- (-) Adds a conversion layer at the sync boundary. +- (-) Conversion fidelity depends on correct handling of grapheme boundaries and concurrent edits. + +**Strategy B: Figma-style per-character ID array** + +Store `characterStyleIDs: uint[]` + `styleOverrideTable: TextStyleRec[]` on disk. Resolve to runs at load time. + +- (+) Trivially CRDT-compatible (per-character granularity matches multiplayer sync). +- (-) Per-character storage cost for uniform text. +- (-) Requires resolution step before layout. + +**Strategy C: Peritext mark operations** + +Store an operation log of `addMark` / `removeMark` with anchor semantics. + +- (+) Handles all edge cases correctly (bold at boundaries, links, overlapping marks, etc.). +- (-) Requires an operation log (not just current state), adding complexity. +- (-) Likely more than needed for a system that does not require real-time multiplayer as an initial capability. + +**Analysis**: Strategy A appears to offer a favorable trade-off for a system that prioritizes storage compactness and direct layout mapping initially, with collaboration added later. Strategy B is proven at scale by Figma. Strategy C is the most theoretically rigorous but carries the highest implementation cost. These are not mutually exclusive -- a system could start with A and migrate toward B or C at the sync boundary. The choice depends on how heavily multiplayer collaboration weighs against other constraints. + +### 5.3 Structural requirements for future CRDT compatibility + +Regardless of which strategy is ultimately chosen, the following structural properties appear important for maintaining future CRDT compatibility: + +1. Run offsets should be convertible to character positions (grapheme-aware). +2. Style identity should be structural (field-by-field equality), not pointer-based, to enable deduplication. +3. The default-style + override pattern maps naturally to Figma's style ID table approach. +4. Reserving an optional `character_style_ids`-like field in the schema could provide a forward-compatible extension point for per-character sync formats. + +--- + +## 6. Candidate persistent model sketch (illustrative) + +> **Note**: This section presents an **illustrative schema sketch** derived from the research above. It is intended to explore what a FlatBuffers-based rich text model could look like given the constraints and trade-offs identified. It is not a committed design. + +### 6.1 Candidate types + +```fbs +// --- Illustrative: Rich text fill for per-run color --- +table TextFillRec { + paint: Paint (id: 0); // reuses existing Paint union +} + +// --- Illustrative: Hyperlink for per-run links --- +table HyperlinkRec { + url: string (id: 0); + open_in_new_tab: bool = false (id: 1); +} + +// --- Illustrative: Styled run --- +table RichTextRun { + /// Start byte offset (UTF-8, inclusive). + start: uint32 (id: 0); + /// End byte offset (UTF-8, exclusive). + end: uint32 (id: 1); + /// Style override. If null, inherits from default_style. + style: TextStyleRec (id: 2); + /// Per-run fill override. If null, inherits from default fill. + fill: TextFillRec (id: 3); + /// Per-run hyperlink. If null, no link. + hyperlink: HyperlinkRec (id: 4); +} + +// --- Illustrative: Paragraph style (grouped) --- +table ParagraphStyleRec { + text_align: TextAlign = Left (id: 0); + text_align_vertical: TextAlignVertical = Top (id: 1); + paragraph_direction: ubyte = 0 (id: 2); // 0=LTR, 1=RTL, 2=Auto + max_lines: uint32 (id: 3); // 0 = unlimited + ellipsis: string (id: 4); + text_indent: float = 0.0 (id: 5); + paragraph_spacing: float = 0.0 (id: 6); +} + +// --- Illustrative: Rich text node properties --- +table RichTextNodeProperties { + stroke_geometry: StrokeGeometryTrait; + /// Default fills (applied to runs without fill override). + fill_paints: [PaintStackItem]; + stroke_paints: [PaintStackItem]; + /// The backing text string. Newlines normalized to LF. + text: string; + /// Default style (base for delta encoding). + default_style: TextStyleRec; + /// Paragraph-level style. + paragraph_style: ParagraphStyleRec; + /// Ordered runs. If empty, entire text uses default_style. + /// Invariant: runs are contiguous, non-overlapping, and cover the full text. + runs: [RichTextRun]; + /// Possible future extension: per-character style IDs for CRDT sync. + // character_style_ids: [uint32]; // not yet specified +} +``` + +### 6.2 Design reasoning behind this sketch + +| Choice | Reasoning | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Runs, not per-char IDs** | More compact for typical design text (k runs vs n integers). Direct layout mapping without resolution step. Trade-off: less natively CRDT-friendly. | +| **Delta encoding** | `null` style on run = inherit default. Single-style text = empty `runs` vector. Minimizes wire size for the common case. | +| **`fill` separate from `TextStyleRec`** | `TextStyleRec` is currently reused in multiple contexts. Adding `Paint` (a union with 6 variants) to it could affect all uses. Keeping fill as a separate field on the run avoids this. This is a design trade-off, not a settled decision -- the alternative (embedding fill in `TextStyleRec`) has the advantage of keeping one unified style type. | +| **`hyperlink` separate** | Same reasoning as fill. Not all runs have links. | +| **`ParagraphStyleRec` grouped** | Paragraph-level properties are currently scattered across the node. Grouping them could enable cleaner separation and future per-paragraph styling. | +| **UTF-8 byte offsets** | Matches Rust native indexing. `u32` aligns with FlatBuffers' natural integer width and Skia's internal `int32_t` text indices. Conversion to UTF-16 happens at the layout boundary. | +| **Reserved `character_style_ids`** | Forward-compatible extension point for CRDT sync, without committing to an implementation now. | + +### 6.3 Approximate serialization size comparison + +These are rough estimates for a 500-character paragraph with 5 style changes, intended to illustrate the order-of-magnitude difference between approaches: + +| Model | Approximate size | +| ------------------------------------------- | ------------------------------------------------------------------------------- | +| **Per-char ID approach (Figma-style)** | 500 x 4 = 2,000 bytes (IDs) + 5 x ~200 = 1,000 bytes (overrides) = ~3,000 bytes | +| **Run-based with delta encoding (5 runs)** | 5 x (8 + ~200) = ~1,040 bytes (runs) + ~200 bytes (default) = ~1,240 bytes | +| **Run-based with delta encoding (uniform)** | 0 bytes (runs) + ~200 bytes (default) = ~200 bytes | + +The per-character ID approach has a fixed per-character cost that the run-based approach avoids for uniformly-styled text. The actual sizes depend on the specific fields populated in each style record. + +--- + +## 7. Implications if a run-based direction is adopted + +> **Note**: This section explores what adoption of the candidate direction from Section 6 would entail. It is speculative and contingent on design decisions not yet made. + +### 7.1 Backward compatibility considerations + +- The existing `TextSpanNode` could remain valid with no breaking change. +- A new node type or an optional `runs` extension to the existing properties could be additive. +- A `TextSpanNode` without runs would be equivalent to a rich text node with an empty `runs` vector (single-style text). + +### 7.2 Figma import path + +If a run-based model is adopted, Figma `TextData` could be converted via RLE of the per-character style ID array: + +``` +default_style = figma.base_style +runs = RLE(figma.characterStyleIDs, figma.styleOverrideTable) +// RLE: group consecutive characters with same style ID into runs +``` + +### 7.3 Export to code (CSS/Flutter) + +A run-based model maps naturally to Flutter's `TextSpan` tree: + +``` +TextSpan( + style: default_style.to_flutter(), + children: runs.map(|r| TextSpan( + text: text[r.start..r.end], + style: r.style.to_flutter() + )) +) +``` + +--- + +## 8. Open questions + +1. **New node type vs. extension of `TextSpanNode`?** + - New type: cleaner separation, no ambiguity. But two text node types in the union. + - Extension: add optional `runs` to `TextSpanNodeProperties`. When `runs` is present, `text_style` becomes the default style. When absent, backward compatible. + - No decision has been made. + +2. **Should `TextStyleRec` gain `fill` and `hyperlink` fields, or should they remain separate?** + - Separate: avoids bloating existing uses of `TextStyleRec`. + - Embedded: simpler (one type for all style properties), matches the in-memory `TextStyle` struct. + - This is a trade-off between type simplicity and separation of concerns. + +3. **Line-level properties (list type, indentation)?** + - Not needed initially. The schema should leave room for future extension (optional field IDs or a separate vector). + +4. **Font postscript name in FBS?** + - Exists in the TypeScript model (`font_postscript_name`) but not in the current FBS schema. Whether to add it to `TextStyleRec` is an open question. + +5. **`max_length` for form fields?** + - Exists in the TypeScript model (`ITextValue.max_length`) but not in the current FBS schema. This is likely a node/form-layer concern rather than a text style property. + +6. **Run-based vs. per-character ID as the primary on-disk format?** + - Section 5 analyzes the trade-offs. The choice depends on how heavily multiplayer collaboration weighs against storage compactness and layout directness. Both are viable; hybrid approaches are possible. + +--- + +## 9. References + +### Internal (Grida repository) + +> These reference the Grida codebase and may change independently of this document. + +- [Text Editing Manifesto](./index.md) +- [Attributed Text Data Model](./attributed-text.md) +- [Performance Model](./impl-performance.md) +- [Paragraph Feature Roadmap](/docs/wg/feat-paragraph/index.md) +- [FlatBuffers Schema](/format/grida.fbs) +- [Figma Kiwi Schema Reference](/.ref/figma/fig.kiwi) +- [Figma Kiwi Glossary](/docs/wg/feat-fig/glossary/fig.kiwi.md) +- [OpenType Features Reference](/docs/reference/open-type-features.md) +- [OpenType Variable Axes Reference](/docs/reference/open-type-variable-axes.md) +- [Italic Reference](/docs/reference/italic.md) + +### External + +- [W3C SVG 2 Text](https://www.w3.org/TR/SVG2/text.html) -- SVG `` and `` specification +- [CSS Text Module Level 3](https://www.w3.org/TR/css-text-3/) -- letter-spacing, word-spacing, text-transform, text-align +- [CSS Text Decoration Level 4](https://www.w3.org/TR/css-text-decor-4/) -- underline, overline, line-through, decoration style/color/thickness +- [CSS Fonts Module Level 4](https://www.w3.org/TR/css-fonts-4/) -- font-weight, font-style, font-stretch, font-feature-settings, font-variation-settings +- [CSS Writing Modes Level 4](https://www.w3.org/TR/css-writing-modes-4/) -- writing-mode, direction, unicode-bidi +- [Flutter TextSpan](https://api.flutter.dev/flutter/painting/TextSpan-class.html) -- immutable tree model +- [Flutter TextStyle](https://api.flutter.dev/flutter/painting/TextStyle-class.html) -- per-run property set +- [Skia ParagraphBuilder](https://api.skia.org/classskia_1_1textlayout_1_1ParagraphBuilder.html) -- pushStyle/addText layout API +- [Figma REST API `@figma/rest-api-spec`](https://www.npmjs.com/package/@figma/rest-api-spec) -- TypeStyle, TextNode types +- [Apple NSAttributedString](https://developer.apple.com/documentation/foundation/nsattributedstring) -- run-based model +- [Android SpannableStringBuilder](https://developer.android.com/reference/android/text/SpannableStringBuilder) -- span-based model +- [ProseMirror Guide](https://prosemirror.net/docs/guide/) -- schema-driven document model, transforms, marks +- [ProseMirror Reference Manual](https://prosemirror.net/docs/ref/) -- API reference for `prosemirror-model`, `prosemirror-transform`, `prosemirror-collab` +- [Peritext: A CRDT for Rich-Text Collaboration](https://www.inkandswitch.com/peritext/) (Ink & Switch, CSCW 2022) -- CRDT mark operations with anchor semantics +- [FlatBuffers Schema Evolution](https://flatbuffers.dev/flatbuffers_guide_writing_schema.html) +- [Unicode Text Segmentation (UAX#29)](https://www.unicode.org/reports/tr29/) +- [Unicode Bidirectional Algorithm (UAX#9)](https://www.unicode.org/reports/tr9/) diff --git a/editor/grida-canvas-react/viewport/surface.tsx b/editor/grida-canvas-react/viewport/surface.tsx index 13bf320de0..819b251767 100644 --- a/editor/grida-canvas-react/viewport/surface.tsx +++ b/editor/grida-canvas-react/viewport/surface.tsx @@ -49,7 +49,7 @@ import { VectorMeasurementGuide } from "./ui/vector-measurement"; import { SnapGuide } from "./ui/snap"; import { Knob } from "./ui/knob"; import { cursors } from "../../components/cursor/cursor-data"; -import { SurfaceTextEditor } from "./ui/text-editor"; +import { SurfaceTextEditor } from "./ui/surface-text-editor"; import { SurfaceVectorEditor } from "./ui/surface-vector-editor"; import { SurfaceGradientEditor } from "./ui/surface-gradient-editor"; import { SurfaceImageEditor } from "./ui/surface-image-editor"; @@ -264,7 +264,11 @@ export function EditorSurface() { if (event.defaultPrevented) return; // [order matters] - otherwise, it will always try to enter the content edit mode - editor.surface.surfaceTryToggleContentEditMode(); // 1 + // Skip toggle when already in content edit mode — prevents double-click + // inside the text editor from exiting it. + if (!content_edit_mode) { + editor.surface.surfaceTryToggleContentEditMode(); // 1 + } editor.surface.surfaceDoubleClick(event); // 2 }, onDragStart: ({ event }) => { diff --git a/editor/grida-canvas-react/viewport/ui/surface-text-editor.tsx b/editor/grida-canvas-react/viewport/ui/surface-text-editor.tsx new file mode 100644 index 0000000000..fbe5ffc427 --- /dev/null +++ b/editor/grida-canvas-react/viewport/ui/surface-text-editor.tsx @@ -0,0 +1,627 @@ +/** + * Surface Text Editor + * + * Renders an inline text editing overlay on the canvas surface. + * + * Two modes based on the rendering backend: + * + * **WASM/Canvas backend (primary):** + * The text editing engine (grida-text-edit) runs entirely in WASM. + * This component provides a thin input relay — a hidden `