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 ec4889340c..4c6f84c709 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -5,7 +5,7 @@ var createGridaCanvas = (() => { async function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{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 wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;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["qg"]();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)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}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&&typeof WebAssembly.instantiateStreaming=="function"){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(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["pg"];updateMemoryViews();wasmTable=wasmExports["rg"];removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}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);___cxa_increment_exception_refcount(ptr);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++}exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);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=()=>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 endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;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=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=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}}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(typeof window!="undefined"&&typeof window.prompt=="function"){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){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 arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{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 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)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},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);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];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"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;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"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("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))throw new Error("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)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("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))throw new Error("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")throw new Error("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(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"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={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];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)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,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){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[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){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>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>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 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_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{if(isNaN(offset))return 61;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 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")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}})}};var _glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glActiveTexture=_glActiveTexture;var _glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glAttachShader=_glAttachShader;var _glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQuery=_glBeginQuery;var _glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginQueryEXT=_glBeginQueryEXT;var _glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBeginTransformFeedback=_glBeginTransformFeedback;var _glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindAttribLocation=_glBindAttribLocation;var _glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBuffer=_glBindBuffer;var _glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferBase=_glBindBufferBase;var _glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindBufferRange=_glBindBufferRange;var _glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindFramebuffer=_glBindFramebuffer;var _glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindRenderbuffer=_glBindRenderbuffer;var _glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindSampler=_glBindSampler;var _glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTexture=_glBindTexture;var _glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindTransformFeedback=_glBindTransformFeedback;var _glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _emscripten_glBindVertexArray=_glBindVertexArray;var _glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArrayOES;var _glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendColor=_glBlendColor;var _glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquation=_glBlendEquation;var _glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendEquationSeparate=_glBlendEquationSeparate;var _glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFunc=_glBlendFunc;var _glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlendFuncSeparate=_glBlendFuncSeparate;var _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_glBlitFramebuffer=_glBlitFramebuffer;var _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_glBufferData=_glBufferData;var _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_glBufferSubData=_glBufferSubData;var _glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glCheckFramebufferStatus=_glCheckFramebufferStatus;var _glClear=x0=>GLctx.clear(x0);var _emscripten_glClear=_glClear;var _glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfi=_glClearBufferfi;var _glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferfv=_glClearBufferfv;var _glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferiv=_glClearBufferiv;var _glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearBufferuiv=_glClearBufferuiv;var _glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearColor=_glClearColor;var _glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearDepthf=_glClearDepthf;var _glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClearStencil=_glClearStencil;var _glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClientWaitSync=_glClientWaitSync;var _glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glClipControlEXT=_glClipControlEXT;var _glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glColorMask=_glColorMask;var _glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompileShader=_glCompileShader;var _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_glCompressedTexImage2D=_glCompressedTexImage2D;var _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_glCompressedTexImage3D=_glCompressedTexImage3D;var _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_glCompressedTexSubImage2D=_glCompressedTexSubImage2D;var _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_glCompressedTexSubImage3D=_glCompressedTexSubImage3D;var _glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyBufferSubData=_glCopyBufferSubData;var _glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexImage2D=_glCopyTexImage2D;var _glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=_glCopyTexSubImage2D;var _glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCopyTexSubImage3D=_glCopyTexSubImage3D;var _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_glCreateProgram=_glCreateProgram;var _glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCreateShader=_glCreateShader;var _glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glCullFace=_glCullFace;var _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_glDeleteBuffers=_glDeleteBuffers;var _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_glDeleteFramebuffers=_glDeleteFramebuffers;var _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_glDeleteProgram=_glDeleteProgram;var _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_glDeleteQueries=_glDeleteQueries;var _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_glDeleteQueriesEXT=_glDeleteQueriesEXT;var _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_glDeleteRenderbuffers=_glDeleteRenderbuffers;var _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_glDeleteSamplers=_glDeleteSamplers;var _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_glDeleteShader=_glDeleteShader;var _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_glDeleteSync=_glDeleteSync;var _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_glDeleteTextures=_glDeleteTextures;var _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_glDeleteTransformFeedbacks=_glDeleteTransformFeedbacks;var _glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _emscripten_glDeleteVertexArrays=_glDeleteVertexArrays;var _glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArraysOES;var _glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthFunc=_glDepthFunc;var _glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthMask=_glDepthMask;var _glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDepthRangef=_glDepthRangef;var _glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDetachShader=_glDetachShader;var _glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisable=_glDisable;var _glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDisableVertexAttribArray=_glDisableVertexAttribArray;var _glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArrays=_glDrawArrays;var _glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _emscripten_glDrawArraysInstanced=_glDrawArraysInstanced;var _glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstancedANGLE;var _glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstancedARB;var _glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=_glDrawArraysInstancedBaseInstanceWEBGL;var _glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstancedEXT;var _glDrawArraysInstancedNV=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstancedNV;var tempFixedLengthArray=[];var _glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _emscripten_glDrawBuffers=_glDrawBuffers;var _glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffersEXT;var _glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffersWEBGL;var _glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElements=_glDrawElements;var _glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _emscripten_glDrawElementsInstanced=_glDrawElementsInstanced;var _glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstancedANGLE;var _glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstancedARB;var _glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstancedEXT;var _glDrawElementsInstancedNV=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstancedNV;var _glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glDrawRangeElements=_glDrawRangeElements;var _glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnable=_glEnable;var _glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEnableVertexAttribArray=_glEnableVertexAttribArray;var _glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQuery=_glEndQuery;var _glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndQueryEXT=_glEndQueryEXT;var _glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glEndTransformFeedback=_glEndTransformFeedback;var _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_glFenceSync=_glFenceSync;var _glFinish=()=>GLctx.finish();var _emscripten_glFinish=_glFinish;var _glFlush=()=>GLctx.flush();var _emscripten_glFlush=_glFlush;var _glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferRenderbuffer=_glFramebufferRenderbuffer;var _glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTexture2D=_glFramebufferTexture2D;var _glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFramebufferTextureLayer=_glFramebufferTextureLayer;var _glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glFrontFace=_glFrontFace;var _glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenBuffers=_glGenBuffers;var _glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenFramebuffers=_glGenFramebuffers;var _glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueries=_glGenQueries;var _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_glGenQueriesEXT=_glGenQueriesEXT;var _glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenRenderbuffers=_glGenRenderbuffers;var _glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenSamplers=_glGenSamplers;var _glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTextures=_glGenTextures;var _glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenTransformFeedbacks=_glGenTransformFeedbacks;var _glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _emscripten_glGenVertexArrays=_glGenVertexArrays;var _glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArraysOES;var _glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var _emscripten_glGenerateMipmap=_glGenerateMipmap;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 _glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveAttrib=_glGetActiveAttrib;var _glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=_glGetActiveUniform;var _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_glGetActiveUniformBlockName=_glGetActiveUniformBlockName;var _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_glGetActiveUniformBlockiv=_glGetActiveUniformBlockiv;var _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_glGetActiveUniformsiv=_glGetActiveUniformsiv;var _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_glGetAttachedShaders=_glGetAttachedShaders;var _glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetAttribLocation=_glGetAttribLocation;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 _glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBooleanv=_glGetBooleanv;var _glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteri64v=_glGetBufferParameteri64v;var _glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetBufferParameteriv=_glGetBufferParameteriv;var _glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetError=_glGetError;var _glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFloatv=_glGetFloatv;var _glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFragDataLocation=_glGetFragDataLocation;var _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 _emscripten_glGetFramebufferAttachmentParameteriv=_glGetFramebufferAttachmentParameteriv;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:throw"internal emscriptenWebGLGetIndexed() error, bad type: "+type}};var _glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64i_v=_glGetInteger64i_v;var _glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetInteger64v=_glGetInteger64v;var _glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegeri_v=_glGetIntegeri_v;var _glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetIntegerv=_glGetIntegerv;var _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_glGetInternalformativ=_glGetInternalformativ;var _glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramBinary=_glGetProgramBinary;var _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_glGetProgramInfoLog=_glGetProgramInfoLog;var _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_glGetProgramiv=_glGetProgramiv;var _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_glGetQueryObjecti64vEXT=_glGetQueryObjecti64vEXT;var _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 _emscripten_glGetQueryObjectivEXT=_glGetQueryObjectivEXT;var _glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjectui64vEXT;var _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 _emscripten_glGetQueryObjectuiv=_glGetQueryObjectuiv;var _glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectuivEXT;var _glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryiv=_glGetQueryiv;var _glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetQueryivEXT=_glGetQueryivEXT;var _glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetRenderbufferParameteriv=_glGetRenderbufferParameteriv;var _glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameterfv=_glGetSamplerParameterfv;var _glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=_glGetSamplerParameteriv;var _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_glGetShaderInfoLog=_glGetShaderInfoLog;var _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_glGetShaderPrecisionFormat=_glGetShaderPrecisionFormat;var _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_glGetShaderSource=_glGetShaderSource;var _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 _emscripten_glGetShaderiv=_glGetShaderiv;var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _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_glGetString=_glGetString;var _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_glGetStringi=_glGetStringi;var _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_glGetSynciv=_glGetSynciv;var _glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameterfv=_glGetTexParameterfv;var _glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=_glGetTexParameteriv;var _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_glGetTransformFeedbackVarying=_glGetTransformFeedbackVarying;var _glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformBlockIndex=_glGetUniformBlockIndex;var _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 _emscripten_glGetUniformIndices=_glGetUniformIndices;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 _glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformfv=_glGetUniformfv;var _glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformiv=_glGetUniformiv;var _glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var _emscripten_glGetUniformuiv=_glGetUniformuiv;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 _glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _emscripten_glGetVertexAttribIiv=_glGetVertexAttribIiv;var _glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIuiv;var _glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribPointerv=_glGetVertexAttribPointerv;var _glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribfv=_glGetVertexAttribfv;var _glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glGetVertexAttribiv=_glGetVertexAttribiv;var _glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glHint=_glHint;var _glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateFramebuffer=_glInvalidateFramebuffer;var _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_glInvalidateSubFramebuffer=_glInvalidateSubFramebuffer;var _glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsBuffer=_glIsBuffer;var _glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsEnabled=_glIsEnabled;var _glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsFramebuffer=_glIsFramebuffer;var _glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsProgram=_glIsProgram;var _glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQuery=_glIsQuery;var _glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsQueryEXT=_glIsQueryEXT;var _glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsRenderbuffer=_glIsRenderbuffer;var _glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsSampler=_glIsSampler;var _glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsShader=_glIsShader;var _glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsSync=_glIsSync;var _glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTexture=_glIsTexture;var _glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsTransformFeedback=_glIsTransformFeedback;var _glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _emscripten_glIsVertexArray=_glIsVertexArray;var _glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArrayOES;var _glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLineWidth=_glLineWidth;var _glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glLinkProgram=_glLinkProgram;var _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_glMultiDrawArraysInstancedBaseInstanceWEBGL=_glMultiDrawArraysInstancedBaseInstanceWEBGL;var _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_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPauseTransformFeedback=_glPauseTransformFeedback;var _glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPixelStorei=_glPixelStorei;var _glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonModeWEBGL=_glPolygonModeWEBGL;var _glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffset=_glPolygonOffset;var _glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glPolygonOffsetClampEXT=_glPolygonOffsetClampEXT;var _glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramBinary=_glProgramBinary;var _glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=_glProgramParameteri;var _glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glQueryCounterEXT=_glQueryCounterEXT;var _glReadBuffer=x0=>GLctx.readBuffer(x0);var _emscripten_glReadBuffer=_glReadBuffer;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 _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_glReadPixels=_glReadPixels;var _glReleaseShaderCompiler=()=>{};var _emscripten_glReleaseShaderCompiler=_glReleaseShaderCompiler;var _glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorage=_glRenderbufferStorage;var _glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glRenderbufferStorageMultisample=_glRenderbufferStorageMultisample;var _glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glResumeTransformFeedback=_glResumeTransformFeedback;var _glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSampleCoverage=_glSampleCoverage;var _glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterf=_glSamplerParameterf;var _glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=_glSamplerParameterfv;var _glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=_glSamplerParameteri;var _glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=_glSamplerParameteriv;var _glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glScissor=_glScissor;var _glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderBinary=_glShaderBinary;var _glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glShaderSource=_glShaderSource;var _glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFunc=_glStencilFunc;var _glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilFuncSeparate=_glStencilFuncSeparate;var _glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMask=_glStencilMask;var _glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilMaskSeparate=_glStencilMaskSeparate;var _glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOp=_glStencilOp;var _glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glStencilOpSeparate=_glStencilOpSeparate;var _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_glTexImage2D=_glTexImage2D;var _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_glTexImage3D=_glTexImage3D;var _glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterf=_glTexParameterf;var _glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameterfv=_glTexParameterfv;var _glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteri=_glTexParameteri;var _glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexParameteriv=_glTexParameteriv;var _glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage2D=_glTexStorage2D;var _glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexStorage3D=_glTexStorage3D;var _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_glTexSubImage2D=_glTexSubImage2D;var _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_glTexSubImage3D=_glTexSubImage3D;var _glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glTransformFeedbackVaryings=_glTransformFeedbackVaryings;var _glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1f=_glUniform1f;var miniTempWebGLFloatBuffers=[];var _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_glUniform1fv=_glUniform1fv;var _glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1i=_glUniform1i;var miniTempWebGLIntBuffers=[];var _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_glUniform1iv=_glUniform1iv;var _glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1ui=_glUniform1ui;var _glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform1uiv=_glUniform1uiv;var _glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2f=_glUniform2f;var _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_glUniform2fv=_glUniform2fv;var _glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2i=_glUniform2i;var _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_glUniform2iv=_glUniform2iv;var _glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2ui=_glUniform2ui;var _glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform2uiv=_glUniform2uiv;var _glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3f=_glUniform3f;var _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_glUniform3fv=_glUniform3fv;var _glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3i=_glUniform3i;var _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_glUniform3iv=_glUniform3iv;var _glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3ui=_glUniform3ui;var _glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform3uiv=_glUniform3uiv;var _glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4f=_glUniform4f;var _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_glUniform4fv=_glUniform4fv;var _glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4i=_glUniform4i;var _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_glUniform4iv=_glUniform4iv;var _glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4ui=_glUniform4ui;var _glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniform4uiv=_glUniform4uiv;var _glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformBlockBinding=_glUniformBlockBinding;var _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_glUniformMatrix2fv=_glUniformMatrix2fv;var _glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x3fv=_glUniformMatrix2x3fv;var _glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix2x4fv=_glUniformMatrix2x4fv;var _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_glUniformMatrix3fv=_glUniformMatrix3fv;var _glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x2fv=_glUniformMatrix3x2fv;var _glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix3x4fv=_glUniformMatrix3x4fv;var _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_glUniformMatrix4fv=_glUniformMatrix4fv;var _glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x2fv=_glUniformMatrix4x2fv;var _glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4x3fv=_glUniformMatrix4x3fv;var _glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glUseProgram=_glUseProgram;var _glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glValidateProgram=_glValidateProgram;var _glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1f=_glVertexAttrib1f;var _glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib1fv=_glVertexAttrib1fv;var _glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2f=_glVertexAttrib2f;var _glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib2fv=_glVertexAttrib2fv;var _glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3f=_glVertexAttrib3f;var _glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib3fv=_glVertexAttrib3fv;var _glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4f=_glVertexAttrib4f;var _glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttrib4fv=_glVertexAttrib4fv;var _glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _emscripten_glVertexAttribDivisor=_glVertexAttribDivisor;var _glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisorANGLE;var _glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisorARB;var _glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisorEXT;var _glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisorNV;var _glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4i=_glVertexAttribI4i;var _glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4iv=_glVertexAttribI4iv;var _glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4ui=_glVertexAttribI4ui;var _glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribI4uiv=_glVertexAttribI4uiv;var _glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribIPointer=_glVertexAttribIPointer;var _glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glVertexAttribPointer=_glVertexAttribPointer;var _glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glViewport=_glViewport;var _glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glWaitSync=_glWaitSync;var wasmTableMirror=[];var wasmTable;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 getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+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=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"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 _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.staticInit();MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";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"]}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var wasmImports={v:___cxa_begin_catch,z:___cxa_end_catch,a:___cxa_find_matching_catch_2,n:___cxa_find_matching_catch_3,M:___cxa_find_matching_catch_4,ea:___cxa_rethrow,w:___cxa_throw,db:___cxa_uncaught_exceptions,d:___resumeException,ga:___syscall_fcntl64,tb:___syscall_fstat64,ob:___syscall_getcwd,vb:___syscall_ioctl,pb:___syscall_lstat64,rb:___syscall_newfstatat,fa:___syscall_openat,sb:___syscall_stat64,yb:__abort_js,fb:__emscripten_throw_longjmp,lb:__gmtime_js,jb:__mmap_js,kb:__munmap_js,zb:__tzset_js,xb:_clock_time_get,wb:_emscripten_date_now,Q:_emscripten_get_now,Gf:_emscripten_glActiveTexture,Hf:_emscripten_glAttachShader,je:_emscripten_glBeginQuery,de:_emscripten_glBeginQueryEXT,Gc:_emscripten_glBeginTransformFeedback,If:_emscripten_glBindAttribLocation,Jf:_emscripten_glBindBuffer,Cc:_emscripten_glBindBufferBase,Dc:_emscripten_glBindBufferRange,He:_emscripten_glBindFramebuffer,Ie:_emscripten_glBindRenderbuffer,pe:_emscripten_glBindSampler,Kf:_emscripten_glBindTexture,Sb:_emscripten_glBindTransformFeedback,bf:_emscripten_glBindVertexArray,ef:_emscripten_glBindVertexArrayOES,Lf:_emscripten_glBlendColor,Mf:_emscripten_glBlendEquation,Md:_emscripten_glBlendEquationSeparate,Nf:_emscripten_glBlendFunc,Ld:_emscripten_glBlendFuncSeparate,Be:_emscripten_glBlitFramebuffer,Of:_emscripten_glBufferData,Pf:_emscripten_glBufferSubData,Je:_emscripten_glCheckFramebufferStatus,Qf:_emscripten_glClear,fc:_emscripten_glClearBufferfi,gc:_emscripten_glClearBufferfv,ic:_emscripten_glClearBufferiv,hc:_emscripten_glClearBufferuiv,Rf:_emscripten_glClearColor,Kd:_emscripten_glClearDepthf,Sf:_emscripten_glClearStencil,ye:_emscripten_glClientWaitSync,ad:_emscripten_glClipControlEXT,Tf:_emscripten_glColorMask,Uf:_emscripten_glCompileShader,Vf:_emscripten_glCompressedTexImage2D,Sc:_emscripten_glCompressedTexImage3D,Wf:_emscripten_glCompressedTexSubImage2D,Rc:_emscripten_glCompressedTexSubImage3D,Ae:_emscripten_glCopyBufferSubData,Jd:_emscripten_glCopyTexImage2D,Xf:_emscripten_glCopyTexSubImage2D,Tc:_emscripten_glCopyTexSubImage3D,Yf:_emscripten_glCreateProgram,Zf:_emscripten_glCreateShader,_f:_emscripten_glCullFace,$f:_emscripten_glDeleteBuffers,Ke:_emscripten_glDeleteFramebuffers,ag:_emscripten_glDeleteProgram,ke:_emscripten_glDeleteQueries,ee:_emscripten_glDeleteQueriesEXT,Le:_emscripten_glDeleteRenderbuffers,qe:_emscripten_glDeleteSamplers,bg:_emscripten_glDeleteShader,ze:_emscripten_glDeleteSync,cg:_emscripten_glDeleteTextures,Rb:_emscripten_glDeleteTransformFeedbacks,cf:_emscripten_glDeleteVertexArrays,ff:_emscripten_glDeleteVertexArraysOES,Id:_emscripten_glDepthFunc,dg:_emscripten_glDepthMask,Hd:_emscripten_glDepthRangef,Gd:_emscripten_glDetachShader,eg:_emscripten_glDisable,fg:_emscripten_glDisableVertexAttribArray,gg:_emscripten_glDrawArrays,$e:_emscripten_glDrawArraysInstanced,Pd:_emscripten_glDrawArraysInstancedANGLE,Eb:_emscripten_glDrawArraysInstancedARB,Ye:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Yc:_emscripten_glDrawArraysInstancedEXT,Fb:_emscripten_glDrawArraysInstancedNV,We:_emscripten_glDrawBuffers,Wc:_emscripten_glDrawBuffersEXT,Qd:_emscripten_glDrawBuffersWEBGL,hg:_emscripten_glDrawElements,af:_emscripten_glDrawElementsInstanced,Od:_emscripten_glDrawElementsInstancedANGLE,Cb:_emscripten_glDrawElementsInstancedARB,Ze:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Db:_emscripten_glDrawElementsInstancedEXT,Xc:_emscripten_glDrawElementsInstancedNV,Qe:_emscripten_glDrawRangeElements,ig:_emscripten_glEnable,jg:_emscripten_glEnableVertexAttribArray,le:_emscripten_glEndQuery,fe:_emscripten_glEndQueryEXT,Fc:_emscripten_glEndTransformFeedback,ve:_emscripten_glFenceSync,kg:_emscripten_glFinish,lg:_emscripten_glFlush,Me:_emscripten_glFramebufferRenderbuffer,Ne:_emscripten_glFramebufferTexture2D,Jc:_emscripten_glFramebufferTextureLayer,mg:_emscripten_glFrontFace,ng:_emscripten_glGenBuffers,Oe:_emscripten_glGenFramebuffers,me:_emscripten_glGenQueries,ge:_emscripten_glGenQueriesEXT,Pe:_emscripten_glGenRenderbuffers,re:_emscripten_glGenSamplers,og:_emscripten_glGenTextures,Qb:_emscripten_glGenTransformFeedbacks,_e:_emscripten_glGenVertexArrays,gf:_emscripten_glGenVertexArraysOES,De:_emscripten_glGenerateMipmap,Fd:_emscripten_glGetActiveAttrib,Ed:_emscripten_glGetActiveUniform,ac:_emscripten_glGetActiveUniformBlockName,bc:_emscripten_glGetActiveUniformBlockiv,dc:_emscripten_glGetActiveUniformsiv,Dd:_emscripten_glGetAttachedShaders,Cd:_emscripten_glGetAttribLocation,Bd:_emscripten_glGetBooleanv,Xb:_emscripten_glGetBufferParameteri64v,ja:_emscripten_glGetBufferParameteriv,ka:_emscripten_glGetError,la:_emscripten_glGetFloatv,rc:_emscripten_glGetFragDataLocation,Ee:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Hc:_emscripten_glGetIntegeri_v,ma:_emscripten_glGetIntegerv,Ib:_emscripten_glGetInternalformativ,Mb:_emscripten_glGetProgramBinary,na:_emscripten_glGetProgramInfoLog,oa:_emscripten_glGetProgramiv,ae:_emscripten_glGetQueryObjecti64vEXT,Sd:_emscripten_glGetQueryObjectivEXT,be:_emscripten_glGetQueryObjectui64vEXT,ne:_emscripten_glGetQueryObjectuiv,he:_emscripten_glGetQueryObjectuivEXT,oe:_emscripten_glGetQueryiv,ie:_emscripten_glGetQueryivEXT,Fe:_emscripten_glGetRenderbufferParameteriv,Tb:_emscripten_glGetSamplerParameterfv,Ub:_emscripten_glGetSamplerParameteriv,pa:_emscripten_glGetShaderInfoLog,Zd:_emscripten_glGetShaderPrecisionFormat,Ad:_emscripten_glGetShaderSource,qa:_emscripten_glGetShaderiv,ra:_emscripten_glGetString,df:_emscripten_glGetStringi,Zb:_emscripten_glGetSynciv,zd:_emscripten_glGetTexParameterfv,yd:_emscripten_glGetTexParameteriv,Ac:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,sa:_emscripten_glGetUniformLocation,xd:_emscripten_glGetUniformfv,wd:_emscripten_glGetUniformiv,sc:_emscripten_glGetUniformuiv,zc:_emscripten_glGetVertexAttribIiv,yc:_emscripten_glGetVertexAttribIuiv,td:_emscripten_glGetVertexAttribPointerv,vd:_emscripten_glGetVertexAttribfv,ud:_emscripten_glGetVertexAttribiv,sd:_emscripten_glHint,_d:_emscripten_glInvalidateFramebuffer,$d:_emscripten_glInvalidateSubFramebuffer,rd:_emscripten_glIsBuffer,qd:_emscripten_glIsEnabled,pd:_emscripten_glIsFramebuffer,od:_emscripten_glIsProgram,Qc:_emscripten_glIsQuery,Td:_emscripten_glIsQueryEXT,nd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,md:_emscripten_glIsShader,we:_emscripten_glIsSync,ta:_emscripten_glIsTexture,Pb:_emscripten_glIsTransformFeedback,Ic:_emscripten_glIsVertexArray,Rd:_emscripten_glIsVertexArrayOES,ua:_emscripten_glLineWidth,va:_emscripten_glLinkProgram,Ue:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Ve:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Ob:_emscripten_glPauseTransformFeedback,wa:_emscripten_glPixelStorei,$c:_emscripten_glPolygonModeWEBGL,ld:_emscripten_glPolygonOffset,bd:_emscripten_glPolygonOffsetClampEXT,Lb:_emscripten_glProgramBinary,Kb:_emscripten_glProgramParameteri,ce:_emscripten_glQueryCounterEXT,Xe:_emscripten_glReadBuffer,xa:_emscripten_glReadPixels,kd:_emscripten_glReleaseShaderCompiler,Ge:_emscripten_glRenderbufferStorage,Ce:_emscripten_glRenderbufferStorageMultisample,Nb:_emscripten_glResumeTransformFeedback,jd:_emscripten_glSampleCoverage,se:_emscripten_glSamplerParameterf,Vb:_emscripten_glSamplerParameterfv,te:_emscripten_glSamplerParameteri,ue:_emscripten_glSamplerParameteriv,ya:_emscripten_glScissor,hd:_emscripten_glShaderBinary,za:_emscripten_glShaderSource,Aa:_emscripten_glStencilFunc,Ba:_emscripten_glStencilFuncSeparate,Ca:_emscripten_glStencilMask,Da:_emscripten_glStencilMaskSeparate,Ea:_emscripten_glStencilOp,Fa:_emscripten_glStencilOpSeparate,Ga:_emscripten_glTexImage2D,Vc:_emscripten_glTexImage3D,Ha:_emscripten_glTexParameterf,Ia:_emscripten_glTexParameterfv,Ja:_emscripten_glTexParameteri,Ka:_emscripten_glTexParameteriv,Re:_emscripten_glTexStorage2D,Jb:_emscripten_glTexStorage3D,La:_emscripten_glTexSubImage2D,Uc:_emscripten_glTexSubImage3D,Bc:_emscripten_glTransformFeedbackVaryings,Ma:_emscripten_glUniform1f,Na:_emscripten_glUniform1fv,Cf:_emscripten_glUniform1i,Df:_emscripten_glUniform1iv,qc:_emscripten_glUniform1ui,mc:_emscripten_glUniform1uiv,Ef:_emscripten_glUniform2f,Ff:_emscripten_glUniform2fv,Bf:_emscripten_glUniform2i,Af:_emscripten_glUniform2iv,pc:_emscripten_glUniform2ui,lc:_emscripten_glUniform2uiv,zf:_emscripten_glUniform3f,yf:_emscripten_glUniform3fv,xf:_emscripten_glUniform3i,wf:_emscripten_glUniform3iv,oc:_emscripten_glUniform3ui,kc:_emscripten_glUniform3uiv,vf:_emscripten_glUniform4f,uf:_emscripten_glUniform4fv,hf:_emscripten_glUniform4i,jf:_emscripten_glUniform4iv,nc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,kf:_emscripten_glUniformMatrix2fv,Pc:_emscripten_glUniformMatrix2x3fv,Nc:_emscripten_glUniformMatrix2x4fv,lf:_emscripten_glUniformMatrix3fv,Oc:_emscripten_glUniformMatrix3x2fv,Lc:_emscripten_glUniformMatrix3x4fv,mf:_emscripten_glUniformMatrix4fv,Mc:_emscripten_glUniformMatrix4x2fv,Kc:_emscripten_glUniformMatrix4x3fv,nf:_emscripten_glUseProgram,gd:_emscripten_glValidateProgram,of:_emscripten_glVertexAttrib1f,fd:_emscripten_glVertexAttrib1fv,ed:_emscripten_glVertexAttrib2f,pf:_emscripten_glVertexAttrib2fv,dd:_emscripten_glVertexAttrib3f,qf:_emscripten_glVertexAttrib3fv,cd:_emscripten_glVertexAttrib4f,rf:_emscripten_glVertexAttrib4fv,Se:_emscripten_glVertexAttribDivisor,Nd:_emscripten_glVertexAttribDivisorANGLE,Gb:_emscripten_glVertexAttribDivisorARB,_c:_emscripten_glVertexAttribDivisorEXT,Hb:_emscripten_glVertexAttribDivisorNV,xc:_emscripten_glVertexAttribI4i,vc:_emscripten_glVertexAttribI4iv,wc:_emscripten_glVertexAttribI4ui,uc:_emscripten_glVertexAttribI4uiv,Te:_emscripten_glVertexAttribIPointer,sf:_emscripten_glVertexAttribPointer,tf:_emscripten_glViewport,xe:_emscripten_glWaitSync,_a:_emscripten_request_animation_frame_loop,hb:_emscripten_resize_heap,Ab:_environ_get,Bb:_environ_sizes_get,Qa:_exit,R:_fd_close,ib:_fd_pread,ub:_fd_read,mb:_fd_seek,L:_fd_write,Oa:_glGetIntegerv,V:_glGetString,Pa:_glGetStringi,Wd:invoke_dd,Vd:invoke_ddd,Yd:invoke_dddd,ca:invoke_diii,Ud:invoke_fff,G:invoke_fi,Va:invoke_fif,da:invoke_fiii,Sa:invoke_fiiiif,p:invoke_i,Xa:invoke_if,Ua:invoke_iffiiiiiiii,h:invoke_ii,x:invoke_iif,_:invoke_iiffi,g:invoke_iii,Ta:invoke_iiif,f:invoke_iiii,k:invoke_iiiii,cb:invoke_iiiiid,P:invoke_iiiiii,s:invoke_iiiiiii,O:invoke_iiiiiiii,W:invoke_iiiiiiiiii,aa:invoke_iiiiiiiiiiifiii,J:invoke_iiiiiiiiiiii,bb:invoke_iiji,eb:invoke_j,Ec:invoke_ji,m:invoke_jii,K:invoke_jiiii,Xd:invoke_jiijj,o:invoke_v,b:invoke_vi,ia:invoke_vid,F:invoke_vif,D:invoke_viff,B:invoke_vifff,t:invoke_vifffff,E:invoke_viffffffffffffffffffff,Wa:invoke_viffi,c:invoke_vii,u:invoke_viif,X:invoke_viiff,ab:invoke_viiffiii,r:invoke_viifii,e:invoke_viii,y:invoke_viiif,Z:invoke_viiiffi,j:invoke_viiii,U:invoke_viiiif,ba:invoke_viiiiff,H:invoke_viiiifi,i:invoke_viiiii,Za:invoke_viiiiif,id:invoke_viiiiiffiiifffi,Zc:invoke_viiiiiffiiifii,$:invoke_viiiiifi,l:invoke_viiiiii,q:invoke_viiiiiii,Y:invoke_viiiiiiii,Ra:invoke_viiiiiiiii,A:invoke_viiiiiiiiii,$a:invoke_viiiiiiiiiii,I:invoke_viiiiiiiiiiiiiii,Ya:invoke_viiiijjiii,ha:invoke_viiij,qb:invoke_viiijj,S:invoke_viij,C:invoke_viiji,tc:invoke_viji,gb:invoke_vijii,N:invoke_vijjjj,T:_llvm_eh_typeid_for,nb:_random_get};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["qg"];var _init=Module["_init"]=wasmExports["sg"];var _tick=Module["_tick"]=wasmExports["tg"];var _resize_surface=Module["_resize_surface"]=wasmExports["ug"];var _redraw=Module["_redraw"]=wasmExports["vg"];var _load_scene_json=Module["_load_scene_json"]=wasmExports["wg"];var _apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["xg"];var _pointer_move=Module["_pointer_move"]=wasmExports["yg"];var _command=Module["_command"]=wasmExports["zg"];var _set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Ag"];var _add_image=Module["_add_image"]=wasmExports["Bg"];var _get_image_bytes=Module["_get_image_bytes"]=wasmExports["Cg"];var _get_image_size=Module["_get_image_size"]=wasmExports["Dg"];var _add_font=Module["_add_font"]=wasmExports["Eg"];var _has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["Fg"];var _list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["Gg"];var _list_available_fonts=Module["_list_available_fonts"]=wasmExports["Hg"];var _set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Ig"];var _get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["Jg"];var _get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["Kg"];var _get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["Lg"];var _get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["Mg"];var _get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["Ng"];var _export_node_as=Module["_export_node_as"]=wasmExports["Og"];var _to_vector_network=Module["_to_vector_network"]=wasmExports["Pg"];var _set_debug=Module["_set_debug"]=wasmExports["Qg"];var _toggle_debug=Module["_toggle_debug"]=wasmExports["Rg"];var _set_verbose=Module["_set_verbose"]=wasmExports["Sg"];var _devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["Tg"];var _devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["Ug"];var _runtime_renderer_set_cache_tile=Module["_runtime_renderer_set_cache_tile"]=wasmExports["Vg"];var _devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Wg"];var _devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["Xg"];var _devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Yg"];var _highlight_strokes=Module["_highlight_strokes"]=wasmExports["Zg"];var _load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["_g"];var _load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["$g"];var _main=Module["_main"]=wasmExports["ah"];var _grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["bh"];var _grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["ch"];var _grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["dh"];var _allocate=Module["_allocate"]=wasmExports["eh"];var _deallocate=Module["_deallocate"]=wasmExports["fh"];var _malloc=wasmExports["gh"];var _emscripten_builtin_memalign=wasmExports["hh"];var _setThrew=wasmExports["ih"];var __emscripten_tempret_set=wasmExports["jh"];var __emscripten_stack_restore=wasmExports["kh"];var __emscripten_stack_alloc=wasmExports["lh"];var _emscripten_stack_get_current=wasmExports["mh"];var ___cxa_decrement_exception_refcount=wasmExports["nh"];var ___cxa_increment_exception_refcount=wasmExports["oh"];var ___cxa_can_catch=wasmExports["ph"];var ___cxa_get_exception_ptr=wasmExports["qh"];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_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_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_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_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_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_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_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_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_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_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_jii(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);return 0n}}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_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_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}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_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_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_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_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_jiijj(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_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiiifffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}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_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_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_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_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_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_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_vifff(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_viiijj(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_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_iiji(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_viiffiii(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_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_fi(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_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_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_viifii(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_iiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}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_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_viiiijjiii(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_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_if(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_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_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_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_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_fif(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_iffiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return 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_viffffffffffffffffffff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(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_fiiiif(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_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_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_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_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_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_ddd(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_fff(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_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_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_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_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;args.forEach(arg=>{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()}}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}preInit();run();moduleRtn=readyPromise; +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});var ENVIRONMENT_IS_WEB=true;var ENVIRONMENT_IS_WORKER=false;var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.slice(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}{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 wasmMemory;var ABORT=false;var EXITSTATUS;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAP64,HEAPU64,HEAPF64;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["Hg"]();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)}var runDependencies=0;var dependenciesFulfilled=null;function getUniqueRunDependency(id){return id}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}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&&typeof WebAssembly.instantiateStreaming=="function"){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(){return{a:wasmImports}}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["Gg"];updateMemoryViews();wasmTable=wasmExports["Ig"];removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(mod,inst)=>{resolve(receiveInstance(mod,inst))})})}wasmBinaryFile??=findWasmBinary();try{var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}catch(e){readyPromiseReject(e);return Promise.reject(e)}}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);___cxa_increment_exception_refcount(ptr);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++}exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);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=()=>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 endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;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=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=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}}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(typeof window!="undefined"&&typeof window.prompt=="function"){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){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 arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url).then(processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{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 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)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},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);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];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"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;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"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("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))throw new Error("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)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("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))throw new Error("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")throw new Error("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(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"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={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];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)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):"";var SYSCALLS={DEFAULT_POLLMASK:5,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){HEAP32[buf>>2]=stat.dev;HEAP32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAP32[buf+12>>2]=stat.uid;HEAP32[buf+16>>2]=stat.gid;HEAP32[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){HEAP32[buf+4>>2]=stats.bsize;HEAP32[buf+40>>2]=stats.bsize;HEAP32[buf+8>>2]=stats.blocks;HEAP32[buf+12>>2]=stats.bfree;HEAP32[buf+16>>2]=stats.bavail;HEAP32[buf+20>>2]=stats.files;HEAP32[buf+24>>2]=stats.ffree;HEAP32[buf+28>>2]=stats.fsid;HEAP32[buf+44>>2]=stats.flags;HEAP32[buf+36>>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>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 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_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{if(isNaN(offset))return 61;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 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")}getEmscriptenSupportedExtensions(GLctx).forEach(ext=>{if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}})}};var _glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glActiveTexture=_glActiveTexture;var _glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glAttachShader=_glAttachShader;var _glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQuery=_glBeginQuery;var _glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginQueryEXT=_glBeginQueryEXT;var _glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBeginTransformFeedback=_glBeginTransformFeedback;var _glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindAttribLocation=_glBindAttribLocation;var _glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBuffer=_glBindBuffer;var _glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferBase=_glBindBufferBase;var _glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindBufferRange=_glBindBufferRange;var _glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindFramebuffer=_glBindFramebuffer;var _glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindRenderbuffer=_glBindRenderbuffer;var _glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindSampler=_glBindSampler;var _glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTexture=_glBindTexture;var _glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindTransformFeedback=_glBindTransformFeedback;var _glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _emscripten_glBindVertexArray=_glBindVertexArray;var _glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArrayOES;var _glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendColor=_glBlendColor;var _glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquation=_glBlendEquation;var _glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendEquationSeparate=_glBlendEquationSeparate;var _glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFunc=_glBlendFunc;var _glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlendFuncSeparate=_glBlendFuncSeparate;var _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_glBlitFramebuffer=_glBlitFramebuffer;var _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_glBufferData=_glBufferData;var _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_glBufferSubData=_glBufferSubData;var _glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glCheckFramebufferStatus=_glCheckFramebufferStatus;var _glClear=x0=>GLctx.clear(x0);var _emscripten_glClear=_glClear;var _glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfi=_glClearBufferfi;var _glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferfv=_glClearBufferfv;var _glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferiv=_glClearBufferiv;var _glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearBufferuiv=_glClearBufferuiv;var _glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearColor=_glClearColor;var _glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearDepthf=_glClearDepthf;var _glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClearStencil=_glClearStencil;var _glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClientWaitSync=_glClientWaitSync;var _glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glClipControlEXT=_glClipControlEXT;var _glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glColorMask=_glColorMask;var _glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompileShader=_glCompileShader;var _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_glCompressedTexImage2D=_glCompressedTexImage2D;var _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_glCompressedTexImage3D=_glCompressedTexImage3D;var _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_glCompressedTexSubImage2D=_glCompressedTexSubImage2D;var _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_glCompressedTexSubImage3D=_glCompressedTexSubImage3D;var _glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyBufferSubData=_glCopyBufferSubData;var _glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexImage2D=_glCopyTexImage2D;var _glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=_glCopyTexSubImage2D;var _glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCopyTexSubImage3D=_glCopyTexSubImage3D;var _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_glCreateProgram=_glCreateProgram;var _glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCreateShader=_glCreateShader;var _glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glCullFace=_glCullFace;var _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_glDeleteBuffers=_glDeleteBuffers;var _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_glDeleteFramebuffers=_glDeleteFramebuffers;var _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_glDeleteProgram=_glDeleteProgram;var _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_glDeleteQueries=_glDeleteQueries;var _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_glDeleteQueriesEXT=_glDeleteQueriesEXT;var _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_glDeleteRenderbuffers=_glDeleteRenderbuffers;var _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_glDeleteSamplers=_glDeleteSamplers;var _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_glDeleteShader=_glDeleteShader;var _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_glDeleteSync=_glDeleteSync;var _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_glDeleteTextures=_glDeleteTextures;var _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_glDeleteTransformFeedbacks=_glDeleteTransformFeedbacks;var _glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _emscripten_glDeleteVertexArrays=_glDeleteVertexArrays;var _glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArraysOES;var _glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthFunc=_glDepthFunc;var _glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthMask=_glDepthMask;var _glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDepthRangef=_glDepthRangef;var _glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDetachShader=_glDetachShader;var _glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisable=_glDisable;var _glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDisableVertexAttribArray=_glDisableVertexAttribArray;var _glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArrays=_glDrawArrays;var _glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _emscripten_glDrawArraysInstanced=_glDrawArraysInstanced;var _glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstancedANGLE;var _glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstancedARB;var _glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=_glDrawArraysInstancedBaseInstanceWEBGL;var _glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstancedEXT;var _glDrawArraysInstancedNV=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstancedNV;var tempFixedLengthArray=[];var _glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _emscripten_glDrawBuffers=_glDrawBuffers;var _glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffersEXT;var _glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffersWEBGL;var _glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElements=_glDrawElements;var _glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _emscripten_glDrawElementsInstanced=_glDrawElementsInstanced;var _glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstancedANGLE;var _glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstancedARB;var _glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstancedEXT;var _glDrawElementsInstancedNV=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstancedNV;var _glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glDrawRangeElements=_glDrawRangeElements;var _glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnable=_glEnable;var _glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEnableVertexAttribArray=_glEnableVertexAttribArray;var _glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQuery=_glEndQuery;var _glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndQueryEXT=_glEndQueryEXT;var _glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glEndTransformFeedback=_glEndTransformFeedback;var _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_glFenceSync=_glFenceSync;var _glFinish=()=>GLctx.finish();var _emscripten_glFinish=_glFinish;var _glFlush=()=>GLctx.flush();var _emscripten_glFlush=_glFlush;var _glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferRenderbuffer=_glFramebufferRenderbuffer;var _glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTexture2D=_glFramebufferTexture2D;var _glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFramebufferTextureLayer=_glFramebufferTextureLayer;var _glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glFrontFace=_glFrontFace;var _glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenBuffers=_glGenBuffers;var _glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenFramebuffers=_glGenFramebuffers;var _glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueries=_glGenQueries;var _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_glGenQueriesEXT=_glGenQueriesEXT;var _glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenRenderbuffers=_glGenRenderbuffers;var _glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenSamplers=_glGenSamplers;var _glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTextures=_glGenTextures;var _glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenTransformFeedbacks=_glGenTransformFeedbacks;var _glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _emscripten_glGenVertexArrays=_glGenVertexArrays;var _glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArraysOES;var _glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var _emscripten_glGenerateMipmap=_glGenerateMipmap;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 _glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveAttrib=_glGetActiveAttrib;var _glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=_glGetActiveUniform;var _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_glGetActiveUniformBlockName=_glGetActiveUniformBlockName;var _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_glGetActiveUniformBlockiv=_glGetActiveUniformBlockiv;var _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_glGetActiveUniformsiv=_glGetActiveUniformsiv;var _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_glGetAttachedShaders=_glGetAttachedShaders;var _glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetAttribLocation=_glGetAttribLocation;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 _glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBooleanv=_glGetBooleanv;var _glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteri64v=_glGetBufferParameteri64v;var _glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetBufferParameteriv=_glGetBufferParameteriv;var _glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetError=_glGetError;var _glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFloatv=_glGetFloatv;var _glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFragDataLocation=_glGetFragDataLocation;var _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 _emscripten_glGetFramebufferAttachmentParameteriv=_glGetFramebufferAttachmentParameteriv;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:throw"internal emscriptenWebGLGetIndexed() error, bad type: "+type}};var _glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64i_v=_glGetInteger64i_v;var _glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetInteger64v=_glGetInteger64v;var _glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegeri_v=_glGetIntegeri_v;var _glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetIntegerv=_glGetIntegerv;var _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_glGetInternalformativ=_glGetInternalformativ;var _glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramBinary=_glGetProgramBinary;var _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_glGetProgramInfoLog=_glGetProgramInfoLog;var _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_glGetProgramiv=_glGetProgramiv;var _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_glGetQueryObjecti64vEXT=_glGetQueryObjecti64vEXT;var _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 _emscripten_glGetQueryObjectivEXT=_glGetQueryObjectivEXT;var _glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjectui64vEXT;var _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 _emscripten_glGetQueryObjectuiv=_glGetQueryObjectuiv;var _glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectuivEXT;var _glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryiv=_glGetQueryiv;var _glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetQueryivEXT=_glGetQueryivEXT;var _glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetRenderbufferParameteriv=_glGetRenderbufferParameteriv;var _glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameterfv=_glGetSamplerParameterfv;var _glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=_glGetSamplerParameteriv;var _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_glGetShaderInfoLog=_glGetShaderInfoLog;var _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_glGetShaderPrecisionFormat=_glGetShaderPrecisionFormat;var _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_glGetShaderSource=_glGetShaderSource;var _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 _emscripten_glGetShaderiv=_glGetShaderiv;var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _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_glGetString=_glGetString;var _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_glGetStringi=_glGetStringi;var _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_glGetSynciv=_glGetSynciv;var _glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameterfv=_glGetTexParameterfv;var _glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=_glGetTexParameteriv;var _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_glGetTransformFeedbackVarying=_glGetTransformFeedbackVarying;var _glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformBlockIndex=_glGetUniformBlockIndex;var _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 _emscripten_glGetUniformIndices=_glGetUniformIndices;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 _glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformfv=_glGetUniformfv;var _glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformiv=_glGetUniformiv;var _glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var _emscripten_glGetUniformuiv=_glGetUniformuiv;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 _glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _emscripten_glGetVertexAttribIiv=_glGetVertexAttribIiv;var _glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIuiv;var _glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribPointerv=_glGetVertexAttribPointerv;var _glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribfv=_glGetVertexAttribfv;var _glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glGetVertexAttribiv=_glGetVertexAttribiv;var _glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glHint=_glHint;var _glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateFramebuffer=_glInvalidateFramebuffer;var _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_glInvalidateSubFramebuffer=_glInvalidateSubFramebuffer;var _glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsBuffer=_glIsBuffer;var _glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsEnabled=_glIsEnabled;var _glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsFramebuffer=_glIsFramebuffer;var _glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsProgram=_glIsProgram;var _glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQuery=_glIsQuery;var _glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsQueryEXT=_glIsQueryEXT;var _glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsRenderbuffer=_glIsRenderbuffer;var _glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsSampler=_glIsSampler;var _glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsShader=_glIsShader;var _glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsSync=_glIsSync;var _glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTexture=_glIsTexture;var _glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsTransformFeedback=_glIsTransformFeedback;var _glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _emscripten_glIsVertexArray=_glIsVertexArray;var _glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArrayOES;var _glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLineWidth=_glLineWidth;var _glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glLinkProgram=_glLinkProgram;var _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_glMultiDrawArraysInstancedBaseInstanceWEBGL=_glMultiDrawArraysInstancedBaseInstanceWEBGL;var _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_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL;var _glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPauseTransformFeedback=_glPauseTransformFeedback;var _glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPixelStorei=_glPixelStorei;var _glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonModeWEBGL=_glPolygonModeWEBGL;var _glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffset=_glPolygonOffset;var _glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glPolygonOffsetClampEXT=_glPolygonOffsetClampEXT;var _glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramBinary=_glProgramBinary;var _glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=_glProgramParameteri;var _glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glQueryCounterEXT=_glQueryCounterEXT;var _glReadBuffer=x0=>GLctx.readBuffer(x0);var _emscripten_glReadBuffer=_glReadBuffer;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 _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_glReadPixels=_glReadPixels;var _glReleaseShaderCompiler=()=>{};var _emscripten_glReleaseShaderCompiler=_glReleaseShaderCompiler;var _glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorage=_glRenderbufferStorage;var _glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glRenderbufferStorageMultisample=_glRenderbufferStorageMultisample;var _glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glResumeTransformFeedback=_glResumeTransformFeedback;var _glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSampleCoverage=_glSampleCoverage;var _glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterf=_glSamplerParameterf;var _glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=_glSamplerParameterfv;var _glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=_glSamplerParameteri;var _glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=_glSamplerParameteriv;var _glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glScissor=_glScissor;var _glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderBinary=_glShaderBinary;var _glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glShaderSource=_glShaderSource;var _glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFunc=_glStencilFunc;var _glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilFuncSeparate=_glStencilFuncSeparate;var _glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMask=_glStencilMask;var _glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilMaskSeparate=_glStencilMaskSeparate;var _glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOp=_glStencilOp;var _glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glStencilOpSeparate=_glStencilOpSeparate;var _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_glTexImage2D=_glTexImage2D;var _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_glTexImage3D=_glTexImage3D;var _glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterf=_glTexParameterf;var _glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameterfv=_glTexParameterfv;var _glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteri=_glTexParameteri;var _glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexParameteriv=_glTexParameteriv;var _glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage2D=_glTexStorage2D;var _glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexStorage3D=_glTexStorage3D;var _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_glTexSubImage2D=_glTexSubImage2D;var _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_glTexSubImage3D=_glTexSubImage3D;var _glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glTransformFeedbackVaryings=_glTransformFeedbackVaryings;var _glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1f=_glUniform1f;var miniTempWebGLFloatBuffers=[];var _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_glUniform1fv=_glUniform1fv;var _glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1i=_glUniform1i;var miniTempWebGLIntBuffers=[];var _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_glUniform1iv=_glUniform1iv;var _glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1ui=_glUniform1ui;var _glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform1uiv=_glUniform1uiv;var _glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2f=_glUniform2f;var _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_glUniform2fv=_glUniform2fv;var _glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2i=_glUniform2i;var _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_glUniform2iv=_glUniform2iv;var _glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2ui=_glUniform2ui;var _glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform2uiv=_glUniform2uiv;var _glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3f=_glUniform3f;var _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_glUniform3fv=_glUniform3fv;var _glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3i=_glUniform3i;var _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_glUniform3iv=_glUniform3iv;var _glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3ui=_glUniform3ui;var _glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform3uiv=_glUniform3uiv;var _glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4f=_glUniform4f;var _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_glUniform4fv=_glUniform4fv;var _glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4i=_glUniform4i;var _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_glUniform4iv=_glUniform4iv;var _glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4ui=_glUniform4ui;var _glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniform4uiv=_glUniform4uiv;var _glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformBlockBinding=_glUniformBlockBinding;var _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_glUniformMatrix2fv=_glUniformMatrix2fv;var _glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x3fv=_glUniformMatrix2x3fv;var _glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix2x4fv=_glUniformMatrix2x4fv;var _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_glUniformMatrix3fv=_glUniformMatrix3fv;var _glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x2fv=_glUniformMatrix3x2fv;var _glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix3x4fv=_glUniformMatrix3x4fv;var _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_glUniformMatrix4fv=_glUniformMatrix4fv;var _glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x2fv=_glUniformMatrix4x2fv;var _glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4x3fv=_glUniformMatrix4x3fv;var _glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glUseProgram=_glUseProgram;var _glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glValidateProgram=_glValidateProgram;var _glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1f=_glVertexAttrib1f;var _glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib1fv=_glVertexAttrib1fv;var _glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2f=_glVertexAttrib2f;var _glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib2fv=_glVertexAttrib2fv;var _glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3f=_glVertexAttrib3f;var _glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib3fv=_glVertexAttrib3fv;var _glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4f=_glVertexAttrib4f;var _glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttrib4fv=_glVertexAttrib4fv;var _glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _emscripten_glVertexAttribDivisor=_glVertexAttribDivisor;var _glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisorANGLE;var _glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisorARB;var _glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisorEXT;var _glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisorNV;var _glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4i=_glVertexAttribI4i;var _glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4iv=_glVertexAttribI4iv;var _glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4ui=_glVertexAttribI4ui;var _glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribI4uiv=_glVertexAttribI4uiv;var _glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribIPointer=_glVertexAttribIPointer;var _glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glVertexAttribPointer=_glVertexAttribPointer;var _glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glViewport=_glViewport;var _glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glWaitSync=_glWaitSync;var wasmTableMirror=[];var wasmTable;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 getHeapMax=()=>2147483648;var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+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=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"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 _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.staticInit();MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack="";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"]}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var wasmImports={B:___cxa_begin_catch,H:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,U:___cxa_find_matching_catch_4,sa:___cxa_rethrow,F:___cxa_throw,gb:___cxa_uncaught_exceptions,d:___resumeException,ua:___syscall_fcntl64,ub:___syscall_fstat64,qb:___syscall_getcwd,wb:___syscall_ioctl,rb:___syscall_lstat64,sb:___syscall_newfstatat,ta:___syscall_openat,tb:___syscall_stat64,zb:__abort_js,ib:__emscripten_throw_longjmp,nb:__gmtime_js,lb:__mmap_js,mb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,da:_emscripten_get_now,Gf:_emscripten_glActiveTexture,Hf:_emscripten_glAttachShader,je:_emscripten_glBeginQuery,de:_emscripten_glBeginQueryEXT,Hc:_emscripten_glBeginTransformFeedback,If:_emscripten_glBindAttribLocation,Jf:_emscripten_glBindBuffer,Ec:_emscripten_glBindBufferBase,Fc:_emscripten_glBindBufferRange,He:_emscripten_glBindFramebuffer,Ie:_emscripten_glBindRenderbuffer,pe:_emscripten_glBindSampler,Kf:_emscripten_glBindTexture,Tb:_emscripten_glBindTransformFeedback,bf:_emscripten_glBindVertexArray,ef:_emscripten_glBindVertexArrayOES,Lf:_emscripten_glBlendColor,Mf:_emscripten_glBlendEquation,Nd:_emscripten_glBlendEquationSeparate,Nf:_emscripten_glBlendFunc,Md:_emscripten_glBlendFuncSeparate,Be:_emscripten_glBlitFramebuffer,Of:_emscripten_glBufferData,Pf:_emscripten_glBufferSubData,Je:_emscripten_glCheckFramebufferStatus,Qf:_emscripten_glClear,gc:_emscripten_glClearBufferfi,hc:_emscripten_glClearBufferfv,jc:_emscripten_glClearBufferiv,ic:_emscripten_glClearBufferuiv,Rf:_emscripten_glClearColor,Ld:_emscripten_glClearDepthf,Sf:_emscripten_glClearStencil,ye:_emscripten_glClientWaitSync,bd:_emscripten_glClipControlEXT,Tf:_emscripten_glColorMask,Uf:_emscripten_glCompileShader,Vf:_emscripten_glCompressedTexImage2D,Tc:_emscripten_glCompressedTexImage3D,Wf:_emscripten_glCompressedTexSubImage2D,Sc:_emscripten_glCompressedTexSubImage3D,Ae:_emscripten_glCopyBufferSubData,Jd:_emscripten_glCopyTexImage2D,Xf:_emscripten_glCopyTexSubImage2D,Uc:_emscripten_glCopyTexSubImage3D,Yf:_emscripten_glCreateProgram,Zf:_emscripten_glCreateShader,_f:_emscripten_glCullFace,$f:_emscripten_glDeleteBuffers,Ke:_emscripten_glDeleteFramebuffers,ag:_emscripten_glDeleteProgram,ke:_emscripten_glDeleteQueries,ee:_emscripten_glDeleteQueriesEXT,Le:_emscripten_glDeleteRenderbuffers,qe:_emscripten_glDeleteSamplers,bg:_emscripten_glDeleteShader,ze:_emscripten_glDeleteSync,cg:_emscripten_glDeleteTextures,Sb:_emscripten_glDeleteTransformFeedbacks,cf:_emscripten_glDeleteVertexArrays,ff:_emscripten_glDeleteVertexArraysOES,Id:_emscripten_glDepthFunc,dg:_emscripten_glDepthMask,Hd:_emscripten_glDepthRangef,Gd:_emscripten_glDetachShader,eg:_emscripten_glDisable,fg:_emscripten_glDisableVertexAttribArray,gg:_emscripten_glDrawArrays,$e:_emscripten_glDrawArraysInstanced,Qd:_emscripten_glDrawArraysInstancedANGLE,Fb:_emscripten_glDrawArraysInstancedARB,Ye:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,_c:_emscripten_glDrawArraysInstancedEXT,Gb:_emscripten_glDrawArraysInstancedNV,We:_emscripten_glDrawBuffers,Yc:_emscripten_glDrawBuffersEXT,Rd:_emscripten_glDrawBuffersWEBGL,hg:_emscripten_glDrawElements,af:_emscripten_glDrawElementsInstanced,Pd:_emscripten_glDrawElementsInstancedANGLE,Db:_emscripten_glDrawElementsInstancedARB,Ze:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Eb:_emscripten_glDrawElementsInstancedEXT,Zc:_emscripten_glDrawElementsInstancedNV,Qe:_emscripten_glDrawRangeElements,ig:_emscripten_glEnable,jg:_emscripten_glEnableVertexAttribArray,le:_emscripten_glEndQuery,fe:_emscripten_glEndQueryEXT,Gc:_emscripten_glEndTransformFeedback,ve:_emscripten_glFenceSync,kg:_emscripten_glFinish,lg:_emscripten_glFlush,Me:_emscripten_glFramebufferRenderbuffer,Ne:_emscripten_glFramebufferTexture2D,Kc:_emscripten_glFramebufferTextureLayer,mg:_emscripten_glFrontFace,ng:_emscripten_glGenBuffers,Oe:_emscripten_glGenFramebuffers,me:_emscripten_glGenQueries,ge:_emscripten_glGenQueriesEXT,Pe:_emscripten_glGenRenderbuffers,re:_emscripten_glGenSamplers,og:_emscripten_glGenTextures,Rb:_emscripten_glGenTransformFeedbacks,_e:_emscripten_glGenVertexArrays,gf:_emscripten_glGenVertexArraysOES,De:_emscripten_glGenerateMipmap,Fd:_emscripten_glGetActiveAttrib,Ed:_emscripten_glGetActiveUniform,bc:_emscripten_glGetActiveUniformBlockName,cc:_emscripten_glGetActiveUniformBlockiv,ec:_emscripten_glGetActiveUniformsiv,Dd:_emscripten_glGetAttachedShaders,Cd:_emscripten_glGetAttribLocation,Bd:_emscripten_glGetBooleanv,Yb:_emscripten_glGetBufferParameteri64v,pg:_emscripten_glGetBufferParameteriv,qg:_emscripten_glGetError,rg:_emscripten_glGetFloatv,tc:_emscripten_glGetFragDataLocation,Ee:_emscripten_glGetFramebufferAttachmentParameteriv,Zb:_emscripten_glGetInteger64i_v,$b:_emscripten_glGetInteger64v,Ic:_emscripten_glGetIntegeri_v,sg:_emscripten_glGetIntegerv,Jb:_emscripten_glGetInternalformativ,Nb:_emscripten_glGetProgramBinary,tg:_emscripten_glGetProgramInfoLog,ug:_emscripten_glGetProgramiv,ae:_emscripten_glGetQueryObjecti64vEXT,Td:_emscripten_glGetQueryObjectivEXT,be:_emscripten_glGetQueryObjectui64vEXT,ne:_emscripten_glGetQueryObjectuiv,he:_emscripten_glGetQueryObjectuivEXT,oe:_emscripten_glGetQueryiv,ie:_emscripten_glGetQueryivEXT,Fe:_emscripten_glGetRenderbufferParameteriv,Ub:_emscripten_glGetSamplerParameterfv,Vb:_emscripten_glGetSamplerParameteriv,vg:_emscripten_glGetShaderInfoLog,Zd:_emscripten_glGetShaderPrecisionFormat,Ad:_emscripten_glGetShaderSource,wg:_emscripten_glGetShaderiv,xg:_emscripten_glGetString,df:_emscripten_glGetStringi,_b:_emscripten_glGetSynciv,zd:_emscripten_glGetTexParameterfv,yd:_emscripten_glGetTexParameteriv,Bc:_emscripten_glGetTransformFeedbackVarying,dc:_emscripten_glGetUniformBlockIndex,fc:_emscripten_glGetUniformIndices,yg:_emscripten_glGetUniformLocation,xd:_emscripten_glGetUniformfv,wd:_emscripten_glGetUniformiv,uc:_emscripten_glGetUniformuiv,Ac:_emscripten_glGetVertexAttribIiv,zc:_emscripten_glGetVertexAttribIuiv,td:_emscripten_glGetVertexAttribPointerv,vd:_emscripten_glGetVertexAttribfv,ud:_emscripten_glGetVertexAttribiv,sd:_emscripten_glHint,_d:_emscripten_glInvalidateFramebuffer,$d:_emscripten_glInvalidateSubFramebuffer,rd:_emscripten_glIsBuffer,qd:_emscripten_glIsEnabled,pd:_emscripten_glIsFramebuffer,od:_emscripten_glIsProgram,Rc:_emscripten_glIsQuery,Ud:_emscripten_glIsQueryEXT,nd:_emscripten_glIsRenderbuffer,Xb:_emscripten_glIsSampler,md:_emscripten_glIsShader,we:_emscripten_glIsSync,zg:_emscripten_glIsTexture,Qb:_emscripten_glIsTransformFeedback,Jc:_emscripten_glIsVertexArray,Sd:_emscripten_glIsVertexArrayOES,Ag:_emscripten_glLineWidth,Bg:_emscripten_glLinkProgram,Ue:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Ve:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Pb:_emscripten_glPauseTransformFeedback,Cg:_emscripten_glPixelStorei,ad:_emscripten_glPolygonModeWEBGL,ld:_emscripten_glPolygonOffset,cd:_emscripten_glPolygonOffsetClampEXT,Mb:_emscripten_glProgramBinary,Lb:_emscripten_glProgramParameteri,ce:_emscripten_glQueryCounterEXT,Xe:_emscripten_glReadBuffer,Dg:_emscripten_glReadPixels,kd:_emscripten_glReleaseShaderCompiler,Ge:_emscripten_glRenderbufferStorage,Ce:_emscripten_glRenderbufferStorageMultisample,Ob:_emscripten_glResumeTransformFeedback,jd:_emscripten_glSampleCoverage,se:_emscripten_glSamplerParameterf,Wb:_emscripten_glSamplerParameterfv,te:_emscripten_glSamplerParameteri,ue:_emscripten_glSamplerParameteriv,Eg:_emscripten_glScissor,id:_emscripten_glShaderBinary,Fg:_emscripten_glShaderSource,Aa:_emscripten_glStencilFunc,Ba:_emscripten_glStencilFuncSeparate,Ca:_emscripten_glStencilMask,Da:_emscripten_glStencilMaskSeparate,Ea:_emscripten_glStencilOp,Fa:_emscripten_glStencilOpSeparate,Ga:_emscripten_glTexImage2D,Wc:_emscripten_glTexImage3D,Ha:_emscripten_glTexParameterf,Ia:_emscripten_glTexParameterfv,Ja:_emscripten_glTexParameteri,Ka:_emscripten_glTexParameteriv,Re:_emscripten_glTexStorage2D,Kb:_emscripten_glTexStorage3D,La:_emscripten_glTexSubImage2D,Vc:_emscripten_glTexSubImage3D,Dc:_emscripten_glTransformFeedbackVaryings,Ma:_emscripten_glUniform1f,Na:_emscripten_glUniform1fv,Cf:_emscripten_glUniform1i,Df:_emscripten_glUniform1iv,sc:_emscripten_glUniform1ui,nc:_emscripten_glUniform1uiv,Ef:_emscripten_glUniform2f,Ff:_emscripten_glUniform2fv,Bf:_emscripten_glUniform2i,Af:_emscripten_glUniform2iv,qc:_emscripten_glUniform2ui,mc:_emscripten_glUniform2uiv,zf:_emscripten_glUniform3f,yf:_emscripten_glUniform3fv,xf:_emscripten_glUniform3i,wf:_emscripten_glUniform3iv,pc:_emscripten_glUniform3ui,lc:_emscripten_glUniform3uiv,vf:_emscripten_glUniform4f,uf:_emscripten_glUniform4fv,hf:_emscripten_glUniform4i,jf:_emscripten_glUniform4iv,oc:_emscripten_glUniform4ui,kc:_emscripten_glUniform4uiv,ac:_emscripten_glUniformBlockBinding,kf:_emscripten_glUniformMatrix2fv,Qc:_emscripten_glUniformMatrix2x3fv,Oc:_emscripten_glUniformMatrix2x4fv,lf:_emscripten_glUniformMatrix3fv,Pc:_emscripten_glUniformMatrix3x2fv,Mc:_emscripten_glUniformMatrix3x4fv,mf:_emscripten_glUniformMatrix4fv,Nc:_emscripten_glUniformMatrix4x2fv,Lc:_emscripten_glUniformMatrix4x3fv,nf:_emscripten_glUseProgram,hd:_emscripten_glValidateProgram,of:_emscripten_glVertexAttrib1f,gd:_emscripten_glVertexAttrib1fv,fd:_emscripten_glVertexAttrib2f,pf:_emscripten_glVertexAttrib2fv,ed:_emscripten_glVertexAttrib3f,qf:_emscripten_glVertexAttrib3fv,dd:_emscripten_glVertexAttrib4f,rf:_emscripten_glVertexAttrib4fv,Se:_emscripten_glVertexAttribDivisor,Od:_emscripten_glVertexAttribDivisorANGLE,Hb:_emscripten_glVertexAttribDivisorARB,$c:_emscripten_glVertexAttribDivisorEXT,Ib:_emscripten_glVertexAttribDivisorNV,yc:_emscripten_glVertexAttribI4i,wc:_emscripten_glVertexAttribI4iv,xc:_emscripten_glVertexAttribI4ui,vc:_emscripten_glVertexAttribI4uiv,Te:_emscripten_glVertexAttribIPointer,sf:_emscripten_glVertexAttribPointer,tf:_emscripten_glViewport,xe:_emscripten_glWaitSync,cb:_emscripten_request_animation_frame_loop,jb:_emscripten_resize_heap,Bb:_environ_get,Cb:_environ_sizes_get,Qa:_exit,fa:_fd_close,kb:_fd_pread,vb:_fd_read,ob:_fd_seek,Y:_fd_write,Oa:_glGetIntegerv,ha:_glGetString,Pa:_glGetStringi,Xd:invoke_dd,Wd:invoke_ddd,Yd:invoke_dddd,qa:invoke_diii,Vd:invoke_fff,x:invoke_ffif,q:invoke_ffifif,Q:invoke_ffifiii,J:invoke_fi,aa:invoke_fif,ra:invoke_fiii,Sa:invoke_fiiiif,ma:invoke_fiiiii,p:invoke_i,Va:invoke_if,Ta:invoke_iffiiiiiiii,g:invoke_ii,E:invoke_iif,na:invoke_iiffi,h:invoke_iii,ab:invoke_iiif,f:invoke_iiii,k:invoke_iiiii,fb:invoke_iiiiid,$:invoke_iiiiii,y:invoke_iiiiiii,s:invoke_iiiiiiii,ia:invoke_iiiiiiiiii,ya:invoke_iiiiiiiiiiifiii,W:invoke_iiiiiiiiiiii,Cc:invoke_iiji,hb:invoke_j,_a:invoke_ji,m:invoke_jii,X:invoke_jiiii,Ua:invoke_jiijj,n:invoke_v,la:invoke_vfffffiii,b:invoke_vi,za:invoke_vid,L:invoke_vif,K:invoke_viff,D:invoke_vifff,M:invoke_viffff,z:invoke_vifffff,N:invoke_viffffffffffffffffffff,eb:invoke_viffi,c:invoke_vii,C:invoke_viif,A:invoke_viiff,rc:invoke_viiffiii,ba:invoke_viifif,u:invoke_viififif,w:invoke_viifii,e:invoke_viii,G:invoke_viiif,ja:invoke_viiiffi,P:invoke_viiiffiffii,R:invoke_viiifi,S:invoke_viiififiiiiiiiiiiii,j:invoke_viiii,ga:invoke_viiiif,Z:invoke_viiiiff,O:invoke_viiiifi,i:invoke_viiiii,Kd:invoke_viiiiif,va:invoke_viiiiiff,Xa:invoke_viiiiiffiiifffi,Wa:invoke_viiiiiffiiifii,xa:invoke_viiiiifi,l:invoke_viiiiii,t:invoke_viiiiiii,ea:invoke_viiiiiiii,Ra:invoke_viiiiiiiii,I:invoke_viiiiiiiiii,db:invoke_viiiiiiiiiii,V:invoke_viiiiiiiiiiiiiii,Za:invoke_viiiijjiiiiff,wa:invoke_viiij,$a:invoke_viiijj,T:invoke_viij,r:invoke_viiji,v:invoke_viijiii,ka:invoke_viijiiiif,oa:invoke_viijj,bb:invoke_vijff,pa:invoke_viji,Xc:invoke_vijii,Ya:invoke_vijiiii,_:invoke_vijjjj,ca:_llvm_eh_typeid_for,pb:_random_get};var wasmExports=await createWasm();var ___wasm_call_ctors=wasmExports["Hg"];var _init=Module["_init"]=wasmExports["Jg"];var _tick=Module["_tick"]=wasmExports["Kg"];var _resize_surface=Module["_resize_surface"]=wasmExports["Lg"];var _redraw=Module["_redraw"]=wasmExports["Mg"];var _load_scene_json=Module["_load_scene_json"]=wasmExports["Ng"];var _apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Og"];var _pointer_move=Module["_pointer_move"]=wasmExports["Pg"];var _command=Module["_command"]=wasmExports["Qg"];var _set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Rg"];var _add_image=Module["_add_image"]=wasmExports["Sg"];var _get_image_bytes=Module["_get_image_bytes"]=wasmExports["Tg"];var _get_image_size=Module["_get_image_size"]=wasmExports["Ug"];var _add_font=Module["_add_font"]=wasmExports["Vg"];var _has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["Wg"];var _list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["Xg"];var _list_available_fonts=Module["_list_available_fonts"]=wasmExports["Yg"];var _set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Zg"];var _get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["_g"];var _get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["$g"];var _get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["ah"];var _get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["bh"];var _get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["ch"];var _export_node_as=Module["_export_node_as"]=wasmExports["dh"];var _to_vector_network=Module["_to_vector_network"]=wasmExports["eh"];var _set_debug=Module["_set_debug"]=wasmExports["fh"];var _toggle_debug=Module["_toggle_debug"]=wasmExports["gh"];var _set_verbose=Module["_set_verbose"]=wasmExports["hh"];var _devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["ih"];var _devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["jh"];var _runtime_renderer_set_cache_tile=Module["_runtime_renderer_set_cache_tile"]=wasmExports["kh"];var _devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["lh"];var _devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["mh"];var _devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["nh"];var _highlight_strokes=Module["_highlight_strokes"]=wasmExports["oh"];var _load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["ph"];var _load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["qh"];var _main=Module["_main"]=wasmExports["rh"];var _grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["sh"];var _grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["th"];var _grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["uh"];var _allocate=Module["_allocate"]=wasmExports["vh"];var _deallocate=Module["_deallocate"]=wasmExports["wh"];var _malloc=wasmExports["xh"];var _emscripten_builtin_memalign=wasmExports["yh"];var _setThrew=wasmExports["zh"];var __emscripten_tempret_set=wasmExports["Ah"];var __emscripten_stack_restore=wasmExports["Bh"];var __emscripten_stack_alloc=wasmExports["Ch"];var _emscripten_stack_get_current=wasmExports["Dh"];var ___cxa_decrement_exception_refcount=wasmExports["Eh"];var ___cxa_increment_exception_refcount=wasmExports["Fh"];var ___cxa_can_catch=wasmExports["Gh"];var ___cxa_get_exception_ptr=wasmExports["Hh"];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_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_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_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_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_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_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_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_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_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_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_jii(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);return 0n}}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_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_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}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_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_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_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_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_fi(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_viifii(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_iiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}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_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffffffffffffffffff(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21)}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_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_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_iiji(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_viiffiii(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_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_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_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_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_vifff(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_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_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_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_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_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_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_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_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_iiif(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_viiijj(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_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_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_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_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_viififif(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_viijiii(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_ffifif(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_ffif(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_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_vijiiii(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_viiiiiffiiifffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_if(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_jiijj(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_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_fiiiii(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_vfffffiii(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_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_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_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_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_viifif(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_fif(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_ffifiii(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_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_iffiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return 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_fiiiif(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_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_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_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_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_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_ddd(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_fff(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_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_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_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_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;args.forEach(arg=>{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()}}function preInit(){if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}preInit();run();moduleRtn=readyPromise; return moduleRtn; 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 bcca02446c..d272dd3bc7 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:256ca879434907400ed15ff00fcf60d661a184e92cc0b516e3d5404501803507 -size 10495917 +oid sha256:ad1a8881c70983028d0825e9491a57f4f97d9017741608ff214c147b9f4abae7 +size 10779473 diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index 1d2053ee9d..a877ae6ab7 100644 --- a/crates/grida-canvas-wasm/package.json +++ b/crates/grida-canvas-wasm/package.json @@ -1,7 +1,7 @@ { "name": "@grida/canvas-wasm", "description": "WASM bindings for Grida Canvas", - "version": "0.0.79", + "version": "0.0.80-canary.3", "keywords": [ "grida", "canvas", diff --git a/crates/grida-canvas/AGENTS.md b/crates/grida-canvas/AGENTS.md index 047bf22e16..f0a719a10a 100644 --- a/crates/grida-canvas/AGENTS.md +++ b/crates/grida-canvas/AGENTS.md @@ -48,6 +48,34 @@ cargo build cargo run --example ``` +## Tools + +### `tool_io_grida` - Grida File Validator + +A CLI tool for validating `.grida` files and debugging parsing issues. + +**Usage:** + +```sh +cargo run --example tool_io_grida +``` + +**Features:** + +- Validates `.grida` file structure and parses all nodes +- Reports total node count, scene references, and entry scene +- Provides node type breakdown (container, text, image, etc.) +- Detects parsing errors with detailed error messages +- Handles legacy file formats gracefully (missing fields, typos, etc.) + +**Example:** + +```sh +cargo run --example tool_io_grida ../../editor/public/examples/canvas/instagram-post-01.grida +``` + +See [examples/tool_io_grida.rs](./examples/tool_io_grida.rs) for full documentation. + ## Package Docs ```sh diff --git a/crates/grida-canvas/CHANGELOG.md b/crates/grida-canvas/CHANGELOG.md index 52ce74a16b..bed71257bf 100644 --- a/crates/grida-canvas/CHANGELOG.md +++ b/crates/grida-canvas/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to the grida-canvas crate will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.0.0-local.2] - 2025-10-19 + +### Added + +- **Infinite Canvas + Flex Layout**: Root nodes (artboards) can be positioned anywhere in the viewport while their children participate in flex layout +- **Universal Flex Layout Support**: All node types (Rectangle, Ellipse, Image, Text, etc.) can now participate in flex layout with `layout_positioning` and `layout_grow` properties +- **Figma Layout Import**: Automatically map Figma's `layoutPositioning` to internal layout properties for all node types +- **Root Node Positioning**: Containers and shapes now respect their x, y coordinates when used as root nodes (infinite canvas) +- **Flex Layout Stacking**: Children are now properly positioned horizontally/vertically instead of stacking at (0, 0) +- **Gap Stretching**: Vertical gap no longer incorrectly grows when parent container height increases +- **Wrap Alignment**: Center alignment now works correctly with flex wrapping enabled +- **Absolute Positioning**: Absolutely positioned children are correctly excluded from flex flow and positioned relative to parent +- **Fixed-size Elements**: Elements maintain their specified dimensions instead of automatically shrinking when overflowing (flex_shrink: 0.0 default) + +### Changed + +- **Non-uniform Padding**: Support for CSS-style padding with individual values per side (`paddingTop`, `paddingRight`, `paddingBottom`, `paddingLeft`) + ## [0.0.0-local.1] - 2025-10-16 ### Added diff --git a/crates/grida-canvas/benches/bench_rectangles.rs b/crates/grida-canvas/benches/bench_rectangles.rs index 3e8752c325..9979e58d07 100644 --- a/crates/grida-canvas/benches/bench_rectangles.rs +++ b/crates/grida-canvas/benches/bench_rectangles.rs @@ -11,7 +11,7 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { // Create rectangles let rectangles: Vec = (0..count) - .map(|i| { + .map(|_i| { Node::Rectangle(RectangleNodeRec { active: true, opacity: 1.0, @@ -39,6 +39,7 @@ fn create_rectangles(count: usize, with_effects: bool) -> Scene { } else { LayerEffects::default() }, + layout_child: None, }) }) .collect(); diff --git a/crates/grida-canvas/examples/golden_container_stroke.rs b/crates/grida-canvas/examples/golden_container_stroke.rs index 91dc412743..c6d9f03ea2 100644 --- a/crates/grida-canvas/examples/golden_container_stroke.rs +++ b/crates/grida-canvas/examples/golden_container_stroke.rs @@ -11,16 +11,15 @@ async fn scene() -> Scene { let mut graph = SceneGraph::new(); let mut container = nf.create_container_node(); - container.size = Size { - width: 400.0, - height: 400.0, - }; + container.layout_dimensions.width = Some(400.0); + container.layout_dimensions.height = Some(400.0); container.stroke_width = 10.0; container.stroke_align = StrokeAlign::Outside; container.strokes = Paints::new([Paint::from(CGColor(255, 0, 0, 255))]); container.set_fill(Paint::from(CGColor(255, 255, 255, 255))); // Center the container in the 800x800 canvas - container.transform = AffineTransform::new(200.0, 200.0, 0.0); + container.position = CGPoint::new(200.0, 200.0).into(); + container.rotation = 0.0; // Create a circle that will overlap with the container's stroke let mut circle = nf.create_ellipse_node(); diff --git a/crates/grida-canvas/examples/golden_layout_flex.rs b/crates/grida-canvas/examples/golden_layout_flex.rs index ca38223d46..71c3cd0cf7 100644 --- a/crates/grida-canvas/examples/golden_layout_flex.rs +++ b/crates/grida-canvas/examples/golden_layout_flex.rs @@ -1,9 +1,12 @@ use cg::cg::types::*; -use cg::layout::tmp_example::{compute_flex_layout_for_container, ContainerWithStyle}; -use cg::layout::{ComputedLayout, LayoutStyle}; -use cg::node::schema::{ContainerNodeRec, Size as SchemaSize}; +use cg::layout::engine::LayoutEngine; +use cg::layout::ComputedLayout; +use cg::node::factory::NodeFactory; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::{ + ContainerNodeRec, LayoutContainerStyle, LayoutDimensionStyle, Node, Scene, Size, +}; use skia_safe::{surfaces, Color, Font, FontMgr, Paint, Rect}; -use taffy::prelude::*; fn create_container(id: &str, width: f32, height: f32) -> ContainerNodeRec { create_container_with_gap(id, width, height, 10.0) @@ -15,33 +18,44 @@ fn create_container_with_gap(id: &str, width: f32, height: f32, gap: f32) -> Con use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); - let id_u64 = hasher.finish(); + let _ = hasher.finish(); // Generate ID but don't store it since it's not used ContainerNodeRec { active: true, opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, - transform: math2::transform::AffineTransform::identity(), - size: SchemaSize { width, height }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), + rotation: 0.0, + position: Default::default(), + corner_radius: Default::default(), + fills: Default::default(), + strokes: Default::default(), stroke_width: 0.0, stroke_align: StrokeAlign::Center, stroke_dash_array: None, - effects: cg::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: LayoutMode::Flex, - layout_direction: Axis::Horizontal, - layout_wrap: LayoutWrap::Wrap, - layout_main_axis_alignment: MainAxisAlignment::Start, - layout_cross_axis_alignment: CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap { - main_axis_gap: gap, - cross_axis_gap: gap, + effects: Default::default(), + clip: Default::default(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + layout_wrap: Some(LayoutWrap::Wrap), + layout_main_axis_alignment: Some(MainAxisAlignment::Start), + layout_cross_axis_alignment: Some(CrossAxisAlignment::Start), + layout_padding: None, + layout_gap: Some(LayoutGap { + main_axis_gap: gap, + cross_axis_gap: gap, + }), }, + layout_dimensions: LayoutDimensionStyle { + width: Some(width), + height: Some(height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: None, } } @@ -86,30 +100,50 @@ fn main() { // Scenario 1: Wide container (800px) - No wrap expected { - let container = - ContainerWithStyle::from_container(create_container("scenario-1", 800.0, 200.0)) - .with_layout(LayoutStyle { - width: Dimension::length(800.0), - height: Dimension::auto(), - flex_grow: 0.0, - }); - - let children: Vec = (0..6) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-1-{}", i), - 100.0, - 100.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(100.0), - height: Dimension::length(100.0), - ..Default::default() - }) - }) - .collect(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create ICB with flex layout + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::Wrap; + icb.layout_main_axis_alignment = MainAxisAlignment::Start; + icb.layout_cross_axis_alignment = CrossAxisAlignment::Start; + icb.layout_gap = LayoutGap { + main_axis_gap: 10.0, + cross_axis_gap: 10.0, + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + // Add children to scene graph + for i in 0..6 { + let child = create_container(&format!("child-1-{}", i), 100.0, 100.0); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + // Compute layout using production pipeline + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { + width: 800.0, + height: 200.0, + }, + ); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + // Extract layouts in order + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect(); let height_used = render_scenario( canvas, @@ -128,30 +162,46 @@ fn main() { // Scenario 2: Medium container (400px) - Partial wrap { - let container = - ContainerWithStyle::from_container(create_container("scenario-2", 400.0, 300.0)) - .with_layout(LayoutStyle { - width: Dimension::length(400.0), - height: Dimension::auto(), - flex_grow: 0.0, - }); - - let children: Vec = (0..6) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-2-{}", i), - 100.0, - 100.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(100.0), - height: Dimension::length(100.0), - ..Default::default() - }) - }) - .collect(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::Wrap; + icb.layout_main_axis_alignment = MainAxisAlignment::Start; + icb.layout_cross_axis_alignment = CrossAxisAlignment::Start; + icb.layout_gap = LayoutGap { + main_axis_gap: 10.0, + cross_axis_gap: 10.0, + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + for i in 0..6 { + let child = create_container(&format!("child-2-{}", i), 100.0, 100.0); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { + width: 400.0, + height: 300.0, + }, + ); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect(); let height_used = render_scenario( canvas, @@ -170,30 +220,46 @@ fn main() { // Scenario 3: Narrow container (250px) - Heavy wrap { - let container = - ContainerWithStyle::from_container(create_container("scenario-3", 250.0, 700.0)) - .with_layout(LayoutStyle { - width: Dimension::length(250.0), - height: Dimension::auto(), - flex_grow: 0.0, - }); - - let children: Vec = (0..6) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-3-{}", i), - 100.0, - 100.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(100.0), - height: Dimension::length(100.0), - ..Default::default() - }) - }) - .collect(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::Wrap; + icb.layout_main_axis_alignment = MainAxisAlignment::Start; + icb.layout_cross_axis_alignment = CrossAxisAlignment::Start; + icb.layout_gap = LayoutGap { + main_axis_gap: 10.0, + cross_axis_gap: 10.0, + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + for i in 0..6 { + let child = create_container(&format!("child-3-{}", i), 100.0, 100.0); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { + width: 250.0, + height: 700.0, + }, + ); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect(); let height_used = render_scenario( canvas, @@ -212,13 +278,20 @@ fn main() { // Scenario 4: Different child sizes in medium container { - let container = - ContainerWithStyle::from_container(create_container("scenario-4", 500.0, 300.0)) - .with_layout(LayoutStyle { - width: Dimension::length(500.0), - height: Dimension::auto(), - flex_grow: 0.0, - }); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::Wrap; + icb.layout_main_axis_alignment = MainAxisAlignment::Start; + icb.layout_cross_axis_alignment = CrossAxisAlignment::Start; + icb.layout_gap = LayoutGap { + main_axis_gap: 10.0, + cross_axis_gap: 10.0, + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); let child_sizes = vec![ (80.0, 80.0), @@ -229,25 +302,32 @@ fn main() { (110.0, 70.0), ]; - let children: Vec = child_sizes + for (i, (w, h)) in child_sizes.iter().enumerate() { + let child = create_container(&format!("child-4-{}", i), *w, *h); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { + width: 500.0, + height: 300.0, + }, + ); + + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids .iter() - .enumerate() - .map(|(i, (w, h))| { - ContainerWithStyle::from_container(create_container( - &format!("child-4-{}", i), - *w, - *h, - )) - .with_layout(LayoutStyle { - width: Dimension::length(*w), - height: Dimension::length(*h), - ..Default::default() - }) - }) + .map(|id| layout_result.get(id).cloned().unwrap()) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); - let _height_used = render_scenario( canvas, &layouts, @@ -266,13 +346,12 @@ fn main() { let data = image .encode(None, skia_safe::EncodedImageFormat::PNG, None) .expect("encode png"); - std::fs::write( - concat!(env!("CARGO_MANIFEST_DIR"), "/goldens/layout_flex.png"), - data.as_bytes(), - ) - .unwrap(); + // Use cargo env to get the correct output directory + let output_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()); + let output_path = format!("{}/goldens/layout_flex.png", output_dir); + std::fs::write(&output_path, data.as_bytes()).unwrap(); - println!("✓ Generated goldens/layout_flex.png"); + println!("✓ Generated {}", output_path); } /// Render a single layout scenario and return the total height used diff --git a/crates/grida-canvas/examples/golden_layout_flex_alignment.rs b/crates/grida-canvas/examples/golden_layout_flex_alignment.rs index 332f38b46e..282b259067 100644 --- a/crates/grida-canvas/examples/golden_layout_flex_alignment.rs +++ b/crates/grida-canvas/examples/golden_layout_flex_alignment.rs @@ -1,48 +1,12 @@ use cg::cg::types::*; -use cg::layout::tmp_example::{compute_flex_layout_for_container, ContainerWithStyle}; -use cg::layout::{ComputedLayout, LayoutStyle}; -use cg::node::schema::{ContainerNodeRec, Size as SchemaSize}; +use cg::layout::engine::LayoutEngine; +use cg::layout::ComputedLayout; +use cg::node::factory::NodeFactory; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::{ + ContainerNodeRec, LayoutContainerStyle, LayoutDimensionStyle, Node, Scene, Size, +}; use skia_safe::{surfaces, Color, Font, FontMgr, Paint, Rect}; -use taffy::prelude::*; - -fn create_container_with_alignment( - id: &str, - width: f32, - height: f32, - main_axis_alignment: MainAxisAlignment, - cross_axis_alignment: CrossAxisAlignment, -) -> ContainerNodeRec { - // Use a simple hash of the string as u64 ID - use std::collections::hash_map::DefaultHasher; - use std::hash::{Hash, Hasher}; - let mut hasher = DefaultHasher::new(); - id.hash(&mut hasher); - let id_u64 = hasher.finish(); - - ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: math2::transform::AffineTransform::identity(), - size: SchemaSize { width, height }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - stroke_dash_array: None, - effects: cg::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: LayoutMode::Flex, - layout_direction: Axis::Horizontal, - layout_wrap: LayoutWrap::NoWrap, - layout_main_axis_alignment: main_axis_alignment, - layout_cross_axis_alignment: cross_axis_alignment, - padding: EdgeInsets::default(), - layout_gap: LayoutGap::uniform(10.0), - } -} fn create_child_container(id: &str, width: f32, height: f32) -> ContainerNodeRec { // Use a simple hash of the string as u64 ID @@ -50,30 +14,41 @@ fn create_child_container(id: &str, width: f32, height: f32) -> ContainerNodeRec use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); - let id_u64 = hasher.finish(); + let _ = hasher.finish(); // Generate ID but don't store it since it's not used ContainerNodeRec { active: true, opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, - transform: math2::transform::AffineTransform::identity(), - size: SchemaSize { width, height }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), + rotation: 0.0, + position: Default::default(), + corner_radius: Default::default(), + fills: Default::default(), + strokes: Default::default(), stroke_width: 0.0, stroke_align: StrokeAlign::Center, stroke_dash_array: None, - effects: cg::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: LayoutMode::Normal, - layout_direction: Axis::Horizontal, - layout_wrap: LayoutWrap::NoWrap, - layout_main_axis_alignment: MainAxisAlignment::Start, - layout_cross_axis_alignment: CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap::default(), + effects: Default::default(), + clip: Default::default(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(width), + height: Some(height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: None, } } @@ -124,35 +99,44 @@ fn main() { ]; for (alignment, label) in main_axis_alignments { - let container = ContainerWithStyle::from_container(create_container_with_alignment( - &format!("main-{:?}", alignment), - 600.0, - 120.0, - alignment, - CrossAxisAlignment::Center, - )) - .with_layout(LayoutStyle { - width: Dimension::length(600.0), - height: Dimension::length(120.0), - ..Default::default() - }); - - let children: Vec = (0..3) - .map(|i| { - ContainerWithStyle::from_container(create_child_container( - &format!("child-main-{:?}-{}", alignment, i), - 80.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: Dimension::length(80.0), - ..Default::default() - }) - }) - .collect(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::NoWrap; + icb.layout_main_axis_alignment = alignment; + icb.layout_cross_axis_alignment = CrossAxisAlignment::Center; + icb.layout_gap = LayoutGap::uniform(10.0); + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + for i in 0..3 { + let child = + create_child_container(&format!("child-main-{:?}-{}", alignment, i), 80.0, 80.0); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { + width: 600.0, + height: 120.0, + }, + ); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect(); let height_used = render_scenario( canvas, @@ -181,57 +165,44 @@ fn main() { ]; for (alignment, label) in cross_axis_alignments { - let container = ContainerWithStyle::from_container(create_container_with_alignment( - &format!("cross-{:?}", alignment), - 600.0, - 150.0, - MainAxisAlignment::Start, - alignment, - )) - .with_layout(LayoutStyle { - width: Dimension::length(600.0), - height: Dimension::length(150.0), - ..Default::default() - }); - - let children: Vec = vec![ - ContainerWithStyle::from_container(create_child_container("child-cross-1", 80.0, 60.0)) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: if matches!(alignment, CrossAxisAlignment::Stretch) { - Dimension::auto() - } else { - Dimension::length(60.0) - }, - ..Default::default() - }), - ContainerWithStyle::from_container(create_child_container( - "child-cross-2", - 80.0, - 100.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: if matches!(alignment, CrossAxisAlignment::Stretch) { - Dimension::auto() - } else { - Dimension::length(100.0) - }, - ..Default::default() - }), - ContainerWithStyle::from_container(create_child_container("child-cross-3", 80.0, 80.0)) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: if matches!(alignment, CrossAxisAlignment::Stretch) { - Dimension::auto() - } else { - Dimension::length(80.0) - }, - ..Default::default() - }), - ]; - - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::NoWrap; + icb.layout_main_axis_alignment = MainAxisAlignment::Start; + icb.layout_cross_axis_alignment = alignment; + icb.layout_gap = LayoutGap::uniform(10.0); + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + let child_sizes = vec![(80.0, 60.0), (80.0, 100.0), (80.0, 80.0)]; + for (i, (w, h)) in child_sizes.iter().enumerate() { + let child = create_child_container(&format!("child-cross-{}", i + 1), *w, *h); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { + width: 600.0, + height: 150.0, + }, + ); + + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect(); let height_used = render_scenario( canvas, @@ -253,9 +224,12 @@ fn main() { let data = image .encode(None, skia_safe::EncodedImageFormat::PNG, None) .unwrap(); - std::fs::write("goldens/layout_flex_alignment.png", data.as_bytes()).unwrap(); + // Use cargo env to get the correct output directory + let output_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()); + let output_path = format!("{}/goldens/layout_flex_alignment.png", output_dir); + std::fs::write(&output_path, data.as_bytes()).unwrap(); - println!("✓ Generated goldens/layout_flex_alignment.png"); + println!("✓ Generated {}", output_path); } fn render_scenario( diff --git a/crates/grida-canvas/examples/golden_layout_flex_padding.rs b/crates/grida-canvas/examples/golden_layout_flex_padding.rs index 625dbb263e..95f0b045fb 100644 --- a/crates/grida-canvas/examples/golden_layout_flex_padding.rs +++ b/crates/grida-canvas/examples/golden_layout_flex_padding.rs @@ -1,9 +1,10 @@ use cg::cg::types::*; -use cg::layout::tmp_example::{compute_flex_layout_for_container, ContainerWithStyle}; -use cg::layout::LayoutStyle; -use cg::node::schema::{ContainerNodeRec, Size}; -use skia_safe::{Canvas, Color, Font, Paint, Rect}; -use taffy::prelude::*; +use cg::layout::engine::LayoutEngine; +use cg::layout::ComputedLayout; +use cg::node::factory::NodeFactory; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::*; +use skia_safe::{Color, Paint, Rect}; fn main() { // Create a surface for rendering @@ -13,93 +14,97 @@ fn main() { // Clear background canvas.clear(Color::from_argb(255, 255, 255, 255)); - // Create base container - let base_container = ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: math2::transform::AffineTransform::identity(), - size: Size { + // Build scene graph with production pipeline + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create ICB with flex layout and padding + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::Wrap; + icb.layout_main_axis_alignment = MainAxisAlignment::Start; + icb.layout_cross_axis_alignment = CrossAxisAlignment::Start; + icb.padding = EdgeInsets { + left: 20.0, + right: 20.0, + top: 20.0, + bottom: 20.0, + }; + icb.layout_gap = LayoutGap { + main_axis_gap: 10.0, + cross_axis_gap: 10.0, + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + // Add child containers + for i in 1..=4 { + let child = create_child_container(&format!("child-{}", i), 100.0, 80.0); + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + // Compute layout using production pipeline + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute( + &scene, + Size { width: 400.0, height: 200.0, }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - stroke_dash_array: None, - effects: cg::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: LayoutMode::Flex, - layout_direction: Axis::Horizontal, - layout_wrap: LayoutWrap::Wrap, - layout_main_axis_alignment: MainAxisAlignment::Start, - layout_cross_axis_alignment: CrossAxisAlignment::Start, - padding: EdgeInsets { - left: 20.0, - right: 20.0, - top: 20.0, - bottom: 20.0, - }, - layout_gap: LayoutGap { - main_axis_gap: 10.0, - cross_axis_gap: 10.0, - }, - }; + ); - // Create container with layout style - let container_with_style = - ContainerWithStyle::from_container(base_container).with_layout(LayoutStyle { - width: Dimension::length(400.0), - height: Dimension::length(200.0), - flex_grow: 0.0, - }); + // Extract layouts + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + let layouts: Vec = children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect(); - // Create child containers - let child_containers = vec![ - create_child_container("child-1", 100.0, 80.0), - create_child_container("child-2", 100.0, 80.0), - create_child_container("child-3", 100.0, 80.0), - create_child_container("child-4", 100.0, 80.0), + // Render the demo (simplified - just show the layouts work) + // Note: render_demo function expects ContainerNodeRec which we don't have anymore + // For now, let's just draw the computed layouts directly + let colors = [ + CGColor::from_rgb(239, 68, 68), // red + CGColor::from_rgb(59, 130, 246), // blue + CGColor::from_rgb(34, 197, 94), // green + CGColor::from_rgb(234, 179, 8), // yellow ]; - // Create children with layout styles - let children_with_styles: Vec = child_containers - .into_iter() - .map(|child| { - ContainerWithStyle::from_container(child).with_layout(LayoutStyle { - width: Dimension::length(100.0), - height: Dimension::length(80.0), - flex_grow: 1.0, - ..Default::default() - }) - }) - .collect(); - - // Compute layout - let layouts = compute_flex_layout_for_container( - &container_with_style, - children_with_styles.iter().collect(), - ); + for (i, layout) in layouts.iter().enumerate() { + let mut paint = Paint::default(); + paint.set_anti_alias(true); + let color = colors[i % colors.len()]; + paint.set_color(Color::from_argb(255, color.r(), color.g(), color.b())); + paint.set_style(skia_safe::PaintStyle::Fill); - // Render the demo - render_demo( - canvas, - &container_with_style, - &children_with_styles, - &layouts, - ); + // Offset by base position and padding + let rect = Rect::from_xywh( + 50.0 + layout.x, + 50.0 + layout.y, + layout.width, + layout.height, + ); + canvas.draw_rect(&rect, &paint); + } // Save the result let image = surface.image_snapshot(); let data = image .encode(None, skia_safe::EncodedImageFormat::PNG, None) .unwrap(); - std::fs::write("goldens/layout_flex_padding.png", data.as_bytes()).unwrap(); - println!("✓ Generated goldens/layout_flex_padding.png"); + // Use cargo env to get the correct output directory + let output_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()); + let output_path = format!("{}/goldens/layout_flex_padding.png", output_dir); + std::fs::write(&output_path, data.as_bytes()).unwrap(); + + println!("✓ Generated {}", output_path); } fn create_child_container(id: &str, width: f32, height: f32) -> ContainerNodeRec { @@ -108,139 +113,40 @@ fn create_child_container(id: &str, width: f32, height: f32) -> ContainerNodeRec use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); - let id_u64 = hasher.finish(); + let _ = hasher.finish(); // Generate ID but don't store it since it's not used ContainerNodeRec { active: true, opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, - transform: math2::transform::AffineTransform::identity(), - size: Size { width, height }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), + rotation: 0.0, + position: Default::default(), + corner_radius: Default::default(), + fills: Default::default(), + strokes: Default::default(), stroke_width: 0.0, stroke_align: StrokeAlign::Center, stroke_dash_array: None, - effects: cg::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: LayoutMode::Normal, - layout_direction: Axis::Horizontal, - layout_wrap: LayoutWrap::NoWrap, - layout_main_axis_alignment: MainAxisAlignment::Start, - layout_cross_axis_alignment: CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap::default(), - } -} - -fn render_demo( - canvas: &Canvas, - container: &ContainerWithStyle, - children: &[ContainerWithStyle], - layouts: &[cg::layout::ComputedLayout], -) { - let base_x = 50.0; - let base_y = 50.0; - - // Draw container outline - let mut container_paint = Paint::default(); - container_paint.set_anti_alias(true); - container_paint.set_color(Color::from_argb(100, 0, 0, 0)); - container_paint.set_style(skia_safe::PaintStyle::Stroke); - container_paint.set_stroke_width(2.0); - - let container_rect = Rect::from_xywh( - base_x, - base_y, - container.available_size().0, - container.available_size().1, - ); - canvas.draw_rect(&container_rect, &container_paint); - - // Draw padding area - let mut padding_paint = Paint::default(); - padding_paint.set_anti_alias(true); - padding_paint.set_color(Color::from_argb(50, 0, 0, 255)); - padding_paint.set_style(skia_safe::PaintStyle::Fill); - - let padding_rect = Rect::from_xywh( - base_x + 20.0, // padding left - base_y + 20.0, // padding top - container.available_size().0 - 40.0, // width - padding - container.available_size().1 - 40.0, // height - padding - ); - canvas.draw_rect(&padding_rect, &padding_paint); - - // Draw children - let colors = [ - Color::from_argb(200, 255, 0, 0), // Red - Color::from_argb(200, 0, 255, 0), // Green - Color::from_argb(200, 0, 0, 255), // Blue - Color::from_argb(200, 255, 255, 0), // Yellow - ]; - - for (i, layout) in layouts.iter().enumerate() { - let color = colors[i % colors.len()]; - let mut paint = Paint::default(); - paint.set_anti_alias(true); - paint.set_color(color); - - let child_rect = Rect::from_xywh( - base_x + layout.x, - base_y + layout.y, - layout.width, - layout.height, - ); - - // Draw rounded rectangle - let rrect = skia_safe::RRect::new_rect_radii( - child_rect, - &[ - skia_safe::Point::new(6.0, 6.0), - skia_safe::Point::new(6.0, 6.0), - skia_safe::Point::new(6.0, 6.0), - skia_safe::Point::new(6.0, 6.0), - ], - ); - canvas.draw_rrect(&rrect, &paint); - - // Draw border - let mut border_paint = Paint::default(); - border_paint.set_anti_alias(true); - border_paint.set_color(Color::from_argb(80, 0, 0, 0)); - border_paint.set_style(skia_safe::PaintStyle::Stroke); - border_paint.set_stroke_width(1.5); - canvas.draw_rrect(&rrect, &border_paint); - - // Draw child number - let typeface = cg::fonts::embedded::TYPEFACE_GEISTMONO.with(|t| t.clone()); - let font = Font::new(typeface, 16.0); - let mut text_paint = Paint::default(); - text_paint.set_anti_alias(true); - text_paint.set_color(Color::from_argb(255, 0, 0, 0)); - - let text = format!("{}", i + 1); - canvas.draw_str( - &text, - (base_x + layout.x + 5.0, base_y + layout.y + 20.0), - &font, - &text_paint, - ); + effects: Default::default(), + clip: Default::default(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(width), + height: Some(height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: None, } - - // Draw title - let typeface = cg::fonts::embedded::TYPEFACE_GEISTMONO.with(|t| t.clone()); - let title_font = Font::new(typeface, 20.0); - let mut title_paint = Paint::default(); - title_paint.set_anti_alias(true); - title_paint.set_color(Color::from_argb(255, 0, 0, 0)); - - canvas.draw_str( - "ContainerWithStyle Demo - Flex Layout with Padding", - (base_x, base_y - 30.0), - &title_font, - &title_paint, - ); } diff --git a/crates/grida-canvas/examples/golden_layout_padding.rs b/crates/grida-canvas/examples/golden_layout_padding.rs index 7bea1acf1d..481276ee83 100644 --- a/crates/grida-canvas/examples/golden_layout_padding.rs +++ b/crates/grida-canvas/examples/golden_layout_padding.rs @@ -1,9 +1,9 @@ use cg::cg::types::*; -use cg::layout::tmp_example::{compute_flex_layout_for_container, ContainerWithStyle}; -use cg::layout::{ComputedLayout, LayoutStyle}; -use cg::node::schema::{ContainerNodeRec, Size as SchemaSize}; +use cg::layout::engine::LayoutEngine; +use cg::layout::ComputedLayout; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::*; use skia_safe::{surfaces, Color, Font, FontMgr, Paint, Rect}; -use taffy::prelude::*; fn create_container(id: &str, width: f32, height: f32) -> ContainerNodeRec { create_container_with_padding(id, width, height, 0.0) @@ -20,38 +20,108 @@ fn create_container_with_padding( use std::hash::{Hash, Hasher}; let mut hasher = DefaultHasher::new(); id.hash(&mut hasher); - let id_u64 = hasher.finish(); + let _ = hasher.finish(); // Generate ID but don't store it since it's not used ContainerNodeRec { active: true, opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, - transform: math2::transform::AffineTransform::identity(), - size: SchemaSize { width, height }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), + rotation: 0.0, + position: Default::default(), + corner_radius: Default::default(), + fills: Default::default(), + strokes: Default::default(), stroke_width: 0.0, stroke_align: StrokeAlign::Center, stroke_dash_array: None, - effects: cg::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: LayoutMode::Flex, - layout_direction: Axis::Horizontal, - layout_wrap: LayoutWrap::NoWrap, - layout_main_axis_alignment: MainAxisAlignment::Start, - layout_cross_axis_alignment: CrossAxisAlignment::Start, - padding: EdgeInsets { - left: padding, - right: padding, - top: padding, - bottom: padding, + effects: Default::default(), + clip: Default::default(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + layout_wrap: Some(LayoutWrap::NoWrap), + layout_main_axis_alignment: Some(MainAxisAlignment::Start), + layout_cross_axis_alignment: Some(CrossAxisAlignment::Start), + layout_padding: Some(EdgeInsets { + left: padding, + right: padding, + top: padding, + bottom: padding, + }), + layout_gap: None, }, - layout_gap: LayoutGap::default(), + layout_dimensions: LayoutDimensionStyle { + width: Some(width), + height: Some(height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: None, } } +/// Build scene and compute layout using production pipeline +fn compute_layout_with_production_pipeline( + icb_config: ContainerNodeRec, + children: Vec, + viewport_size: Size, +) -> Vec { + let mut graph = SceneGraph::new(); + + // Create ICB from config (convert to InitialContainer) + let icb = cg::node::schema::InitialContainerNodeRec { + active: icb_config.active, + layout_mode: icb_config.layout_container.layout_mode, + layout_direction: icb_config.layout_container.layout_direction, + layout_wrap: icb_config + .layout_container + .layout_wrap + .unwrap_or(LayoutWrap::NoWrap), + layout_main_axis_alignment: icb_config + .layout_container + .layout_main_axis_alignment + .unwrap_or(MainAxisAlignment::Start), + layout_cross_axis_alignment: icb_config + .layout_container + .layout_cross_axis_alignment + .unwrap_or(CrossAxisAlignment::Start), + padding: icb_config + .layout_container + .layout_padding + .unwrap_or(EdgeInsets::default()), + layout_gap: icb_config + .layout_container + .layout_gap + .unwrap_or(LayoutGap::default()), + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + // Add children with Inset location for flex layout + for child in children { + // Position is now handled by layout field + graph.append_child(Node::Container(child), Parent::NodeId(icb_id)); + } + + // Compute layout + let scene = Scene { + name: String::new(), + graph, + background_color: None, + }; + let mut layout_engine = LayoutEngine::new(); + let layout_result = layout_engine.compute(&scene, viewport_size); + + // Extract layouts + let children_ids = scene.graph.get_children(&icb_id).unwrap(); + children_ids + .iter() + .map(|id| layout_result.get(id).cloned().unwrap()) + .collect() +} + fn main() { // Create a surface to draw on let (width, height) = (1400, 2000); @@ -94,30 +164,20 @@ fn main() { // Scenario 1: Fixed container + No padding + Fixed children { - let container = - ContainerWithStyle::from_container(create_container("scenario-1", 400.0, 120.0)) - .with_layout(LayoutStyle { - width: Dimension::length(400.0), - height: Dimension::length(120.0), - flex_grow: 0.0, - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-1-{}", i), - 80.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: Dimension::length(80.0), - ..Default::default() - }) - }) + let container = create_container("scenario-1", 400.0, 120.0); + + let children: Vec = (0..4) + .map(|i| create_container(&format!("child-1-{}", i), 80.0, 80.0)) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 400.0, + height: 120.0, + }, + ); let height_used = render_scenario( canvas, @@ -137,34 +197,20 @@ fn main() { // Scenario 2: Fixed container + Padding + Fixed children { - let container = ContainerWithStyle::from_container(create_container_with_padding( - "scenario-2", - 400.0, - 120.0, - 20.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(400.0), - height: Dimension::length(120.0), - flex_grow: 0.0, - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-2-{}", i), - 80.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: Dimension::length(80.0), - ..Default::default() - }) - }) + let container = create_container_with_padding("scenario-2", 400.0, 120.0, 20.0); + + let children: Vec = (0..4) + .map(|i| create_container(&format!("child-2-{}", i), 80.0, 80.0)) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 400.0, + height: 120.0, + }, + ); let _height_used = render_scenario_with_padding( canvas, @@ -185,30 +231,20 @@ fn main() { // Scenario 3: Auto container + No padding + Fixed children { - let container = - ContainerWithStyle::from_container(create_container("scenario-3", 320.0, 80.0)) - .with_layout(LayoutStyle { - width: Dimension::auto(), - height: Dimension::auto(), - flex_grow: 0.0, - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-3-{}", i), - 80.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: Dimension::length(80.0), - ..Default::default() - }) - }) + let container = create_container("scenario-3", 320.0, 80.0); + + let children: Vec = (0..4) + .map(|i| create_container(&format!("child-3-{}", i), 80.0, 80.0)) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 320.0, + height: 80.0, + }, + ); let _height_used = render_scenario( canvas, @@ -228,34 +264,20 @@ fn main() { // Scenario 4: Auto container + Padding + Fixed children { - let container = ContainerWithStyle::from_container(create_container_with_padding( - "scenario-4", - 360.0, - 120.0, - 20.0, - )) - .with_layout(LayoutStyle { - width: Dimension::auto(), - height: Dimension::auto(), - flex_grow: 0.0, - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-4-{}", i), - 80.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(80.0), - height: Dimension::length(80.0), - ..Default::default() - }) - }) + let container = create_container_with_padding("scenario-4", 360.0, 120.0, 20.0); + + let children: Vec = (0..4) + .map(|i| create_container(&format!("child-4-{}", i), 80.0, 80.0)) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 360.0, + height: 120.0, + }, + ); let height_used = render_scenario_with_padding( canvas, @@ -276,31 +298,20 @@ fn main() { // Scenario 5: Fixed container + No padding + Flexible children { - let container = - ContainerWithStyle::from_container(create_container("scenario-5", 400.0, 120.0)) - .with_layout(LayoutStyle { - width: Dimension::length(400.0), - height: Dimension::length(120.0), - flex_grow: 0.0, - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-5-{}", i), - 100.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::auto(), - height: Dimension::length(80.0), - flex_grow: 1.0, - ..Default::default() - }) - }) + let container = create_container("scenario-5", 400.0, 120.0); + + let children: Vec = (0..4) + .map(|i| create_container(&format!("child-5-{}", i), 100.0, 80.0)) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 400.0, + height: 120.0, + }, + ); let height_used = render_scenario( canvas, @@ -320,35 +331,20 @@ fn main() { // Scenario 6: Fixed container + Padding + Flexible children { - let container = ContainerWithStyle::from_container(create_container_with_padding( - "scenario-6", - 400.0, - 120.0, - 20.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(400.0), - height: Dimension::length(120.0), - flex_grow: 0.0, - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(create_container( - &format!("child-6-{}", i), - 90.0, - 80.0, - )) - .with_layout(LayoutStyle { - width: Dimension::auto(), - height: Dimension::length(80.0), - flex_grow: 1.0, - ..Default::default() - }) - }) + let container = create_container_with_padding("scenario-6", 400.0, 120.0, 20.0); + + let children: Vec = (0..4) + .map(|i| create_container(&format!("child-6-{}", i), 90.0, 80.0)) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 400.0, + height: 120.0, + }, + ); let height_used = render_scenario_with_padding( canvas, @@ -369,17 +365,7 @@ fn main() { // Scenario 7: Mixed children (fixed + flexible) + padding { - let container = ContainerWithStyle::from_container(create_container_with_padding( - "scenario-7", - 500.0, - 120.0, - 20.0, - )) - .with_layout(LayoutStyle { - width: Dimension::length(500.0), - height: Dimension::length(120.0), - flex_grow: 0.0, - }); + let container = create_container_with_padding("scenario-7", 500.0, 120.0, 20.0); let child_configs = vec![ (100.0, 80.0, false), // Fixed @@ -388,32 +374,31 @@ fn main() { (120.0, 80.0, true), // Flexible ]; - let children: Vec = child_configs + let children: Vec = child_configs .iter() .enumerate() .map(|(i, (w, h, flexible))| { - let mut layout = LayoutStyle { - width: if *flexible { - Dimension::auto() - } else { - Dimension::length(*w) - }, - height: Dimension::length(*h), - ..Default::default() - }; + let mut container = create_container(&format!("child-7-{}", i), *w, *h); if *flexible { - layout.flex_grow = 1.0; + // Auto width to allow flex-grow - set width to None to indicate auto + container.layout_dimensions.width = None; + container.layout_child = Some(LayoutChildStyle { + layout_grow: 1.0, + layout_positioning: LayoutPositioning::Auto, + }); } - ContainerWithStyle::from_container(create_container( - &format!("child-7-{}", i), - *w, - *h, - )) - .with_layout(layout) + container }) .collect(); - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); + let layouts = compute_layout_with_production_pipeline( + container, + children, + Size { + width: 500.0, + height: 120.0, + }, + ); let _height_used = render_scenario_with_padding( canvas, @@ -435,13 +420,12 @@ fn main() { let data = image .encode(None, skia_safe::EncodedImageFormat::PNG, None) .expect("encode png"); - std::fs::write( - concat!(env!("CARGO_MANIFEST_DIR"), "/goldens/layout_padding.png"), - data.as_bytes(), - ) - .unwrap(); + // Use cargo env to get the correct output directory + let output_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| ".".to_string()); + let output_path = format!("{}/goldens/layout_padding.png", output_dir); + std::fs::write(&output_path, data.as_bytes()).unwrap(); - println!("✓ Generated goldens/layout_padding.png"); + println!("✓ Generated {}", output_path); } /// Render a single layout scenario and return the total height used diff --git a/crates/grida-canvas/examples/golden_pdf.rs b/crates/grida-canvas/examples/golden_pdf.rs index d8f8ccf22b..f6fbb450d3 100644 --- a/crates/grida-canvas/examples/golden_pdf.rs +++ b/crates/grida-canvas/examples/golden_pdf.rs @@ -14,10 +14,8 @@ async fn demo_scene() -> Scene { // Create a root container let mut root_container = nf.create_container_node(); - root_container.size = Size { - width: 900.0, - height: 700.0, - }; + root_container.layout_dimensions.width = Some(900.0); + root_container.layout_dimensions.height = Some(700.0); // Title text let mut title_text = nf.create_text_span_node(); diff --git a/crates/grida-canvas/examples/golden_svg.rs b/crates/grida-canvas/examples/golden_svg.rs index 613c74269c..7599d0c5e9 100644 --- a/crates/grida-canvas/examples/golden_svg.rs +++ b/crates/grida-canvas/examples/golden_svg.rs @@ -15,10 +15,8 @@ async fn demo_scene() -> Scene { // Create a root container let mut root_container = nf.create_container_node(); - root_container.size = Size { - width: 900.0, - height: 700.0, - }; + root_container.layout_dimensions.width = Some(900.0); + root_container.layout_dimensions.height = Some(700.0); let root_container_id = graph.append_child(Node::Container(root_container), Parent::Root); diff --git a/crates/grida-canvas/examples/golden_type_stroke.rs b/crates/grida-canvas/examples/golden_type_stroke.rs index eab599f95b..09858fb71f 100644 --- a/crates/grida-canvas/examples/golden_type_stroke.rs +++ b/crates/grida-canvas/examples/golden_type_stroke.rs @@ -28,6 +28,7 @@ async fn scene() -> Scene { blend_mode: LayerBlendMode::default(), mask: None, effects: LayerEffects::default(), + layout_child: None, }; // Text with Center stroke alignment @@ -50,6 +51,7 @@ async fn scene() -> Scene { blend_mode: LayerBlendMode::default(), mask: None, effects: LayerEffects::default(), + layout_child: None, }; // Text with Inside stroke alignment @@ -72,6 +74,7 @@ async fn scene() -> Scene { blend_mode: LayerBlendMode::default(), mask: None, effects: LayerEffects::default(), + layout_child: None, }; // Add all text nodes as root children in one operation diff --git a/crates/grida-canvas/examples/grida_basic.rs b/crates/grida-canvas/examples/grida_basic.rs index 85f1e879bb..47fe8f5b4f 100644 --- a/crates/grida-canvas/examples/grida_basic.rs +++ b/crates/grida-canvas/examples/grida_basic.rs @@ -145,10 +145,8 @@ async fn demo_basic() -> Scene { // Create a root container node containing the shapes group, text, and line let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1080.0, - height: 1080.0, - }; + root_container_node.layout_dimensions.width = Some(1080.0); + root_container_node.layout_dimensions.height = Some(1080.0); // Build the scene graph let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/grida_blendmode.rs b/crates/grida-canvas/examples/grida_blendmode.rs index 62264e7c73..067906d847 100644 --- a/crates/grida-canvas/examples/grida_blendmode.rs +++ b/crates/grida-canvas/examples/grida_blendmode.rs @@ -13,10 +13,8 @@ async fn demo_blendmode() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 2000.0, - height: 4000.0, - }; + root_container_node.layout_dimensions.width = Some(2000.0); + root_container_node.layout_dimensions.height = Some(4000.0); let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 400.0; diff --git a/crates/grida-canvas/examples/grida_booleans.rs b/crates/grida-canvas/examples/grida_booleans.rs index 4c013c9eb8..1c2cb7786e 100644 --- a/crates/grida-canvas/examples/grida_booleans.rs +++ b/crates/grida-canvas/examples/grida_booleans.rs @@ -13,10 +13,8 @@ async fn demo_booleans() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1080.0, - height: 1080.0, - }; + root_container_node.layout_dimensions.width = Some(1080.0); + root_container_node.layout_dimensions.height = Some(1080.0); let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 200.0; diff --git a/crates/grida-canvas/examples/grida_container.rs b/crates/grida-canvas/examples/grida_container.rs index 9fe3098ae8..3c99748a9b 100644 --- a/crates/grida-canvas/examples/grida_container.rs +++ b/crates/grida-canvas/examples/grida_container.rs @@ -10,11 +10,10 @@ async fn demo_clip() -> Scene { // Create a single container with solid fill let mut container = nf.create_container_node(); - container.transform = AffineTransform::new(100.0, 100.0, 0.0); - container.size = Size { - width: 300.0, - height: 300.0, - }; + container.position = CGPoint::new(100.0, 100.0).into(); + container.rotation = 0.0; + container.layout_dimensions.width = Some(300.0); + container.layout_dimensions.height = Some(300.0); container.corner_radius = RectangularCornerRadius::circular(20.0); container.set_fill(Paint::from(CGColor(240, 100, 100, 255))); container.strokes = Paints::new([Paint::from(CGColor(200, 50, 50, 255))]); diff --git a/crates/grida-canvas/examples/grida_effects.rs b/crates/grida-canvas/examples/grida_effects.rs index aaee436d78..ba7440cdb4 100644 --- a/crates/grida-canvas/examples/grida_effects.rs +++ b/crates/grida-canvas/examples/grida_effects.rs @@ -11,10 +11,8 @@ async fn demo_effects() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 2000.0, - height: 2000.0, - }; + root_container_node.layout_dimensions.width = Some(2000.0); + root_container_node.layout_dimensions.height = Some(2000.0); let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 200.0; diff --git a/crates/grida-canvas/examples/grida_fills.rs b/crates/grida-canvas/examples/grida_fills.rs index 672fa8ef1a..a03c876ba0 100644 --- a/crates/grida-canvas/examples/grida_fills.rs +++ b/crates/grida-canvas/examples/grida_fills.rs @@ -11,10 +11,8 @@ async fn demo_fills() -> Scene { // Root container let mut root = nf.create_container_node(); - root.size = Size { - width: 1200.0, - height: 800.0, - }; + root.layout_dimensions.width = Some(1200.0); + root.layout_dimensions.height = Some(800.0); let root_id = graph.append_child(Node::Container(root), Parent::Root); @@ -348,12 +346,9 @@ async fn demo_fills() -> Scene { // 8. Container with multiple fills (demonstrating container fill capability) let mut multi_fill_container = nf.create_container_node(); - multi_fill_container.transform = - AffineTransform::new(start_x + spacing * 3.0, base_y + spacing, 0.0); - multi_fill_container.size = Size { - width: 150.0, - height: 150.0, - }; + multi_fill_container.position = CGPoint::new(start_x + spacing * 3.0, base_y + spacing).into(); + multi_fill_container.layout_dimensions.width = Some(150.0); + multi_fill_container.layout_dimensions.height = Some(150.0); multi_fill_container.fills = Paints::new([ Paint::from(CGColor(128, 0, 128, 255)), Paint::RadialGradient(RadialGradientPaint { diff --git a/crates/grida-canvas/examples/grida_gradients.rs b/crates/grida-canvas/examples/grida_gradients.rs index 35aabd80a2..6d8105a806 100644 --- a/crates/grida-canvas/examples/grida_gradients.rs +++ b/crates/grida-canvas/examples/grida_gradients.rs @@ -11,10 +11,8 @@ async fn demo_gradients() -> Scene { // root container let mut root = nf.create_container_node(); - root.size = Size { - width: 1200.0, - height: 800.0, - }; + root.layout_dimensions.width = Some(1200.0); + root.layout_dimensions.height = Some(800.0); let root_id = graph.append_child(Node::Container(root), Parent::Root); diff --git a/crates/grida-canvas/examples/grida_image.rs b/crates/grida-canvas/examples/grida_image.rs index 213a731286..19aae38a05 100644 --- a/crates/grida-canvas/examples/grida_image.rs +++ b/crates/grida-canvas/examples/grida_image.rs @@ -24,7 +24,8 @@ async fn demo_image() -> (Scene, Vec) { // Root container let mut root = nf.create_container_node(); - root.size = image8ksize.clone(); + root.layout_dimensions.width = Some(image8ksize.width); + root.layout_dimensions.height = Some(image8ksize.height); // First example: Rectangle with ImagePaint fill let mut rect1 = nf.create_rectangle_node(); diff --git a/crates/grida-canvas/examples/grida_images.rs b/crates/grida-canvas/examples/grida_images.rs index 64803417bc..7ed4c819c8 100644 --- a/crates/grida-canvas/examples/grida_images.rs +++ b/crates/grida-canvas/examples/grida_images.rs @@ -16,10 +16,8 @@ async fn demo_images() -> (Scene, Vec) { // Root container let mut root = nf.create_container_node(); - root.size = Size { - width: 800.0, - height: 800.0, - }; + root.layout_dimensions.width = Some(800.0); + root.layout_dimensions.height = Some(800.0); // First example: Rectangle with ImagePaint fill let mut rect1 = nf.create_rectangle_node(); diff --git a/crates/grida-canvas/examples/grida_lines.rs b/crates/grida-canvas/examples/grida_lines.rs index 0c77c29c13..b22b66e2eb 100644 --- a/crates/grida-canvas/examples/grida_lines.rs +++ b/crates/grida-canvas/examples/grida_lines.rs @@ -9,10 +9,8 @@ async fn demo_lines() -> Scene { let nf = NodeFactory::new(); let mut root = nf.create_container_node(); - root.size = Size { - width: 1000.0, - height: 600.0, - }; + root.layout_dimensions.width = Some(1000.0); + root.layout_dimensions.height = Some(600.0); let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/grida_mask.rs b/crates/grida-canvas/examples/grida_mask.rs index d59db52add..b4a3826de0 100644 --- a/crates/grida-canvas/examples/grida_mask.rs +++ b/crates/grida-canvas/examples/grida_mask.rs @@ -156,7 +156,8 @@ async fn demo_mask_panels() -> Scene { let mut root = nf.create_container_node(); let width = 1400.0; let height = 400.0; - root.size = Size { width, height }; + root.layout_dimensions.width = Some(width); + root.layout_dimensions.height = Some(height); root.clip = false; root.set_fill(CGColor(255, 255, 255, 255).into()); @@ -179,11 +180,10 @@ async fn demo_mask_panels() -> Scene { for kind in kinds { // Panel container per kind let mut panel = nf.create_container_node(); - panel.transform = AffineTransform::new(left, top, 0.0); - panel.size = Size { - width: panel_w, - height: panel_h, - }; + panel.position = CGPoint::new(left, top).into(); + panel.rotation = 0.0; + panel.layout_dimensions.width = Some(panel_w); + panel.layout_dimensions.height = Some(panel_h); panel.corner_radius = RectangularCornerRadius::circular(6.0); panel.set_fill(CGColor(245, 245, 245, 255).into()); diff --git a/crates/grida-canvas/examples/grida_nested.rs b/crates/grida-canvas/examples/grida_nested.rs index 05eb51dd78..6b86f25aa3 100644 --- a/crates/grida-canvas/examples/grida_nested.rs +++ b/crates/grida-canvas/examples/grida_nested.rs @@ -29,16 +29,10 @@ async fn demo_nested() -> Scene { let mut container = nf.create_container_node(); // Each level is centered in its parent with rotation - container.transform = AffineTransform::new( - current_size * 0.075, // Small offset for visual clarity - current_size * 0.075, - rotation, - ); - - container.size = Size { - width: current_size, - height: current_size, - }; + container.position = CGPoint::new(current_size * 0.075, current_size * 0.075).into(); + container.rotation = rotation; + container.layout_dimensions.width = Some(current_size); + container.layout_dimensions.height = Some(current_size); container.corner_radius = RectangularCornerRadius::circular(8.0); // Color gradient from blue (outer) to red (inner) diff --git a/crates/grida-canvas/examples/grida_paint.rs b/crates/grida-canvas/examples/grida_paint.rs index 7b177bd0ae..65f2d450c5 100644 --- a/crates/grida-canvas/examples/grida_paint.rs +++ b/crates/grida-canvas/examples/grida_paint.rs @@ -11,10 +11,8 @@ async fn demo_paints() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1080.0, - height: 1080.0, - }; + root_container_node.layout_dimensions.width = Some(1080.0); + root_container_node.layout_dimensions.height = Some(1080.0); let root_container_id = graph.append_child(Node::Container(root_container_node), Parent::Root); let spacing = 100.0; diff --git a/crates/grida-canvas/examples/grida_shapes.rs b/crates/grida-canvas/examples/grida_shapes.rs index aabcbb73e8..1e12b2e2ef 100644 --- a/crates/grida-canvas/examples/grida_shapes.rs +++ b/crates/grida-canvas/examples/grida_shapes.rs @@ -10,10 +10,8 @@ async fn demo_shapes() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1080.0, - height: 1200.0, - }; + root_container_node.layout_dimensions.width = Some(1080.0); + root_container_node.layout_dimensions.height = Some(1200.0); let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/grida_shapes_ellipse.rs b/crates/grida-canvas/examples/grida_shapes_ellipse.rs index 1d0910f750..491425cc6c 100644 --- a/crates/grida-canvas/examples/grida_shapes_ellipse.rs +++ b/crates/grida-canvas/examples/grida_shapes_ellipse.rs @@ -10,10 +10,8 @@ async fn demo_ellipses() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1200.0, - height: 800.0, - }; + root_container_node.layout_dimensions.width = Some(1200.0); + root_container_node.layout_dimensions.height = Some(800.0); let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/grida_strokes.rs b/crates/grida-canvas/examples/grida_strokes.rs index 20ce231585..c69da8ca29 100644 --- a/crates/grida-canvas/examples/grida_strokes.rs +++ b/crates/grida-canvas/examples/grida_strokes.rs @@ -10,10 +10,8 @@ async fn demo_strokes() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1000.0, - height: 1200.0, - }; + root_container_node.layout_dimensions.width = Some(1000.0); + root_container_node.layout_dimensions.height = Some(1200.0); let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/grida_texts.rs b/crates/grida-canvas/examples/grida_texts.rs index 759ed0d0a0..c949358aa1 100644 --- a/crates/grida-canvas/examples/grida_texts.rs +++ b/crates/grida-canvas/examples/grida_texts.rs @@ -99,10 +99,8 @@ async fn demo_texts() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1080.0, - height: 1080.0, - }; + root_container_node.layout_dimensions.width = Some(1080.0); + root_container_node.layout_dimensions.height = Some(1080.0); // Create a node repository and add all nodes let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/grida_vector.rs b/crates/grida-canvas/examples/grida_vector.rs index 54d16d4139..e84fd5013a 100644 --- a/crates/grida-canvas/examples/grida_vector.rs +++ b/crates/grida-canvas/examples/grida_vector.rs @@ -12,10 +12,8 @@ async fn demo_vectors() -> Scene { // Root container let mut root = nf.create_container_node(); - root.size = Size { - width: 1200.0, - height: 800.0, - }; + root.layout_dimensions.width = Some(1200.0); + root.layout_dimensions.height = Some(800.0); let root_id = graph.append_child(Node::Container(root), Parent::Root); let spacing = 200.0; @@ -46,6 +44,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child( @@ -78,6 +77,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child( @@ -111,6 +111,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child(Node::Vector(vector_node_3), Parent::NodeId(root_id.clone())); @@ -140,6 +141,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child(Node::Vector(vector_node_4), Parent::NodeId(root_id.clone())); @@ -172,6 +174,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child( @@ -213,6 +216,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child(Node::Vector(vector_node_5), Parent::NodeId(root_id.clone())); @@ -243,6 +247,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child( @@ -283,6 +288,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child(Node::Vector(vector_node_6), Parent::NodeId(root_id.clone())); @@ -318,6 +324,7 @@ async fn demo_vectors() -> Scene { stroke_width_profile: None, stroke_align: StrokeAlign::Center, stroke_dash_array: None, + layout_child: None, }; graph.append_child(Node::Vector(vector_node_7), Parent::NodeId(root_id.clone())); diff --git a/crates/grida-canvas/examples/grida_webfonts.rs b/crates/grida-canvas/examples/grida_webfonts.rs index 8ac3d1705a..878622351d 100644 --- a/crates/grida-canvas/examples/grida_webfonts.rs +++ b/crates/grida-canvas/examples/grida_webfonts.rs @@ -114,10 +114,8 @@ async fn demo_webfonts() -> Scene { // Create a root container node let mut root_container_node = nf.create_container_node(); - root_container_node.size = Size { - width: 1080.0, - height: 1080.0, - }; + root_container_node.layout_dimensions.width = Some(1080.0); + root_container_node.layout_dimensions.height = Some(1080.0); // Create a node repository and add all nodes let mut graph = SceneGraph::new(); diff --git a/crates/grida-canvas/examples/taffy_layout_inset.rs b/crates/grida-canvas/examples/taffy_layout_inset.rs new file mode 100644 index 0000000000..6cb7c8e8cd --- /dev/null +++ b/crates/grida-canvas/examples/taffy_layout_inset.rs @@ -0,0 +1,632 @@ +use skia_safe::{surfaces, Color, Font, FontMgr, Paint, Rect as SkRect}; +use taffy::prelude::*; +use taffy::{Overflow, Point}; + +fn main() { + // Create a surface to draw on + let (width, height) = (1400, 2000); + let mut surface = surfaces::raster_n32_premul((width, height)).expect("surface"); + let canvas = surface.canvas(); + canvas.clear(Color::WHITE); + + // Load font for labels + let font_mgr = FontMgr::new(); + let typeface = font_mgr + .match_family_style("Arial", skia_safe::FontStyle::default()) + .unwrap_or_else(|| { + font_mgr + .legacy_make_typeface(None, skia_safe::FontStyle::default()) + .unwrap() + }); + let label_font = Font::new(typeface.clone(), 14.0); + let title_font = Font::new(typeface.clone(), 28.0); + let desc_font = Font::new(typeface.clone(), 12.0); + + let start_x = 50.0; + let mut current_y = 120.0; + let scenario_spacing = 100.0; + + // Draw title + let mut title_paint = Paint::default(); + title_paint.set_anti_alias(true); + title_paint.set_color(Color::BLACK); + canvas.draw_str( + "Inset Layout Model - Over-Constrained Scenarios (Taffy + Skia + Relative)", + skia_safe::Point::new(start_x, 50.0), + &title_font, + &title_paint, + ); + + // Draw subtitle + let mut desc_paint = Paint::default(); + desc_paint.set_anti_alias(true); + desc_paint.set_color(Color::from_argb(160, 0, 0, 0)); + canvas.draw_str( + "Demonstrates what happens when left + width + right is set and parent size changes", + skia_safe::Point::new(start_x, 80.0), + &desc_font, + &desc_paint, + ); + + // Scenario 1: Wide parent (300px) - Plenty of space + // left: 20, right: 20, width: 200 = needs 240px, has 300px + { + let layout = compute_layout( + Some(200.0), + Some(80.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + 300.0, + 150.0, + ); + + let height_used = render_scenario( + canvas, + &layout, + start_x, + current_y, + "Scenario 1: Wide Parent (300px)", + "left: 20px, width: 200px, right: 20px (needs 240px total)", + "✓ Plenty of space - all constraints satisfied", + &label_font, + &desc_font, + 300.0, + 150.0, + ); + + current_y += height_used + scenario_spacing; + } + + // Scenario 2: Exact fit parent (240px) + // left: 20, right: 20, width: 200 = needs 240px, has 240px + { + let layout = compute_layout( + Some(200.0), + Some(80.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + 240.0, + 150.0, + ); + + let height_used = render_scenario( + canvas, + &layout, + start_x, + current_y, + "Scenario 2: Exact Fit Parent (240px)", + "left: 20px, width: 200px, right: 20px (needs 240px total)", + "✓ Exact fit - all constraints satisfied", + &label_font, + &desc_font, + 240.0, + 150.0, + ); + + current_y += height_used + scenario_spacing; + } + + // Scenario 3: Slightly tight parent (220px) + // left: 20, right: 20, width: 200 = needs 240px, has 220px (20px short) + { + let layout = compute_layout( + Some(200.0), + Some(80.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + 220.0, + 150.0, + ); + + let height_used = render_scenario( + canvas, + &layout, + start_x, + current_y, + "Scenario 3: Slightly Tight Parent (220px)", + "left: 20px, width: 200px, right: 20px (needs 240px, short by 20px)", + "⚠ Over-constrained - Taffy ignores 'right' constraint", + &label_font, + &desc_font, + 220.0, + 150.0, + ); + + current_y += height_used + scenario_spacing; + } + + // Scenario 4: Tight parent (200px) + // left: 20, right: 20, width: 200 = needs 240px, has 200px (40px short) + { + let layout = compute_layout( + Some(200.0), + Some(80.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + 200.0, + 150.0, + ); + + let height_used = render_scenario( + canvas, + &layout, + start_x, + current_y, + "Scenario 4: Tight Parent (200px)", + "left: 20px, width: 200px, right: 20px (needs 240px, short by 40px)", + "⚠ Over-constrained - Taffy ignores 'right' constraint", + &label_font, + &desc_font, + 200.0, + 150.0, + ); + + current_y += height_used + scenario_spacing; + } + + // Scenario 5: Very tight parent (150px) + // left: 20, right: 20, width: 200 = needs 240px, has 150px (90px short) + { + let layout = compute_layout( + Some(200.0), + Some(80.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + 150.0, + 150.0, + ); + + let height_used = render_scenario( + canvas, + &layout, + start_x, + current_y, + "Scenario 5: Very Tight Parent (150px)", + "left: 20px, width: 200px, right: 20px (needs 240px, short by 90px)", + "⚠ Over-constrained - Taffy ignores 'right' and child overflows", + &label_font, + &desc_font, + 150.0, + 150.0, + ); + + current_y += height_used + scenario_spacing; + } + + // Scenario 6: Comparing different inset combinations + { + let container_width = 200.0; + let container_height = 350.0; + + // Draw label + let mut label_paint = Paint::default(); + label_paint.set_anti_alias(true); + label_paint.set_color(Color::from_argb(180, 0, 0, 0)); + canvas.draw_str( + "Scenario 6: Comparing Different Inset Patterns (parent: 200px × 350px)", + skia_safe::Point::new(start_x, current_y - 10.0), + &label_font, + &label_paint, + ); + + // Draw container outline + let container_rect = + SkRect::from_xywh(start_x, current_y + 20.0, container_width, container_height); + draw_container_outline(canvas, &container_rect, "Parent Container", &desc_font); + + // 6a: Left + Width (right: auto) + let layout_6a = compute_layout( + Some(160.0), + Some(60.0), + Some(lpa_length(20.0)), + None, + container_width, + container_height, + ); + draw_child_with_label( + canvas, + &layout_6a, + start_x, + current_y + 20.0, + "left: 20, width: 160, right: auto", + &desc_font, + Color::from_argb(200, 59, 130, 246), // blue + ); + + // 6b: Right + Width (left: auto) + let layout_6b = compute_layout( + Some(160.0), + Some(60.0), + None, + Some(lpa_length(20.0)), + container_width, + container_height, + ); + draw_child_with_label( + canvas, + &layout_6b, + start_x, + current_y + 20.0, + "left: auto, width: 160, right: 20", + &desc_font, + Color::from_argb(200, 34, 197, 94), // green + ); + + // 6c: Left + Right (width: auto) + let layout_6c = compute_layout( + None, + Some(60.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + container_width, + container_height, + ); + draw_child_with_label( + canvas, + &layout_6c, + start_x, + current_y + 20.0, + "left: 20, width: auto, right: 20", + &desc_font, + Color::from_argb(200, 168, 85, 247), // purple + ); + + // 6d: Left + Right + Width (over-constrained) + let layout_6d = compute_layout( + Some(160.0), + Some(60.0), + Some(lpa_length(20.0)), + Some(lpa_length(20.0)), + container_width, + container_height, + ); + draw_child_with_label( + canvas, + &layout_6d, + start_x, + current_y + 20.0, + "left: 20, width: 160, right: 20 (ignores right)", + &desc_font, + Color::from_argb(200, 239, 68, 68), // red + ); + } + + // Save the result to a PNG file + let image = surface.image_snapshot(); + let data = image + .encode(None, skia_safe::EncodedImageFormat::PNG, None) + .expect("encode png"); + std::fs::write( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/goldens/taffy_layout_inset.png" + ), + data.as_bytes(), + ) + .unwrap(); + + println!("✓ Generated goldens/taffy_layout_inset.png"); +} + +/// Helper to create LengthPercentageAuto +fn lpa_length(val: f32) -> LengthPercentageAuto { + LengthPercentageAuto::length(val) +} + +/// Compute layout using Taffy directly +fn compute_layout( + width: Option, + height: Option, + left: Option, + right: Option, + parent_width: f32, + parent_height: f32, +) -> Layout { + let mut taffy: TaffyTree<()> = TaffyTree::new(); + + // Create child style with inset positioning + let child_style = Style { + position: Position::Relative, + inset: Rect { + left: left.unwrap_or_else(LengthPercentageAuto::auto), + top: lpa_length(20.0), + right: right.unwrap_or_else(LengthPercentageAuto::auto), + bottom: LengthPercentageAuto::auto(), + }, + size: Size { + width: width.map(Dimension::length).unwrap_or_else(Dimension::auto), + height: height + .map(Dimension::length) + .unwrap_or_else(Dimension::auto), + }, + overflow: Point { + x: Overflow::Hidden, + y: Overflow::Hidden, + }, + ..Default::default() + }; + + let child = taffy.new_leaf(child_style).unwrap(); + + // Create parent container + let parent_style = Style { + size: Size { + width: Dimension::length(parent_width), + height: Dimension::length(parent_height), + }, + ..Default::default() + }; + + let parent = taffy.new_with_children(parent_style, &[child]).unwrap(); + + // Compute layout + taffy + .compute_layout( + parent, + Size { + width: AvailableSpace::Definite(parent_width), + height: AvailableSpace::Definite(parent_height), + }, + ) + .unwrap(); + + // Get child layout + *taffy.layout(child).unwrap() +} + +/// Render a single layout scenario +fn render_scenario( + canvas: &skia_safe::Canvas, + layout: &Layout, + base_x: f32, + base_y: f32, + title: &str, + subtitle: &str, + result: &str, + label_font: &Font, + desc_font: &Font, + container_width: f32, + container_height: f32, +) -> f32 { + // Draw title + let mut title_paint = Paint::default(); + title_paint.set_anti_alias(true); + title_paint.set_color(Color::from_argb(180, 0, 0, 0)); + canvas.draw_str( + title, + skia_safe::Point::new(base_x, base_y - 10.0), + label_font, + &title_paint, + ); + + // Draw subtitle (constraint info) + let mut subtitle_paint = Paint::default(); + subtitle_paint.set_anti_alias(true); + subtitle_paint.set_color(Color::from_argb(130, 0, 0, 0)); + canvas.draw_str( + subtitle, + skia_safe::Point::new(base_x, base_y + 10.0), + desc_font, + &subtitle_paint, + ); + + // Draw result + let result_color = if result.starts_with('✓') { + Color::from_argb(180, 0, 150, 0) + } else { + Color::from_argb(180, 200, 100, 0) + }; + let mut result_paint = Paint::default(); + result_paint.set_anti_alias(true); + result_paint.set_color(result_color); + canvas.draw_str( + result, + skia_safe::Point::new(base_x, base_y + 28.0), + desc_font, + &result_paint, + ); + + // Draw container outline + let container_rect = + SkRect::from_xywh(base_x, base_y + 45.0, container_width, container_height); + draw_container_outline(canvas, &container_rect, "Parent Container", desc_font); + + // Draw child + let child_rect = SkRect::from_xywh( + base_x + layout.location.x, + base_y + 45.0 + layout.location.y, + layout.size.width, + layout.size.height, + ); + + // Fill child + let mut child_paint = Paint::default(); + child_paint.set_anti_alias(true); + child_paint.set_color(Color::from_argb(200, 59, 130, 246)); // blue + + let child_rrect = skia_safe::RRect::new_rect_radii( + child_rect, + &[ + skia_safe::Point::new(8.0, 8.0), + skia_safe::Point::new(8.0, 8.0), + skia_safe::Point::new(8.0, 8.0), + skia_safe::Point::new(8.0, 8.0), + ], + ); + canvas.draw_rrect(&child_rrect, &child_paint); + + // Border child + let mut border_paint = Paint::default(); + border_paint.set_anti_alias(true); + border_paint.set_color(Color::from_argb(200, 30, 64, 175)); // darker blue + border_paint.set_style(skia_safe::PaintStyle::Stroke); + border_paint.set_stroke_width(2.0); + canvas.draw_rrect(&child_rrect, &border_paint); + + // Draw child dimensions + let mut dim_paint = Paint::default(); + dim_paint.set_anti_alias(true); + dim_paint.set_color(Color::WHITE); + let dim_text = format!("{}×{}", layout.size.width as i32, layout.size.height as i32); + canvas.draw_str( + &dim_text, + skia_safe::Point::new( + base_x + layout.location.x + layout.size.width / 2.0 - 20.0, + base_y + 45.0 + layout.location.y + layout.size.height / 2.0 + 5.0, + ), + desc_font, + &dim_paint, + ); + + // Draw position annotations + draw_annotations( + canvas, + layout, + base_x, + base_y + 45.0, + container_width, + container_height, + desc_font, + ); + + // Return total height used + 60.0 + container_height +} + +/// Draw container outline with label +fn draw_container_outline(canvas: &skia_safe::Canvas, rect: &SkRect, label: &str, font: &Font) { + // Draw outline + let mut outline_paint = Paint::default(); + outline_paint.set_anti_alias(true); + outline_paint.set_color(Color::from_argb(60, 0, 0, 0)); + outline_paint.set_style(skia_safe::PaintStyle::Stroke); + outline_paint.set_stroke_width(2.0); + outline_paint.set_path_effect(skia_safe::dash_path_effect::new(&[8.0, 4.0], 0.0)); + canvas.draw_rect(rect, &outline_paint); + + // Draw label + let mut label_paint = Paint::default(); + label_paint.set_anti_alias(true); + label_paint.set_color(Color::from_argb(100, 0, 0, 0)); + canvas.draw_str( + label, + skia_safe::Point::new(rect.left + 5.0, rect.top + 15.0), + font, + &label_paint, + ); + + // Draw dimensions + let dim_text = format!("{}×{}", rect.width() as i32, rect.height() as i32); + canvas.draw_str( + &dim_text, + skia_safe::Point::new(rect.left + 5.0, rect.bottom() - 5.0), + font, + &label_paint, + ); +} + +/// Draw measurement annotations +fn draw_annotations( + canvas: &skia_safe::Canvas, + layout: &Layout, + base_x: f32, + base_y: f32, + container_width: f32, + _container_height: f32, + font: &Font, +) { + let mut anno_paint = Paint::default(); + anno_paint.set_anti_alias(true); + anno_paint.set_color(Color::from_argb(150, 200, 0, 0)); + + // Left spacing + if layout.location.x > 0.0 { + let left_line_y = base_y + layout.location.y + layout.size.height + 15.0; + canvas.draw_line( + skia_safe::Point::new(base_x, left_line_y), + skia_safe::Point::new(base_x + layout.location.x, left_line_y), + &anno_paint, + ); + canvas.draw_str( + &format!("left: {}", layout.location.x as i32), + skia_safe::Point::new(base_x + layout.location.x / 2.0 - 15.0, left_line_y - 5.0), + font, + &anno_paint, + ); + } + + // Right spacing + let right_space = container_width - (layout.location.x + layout.size.width); + if right_space > 0.5 { + let right_line_y = base_y + layout.location.y + layout.size.height + 15.0; + let right_start_x = base_x + layout.location.x + layout.size.width; + let right_end_x = base_x + container_width; + canvas.draw_line( + skia_safe::Point::new(right_start_x, right_line_y), + skia_safe::Point::new(right_end_x, right_line_y), + &anno_paint, + ); + canvas.draw_str( + &format!("right: {}", right_space as i32), + skia_safe::Point::new(right_start_x + right_space / 2.0 - 18.0, right_line_y - 5.0), + font, + &anno_paint, + ); + } +} + +/// Draw child with label for comparison scenarios +fn draw_child_with_label( + canvas: &skia_safe::Canvas, + layout: &Layout, + base_x: f32, + base_y: f32, + label: &str, + font: &Font, + color: Color, +) { + let child_rect = SkRect::from_xywh( + base_x + layout.location.x, + base_y + layout.location.y, + layout.size.width, + layout.size.height, + ); + + // Fill + let mut child_paint = Paint::default(); + child_paint.set_anti_alias(true); + child_paint.set_color(color); + + let child_rrect = skia_safe::RRect::new_rect_radii( + child_rect, + &[ + skia_safe::Point::new(6.0, 6.0), + skia_safe::Point::new(6.0, 6.0), + skia_safe::Point::new(6.0, 6.0), + skia_safe::Point::new(6.0, 6.0), + ], + ); + canvas.draw_rrect(&child_rrect, &child_paint); + + // Border + let mut border_paint = Paint::default(); + border_paint.set_anti_alias(true); + border_paint.set_color(color.with_a(255)); + border_paint.set_style(skia_safe::PaintStyle::Stroke); + border_paint.set_stroke_width(2.0); + canvas.draw_rrect(&child_rrect, &border_paint); + + // Label + let mut label_paint = Paint::default(); + label_paint.set_anti_alias(true); + label_paint.set_color(Color::WHITE); + canvas.draw_str( + label, + skia_safe::Point::new( + base_x + layout.location.x + 5.0, + base_y + layout.location.y + layout.size.height / 2.0 + 4.0, + ), + font, + &label_paint, + ); +} diff --git a/crates/grida-canvas/examples/tool_io_grida.rs b/crates/grida-canvas/examples/tool_io_grida.rs new file mode 100644 index 0000000000..55d080c1d8 --- /dev/null +++ b/crates/grida-canvas/examples/tool_io_grida.rs @@ -0,0 +1,96 @@ +//! Grida File Validation Tool +//! +//! This tool validates .grida files by parsing them and reporting success or failure. +//! It's useful for debugging file format issues and verifying that legacy files can be parsed correctly. +//! +//! ## Usage +//! +//! ```bash +//! cargo run --example tool_io_grida +//! ``` +//! +//! ## Example +//! +//! ```bash +//! cargo run --example tool_io_grida ../../editor/public/examples/canvas/instagram-post-01.grida +//! ``` +//! +//! ## Output +//! +//! On success, the tool prints: +//! - Total number of nodes parsed +//! - Scene references +//! - Entry scene ID +//! - Breakdown of node types (group, container, text, etc.) +//! +//! On failure, the tool prints: +//! - Error message describing what failed to parse +//! +//! ## Exit Codes +//! +//! - `0` - Success +//! - `1` - Parse error or file read error + +use cg::io::io_grida::parse; +use std::env; +use std::fs; + +fn main() { + let args: Vec = env::args().collect(); + + if args.len() < 2 { + eprintln!("Usage: cargo run --example tool_io_grida "); + std::process::exit(1); + } + + let file_path = &args[1]; + + println!("Parsing file: {}", file_path); + + match fs::read_to_string(file_path) { + Ok(content) => match parse(&content) { + Ok(file) => { + println!("✓ Successfully parsed {} nodes", file.document.nodes.len()); + println!(" Scenes: {:?}", file.document.scenes_ref); + println!(" Entry scene: {:?}", file.document.entry_scene_id); + + // Count node types + let mut node_types: std::collections::HashMap = + std::collections::HashMap::new(); + for node in file.document.nodes.values() { + let type_name = match node { + cg::io::io_grida::JSONNode::Group(_) => "group", + cg::io::io_grida::JSONNode::Container(_) => "container", + cg::io::io_grida::JSONNode::SVGPath(_) => "svgpath", + cg::io::io_grida::JSONNode::Path(_) => "vector", + cg::io::io_grida::JSONNode::Ellipse(_) => "ellipse", + cg::io::io_grida::JSONNode::Rectangle(_) => "rectangle", + cg::io::io_grida::JSONNode::RegularPolygon(_) => "polygon", + cg::io::io_grida::JSONNode::RegularStarPolygon(_) => "star", + cg::io::io_grida::JSONNode::Line(_) => "line", + cg::io::io_grida::JSONNode::Text(_) => "text", + cg::io::io_grida::JSONNode::BooleanOperation(_) => "boolean", + cg::io::io_grida::JSONNode::Image(_) => "image", + cg::io::io_grida::JSONNode::Scene(_) => "scene", + cg::io::io_grida::JSONNode::Unknown(_) => "unknown", + }; + *node_types.entry(type_name.to_string()).or_insert(0) += 1; + } + if !node_types.is_empty() { + println!(" Node types:"); + for (name, count) in node_types.iter() { + println!(" {}: {}", name, count); + } + } + } + Err(e) => { + eprintln!("✗ Failed to parse: {}", e); + std::process::exit(1); + } + }, + Err(e) => { + eprintln!("✗ Failed to read file: {}", e); + std::process::exit(1); + } + } +} diff --git a/crates/grida-canvas/examples/wd_windowed_mode.rs b/crates/grida-canvas/examples/wd_windowed_mode.rs new file mode 100644 index 0000000000..37a4bf8e18 --- /dev/null +++ b/crates/grida-canvas/examples/wd_windowed_mode.rs @@ -0,0 +1,126 @@ +use cg::cg::types::*; +use cg::node::factory::NodeFactory; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::*; +use cg::window; + +fn create_flex_demo_scene() -> Scene { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // ROOT 1: ICB (resizes with window) + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_wrap = LayoutWrap::Wrap; + icb.layout_gap = LayoutGap { + main_axis_gap: 20.0, + cross_axis_gap: 20.0, + }; + icb.padding = EdgeInsets::all(20.0); + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + // Create colored boxes for ICB + let colors = [ + ("Red", CGColor(239, 68, 68, 255)), + ("Blue", CGColor(59, 130, 246, 255)), + ("Green", CGColor(34, 197, 94, 255)), + ("Yellow", CGColor(234, 179, 8, 255)), + ("Purple", CGColor(168, 85, 247, 255)), + ]; + + for (_name, color) in colors.iter() { + let mut box_node = nf.create_container_node(); + box_node.layout_dimensions.width = Some(100.0); + box_node.layout_dimensions.height = Some(100.0); + // Participate in parent's flex layout + box_node.layout_child = Some(LayoutChildStyle { + layout_grow: 0.0, + layout_positioning: LayoutPositioning::Auto, + }); + box_node.fills = Paints::new([Paint::Solid(SolidPaint { + color: *color, + blend_mode: BlendMode::default(), + active: true, + })]); + box_node.corner_radius = RectangularCornerRadius::all(Radius { rx: 8.0, ry: 8.0 }); + graph.append_child(Node::Container(box_node), Parent::NodeId(icb_id)); + } + + // ROOT 2: Fixed Container (doesn't resize with window) - positioned above ICB + let mut fixed_container = nf.create_container_node(); + fixed_container.layout_dimensions.width = Some(250.0); // Fixed width + fixed_container.layout_dimensions.height = Some(150.0); // Fixed height + fixed_container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Vertical, + layout_gap: Some(LayoutGap::uniform(10.0)), + layout_padding: Some(EdgeInsets::all(15.0)), + ..Default::default() + }; + fixed_container.fills = Paints::new([Paint::Solid(SolidPaint { + color: CGColor(255, 200, 200, 255), // Light red background + blend_mode: BlendMode::default(), + active: true, + })]); + fixed_container.position = CGPoint::new(100.0, -50.0).into(); // Position it at (100, 50) - above ICB area + let fixed_id = graph.append_child(Node::Container(fixed_container), Parent::Root); + + // Create smaller boxes for fixed container + let fixed_colors = [ + ("Orange", CGColor(255, 165, 0, 255)), + ("Pink", CGColor(255, 192, 203, 255)), + ("Cyan", CGColor(0, 255, 255, 255)), + ]; + + for (_name, color) in fixed_colors.iter() { + let mut box_node = nf.create_container_node(); + box_node.layout_dimensions.width = Some(80.0); + box_node.layout_dimensions.height = Some(50.0); + box_node.layout_child = Some(LayoutChildStyle { + layout_grow: 0.0, + layout_positioning: LayoutPositioning::Auto, + }); + box_node.fills = Paints::new([Paint::Solid(SolidPaint { + color: *color, + blend_mode: BlendMode::default(), + active: true, + })]); + box_node.corner_radius = RectangularCornerRadius::all(Radius { rx: 4.0, ry: 4.0 }); + graph.append_child(Node::Container(box_node), Parent::NodeId(fixed_id)); + } + + Scene { + name: "Dual Root Layout Demo".to_string(), + graph, + background_color: Some(CGColor(255, 255, 255, 255)), + } +} + +#[tokio::main] +async fn main() { + println!("Dual Root Layout Demo"); + println!("===================="); + println!(); + println!("✓ ROOT 1: ICB (resizes with window)"); + println!(" - Horizontal flex with 5 colored boxes"); + println!(" - 20px gap, 20px padding"); + println!(" - Items wrap when window is resized"); + println!(); + println!("✓ ROOT 2: Fixed Container (doesn't resize)"); + println!(" - Fixed 250x150 size at position (100, 50)"); + println!(" - Vertical flex with 3 smaller boxes"); + println!(" - 10px gap, 15px padding"); + println!(); + println!("Try resizing the window to see the difference!"); + println!("- ICB boxes will reflow and wrap"); + println!("- Fixed container stays the same size and position"); + + let scene = create_flex_demo_scene(); + + window::run_demo_window_with(scene, |renderer, _tx, _font_tx, _proxy| { + // Disable tile caching for smoother resize + renderer.set_cache_tile(false); + }) + .await; +} diff --git a/crates/grida-canvas/goldens/layout_flex_alignment.png b/crates/grida-canvas/goldens/layout_flex_alignment.png index 2cf456505b..557a558662 100644 Binary files a/crates/grida-canvas/goldens/layout_flex_alignment.png and b/crates/grida-canvas/goldens/layout_flex_alignment.png differ diff --git a/crates/grida-canvas/goldens/layout_flex_padding.png b/crates/grida-canvas/goldens/layout_flex_padding.png index 455bc79150..f663fa4c7e 100644 Binary files a/crates/grida-canvas/goldens/layout_flex_padding.png and b/crates/grida-canvas/goldens/layout_flex_padding.png differ diff --git a/crates/grida-canvas/goldens/taffy_layout_inset.png b/crates/grida-canvas/goldens/taffy_layout_inset.png new file mode 100644 index 0000000000..a1d555a18a Binary files /dev/null and b/crates/grida-canvas/goldens/taffy_layout_inset.png differ diff --git a/crates/grida-canvas/src/cache/geometry.rs b/crates/grida-canvas/src/cache/geometry.rs index 1d5fece801..17a3ccba24 100644 --- a/crates/grida-canvas/src/cache/geometry.rs +++ b/crates/grida-canvas/src/cache/geometry.rs @@ -1,9 +1,19 @@ +//! Geometry cache - Transform and bounds resolution +//! +//! ## Pipeline Guarantees +//! +//! This module guarantees: +//! - Every node in scene graph has a GeometryEntry +//! - All transforms are fully resolved (no None/fallbacks) +//! - All bounds include layout-computed dimensions for V2 nodes +//! - Consumes LayoutResult as immutable input from LayoutEngine +//! - Missing layout for Inset nodes is a PANIC (LayoutEngine bug) +//! - Missing geometry entry when accessed is a PANIC (GeometryCache bug) + use crate::cache::paragraph::ParagraphCache; use crate::cg::types::*; use crate::node::scene_graph::SceneGraph; -use crate::node::schema::{ - IntrinsicSizeNode, LayerEffects, Node, NodeGeometryMixin, NodeId, Scene, -}; +use crate::node::schema::{LayerEffects, Node, NodeGeometryMixin, NodeId, NodeRectMixin, Scene}; use crate::runtime::font_repository::FontRepository; use math2::rect; use math2::rect::Rectangle; @@ -35,6 +45,11 @@ pub struct GeometryEntry { pub dirty_bounds: bool, } +/// Context passed during geometry building +struct GeometryBuildContext { + viewport_size: crate::node::schema::Size, +} + #[derive(Debug, Clone)] pub struct GeometryCache { entries: HashMap, @@ -55,9 +70,25 @@ impl GeometryCache { scene: &Scene, paragraph_cache: &mut ParagraphCache, fonts: &FontRepository, + ) -> Self { + let default_viewport = crate::node::schema::Size { + width: 1920.0, + height: 1080.0, + }; + Self::from_scene_with_layout(scene, paragraph_cache, fonts, None, default_viewport) + } + + pub fn from_scene_with_layout( + scene: &Scene, + paragraph_cache: &mut ParagraphCache, + fonts: &FontRepository, + layout_result: Option<&crate::layout::cache::LayoutResult>, + viewport_size: crate::node::schema::Size, ) -> Self { let mut cache = Self::new(); let root_world = AffineTransform::identity(); + let context = GeometryBuildContext { viewport_size }; + for child in scene.graph.roots() { Self::build_recursive( &child, @@ -67,6 +98,8 @@ impl GeometryCache { &mut cache, paragraph_cache, fonts, + layout_result, + &context, ); } cache @@ -80,6 +113,8 @@ impl GeometryCache { cache: &mut GeometryCache, paragraph_cache: &mut ParagraphCache, fonts: &FontRepository, + layout_result: Option<&crate::layout::cache::LayoutResult>, + context: &GeometryBuildContext, ) -> Rectangle { let node = graph .get_node(id) @@ -100,6 +135,8 @@ impl GeometryCache { cache, paragraph_cache, fonts, + layout_result, + context, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -148,6 +185,57 @@ impl GeometryCache { cache.entries.insert(id.clone(), entry.clone()); entry.absolute_bounding_box } + Node::InitialContainer(_n) => { + // ICB fills viewport - size from context + // Layout was already computed by LayoutEngine + let size = context.viewport_size; + + let local_transform = AffineTransform::identity(); + let world_transform = parent_world.compose(&local_transform); + + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width: size.width, + height: size.height, + }; + + // Build children geometries (may use computed layouts from LayoutEngine) + let mut union_world_bounds = transform_rect(&local_bounds, &world_transform); + + if let Some(children) = graph.get_children(id) { + for child_id in children { + let child_bounds = Self::build_recursive( + child_id, + graph, + &world_transform, + Some(id.clone()), + cache, + paragraph_cache, + fonts, + layout_result, + context, + ); + union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); + } + } + + let render_bounds = union_world_bounds; // ICB has no effects + + let entry = GeometryEntry { + transform: local_transform, + absolute_transform: world_transform, + bounding_box: local_bounds, + absolute_bounding_box: union_world_bounds, + absolute_render_bounds: render_bounds, + parent: parent_id, + dirty_transform: false, + dirty_bounds: false, + }; + + cache.entries.insert(id.clone(), entry); + union_world_bounds + } Node::BooleanOperation(n) => { let world_transform = parent_world.compose(&n.transform.unwrap_or_default()); let mut union_bounds: Option = None; @@ -161,6 +249,8 @@ impl GeometryCache { cache, paragraph_cache, fonts, + layout_result, + context, ); union_bounds = match union_bounds { Some(b) => Some(rect::union(&[b, child_bounds])), @@ -213,9 +303,32 @@ impl GeometryCache { entry.absolute_bounding_box } Node::Container(n) => { - let local_transform = n.transform; + // All containers use computed layout (roots have position corrected by LayoutEngine) + let (x, y, width, height) = if let Some(result) = layout_result { + // Layout engine is active: use computed layout + let computed = result + .get(id) + .expect("Container must have layout result when layout engine is used"); + (computed.x, computed.y, computed.width, computed.height) + } else { + // No layout engine: use schema directly (backward compatibility) + ( + n.position.x().unwrap_or(0.0), + n.position.y().unwrap_or(0.0), + n.layout_dimensions.width.unwrap_or(0.0), + n.layout_dimensions.height.unwrap_or(0.0), + ) + }; + let local_transform = AffineTransform::new(x, y, n.rotation); + + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width, + height, + }; + let world_transform = parent_world.compose(&local_transform); - let local_bounds = n.rect(); let world_bounds = transform_rect(&local_bounds, &world_transform); let mut union_world_bounds = world_bounds; let render_bounds = compute_render_bounds_from_style( @@ -239,6 +352,8 @@ impl GeometryCache { cache, paragraph_cache, fonts, + layout_result, + context, ); union_world_bounds = rect::union(&[union_world_bounds, child_bounds]); } @@ -305,25 +420,57 @@ impl GeometryCache { intrinsic_bounds } _ => { - let intrinsic_node = Box::new(match node { - Node::SVGPath(n) => IntrinsicSizeNode::SVGPath(n.clone()), - Node::Vector(n) => IntrinsicSizeNode::Vector(n.clone()), - Node::Rectangle(n) => IntrinsicSizeNode::Rectangle(n.clone()), - Node::Ellipse(n) => IntrinsicSizeNode::Ellipse(n.clone()), - Node::Polygon(n) => IntrinsicSizeNode::Polygon(n.clone()), - Node::RegularPolygon(n) => IntrinsicSizeNode::RegularPolygon(n.clone()), - Node::RegularStarPolygon(n) => IntrinsicSizeNode::RegularStarPolygon(n.clone()), - Node::Line(n) => IntrinsicSizeNode::Line(n.clone()), - Node::Image(n) => IntrinsicSizeNode::Image(n.clone()), - Node::Container(n) => IntrinsicSizeNode::Container(n.clone()), - Node::Error(n) => IntrinsicSizeNode::Error(n.clone()), - Node::TextSpan(_) | Node::Group(_) | Node::BooleanOperation(_) => { - unreachable!() + // Leaf nodes - check layout result first, fallback to schema transform + let (rec_transform, schema_width, schema_height) = match node { + Node::Rectangle(n) => (n.transform, n.size.width, n.size.height), + Node::Ellipse(n) => (n.transform, n.size.width, n.size.height), + Node::Image(n) => (n.transform, n.size.width, n.size.height), + Node::RegularPolygon(n) => (n.transform, n.size.width, n.size.height), + Node::RegularStarPolygon(n) => (n.transform, n.size.width, n.size.height), + Node::Line(n) => (n.transform, n.size.width, 0.0), + Node::Polygon(n) => { + let rect = n.rect(); + (n.transform, rect.width, rect.height) } - }); - let intrinsic = intrinsic_node.as_ref(); + Node::SVGPath(n) => { + let rect = n.rect(); + (n.transform, rect.width, rect.height) + } + Node::Vector(n) => { + let rect = n.network.bounds(); + (n.transform, rect.width, rect.height) + } + Node::Error(n) => (n.transform, n.size.width, n.size.height), + // V2/special nodes handled above + _ => unreachable!("Has dedicated case above"), + }; + + // Position and size resolution: + // - If layout result exists: Use computed position/size (participating in flex layout) + // - If no layout result: Use schema transform (no layout engine, or non-participating nodes) + let (x, y, width, height) = + if let Some(result) = layout_result.and_then(|r| r.get(id)) { + // Has computed layout: use layout position and size + (result.x, result.y, result.width, result.height) + } else { + // No layout: use schema transform + ( + rec_transform.x(), + rec_transform.y(), + schema_width, + schema_height, + ) + }; + + let local_transform = AffineTransform::new(x, y, rec_transform.rotation()); + + let local_bounds = Rectangle { + x: 0.0, + y: 0.0, + width, + height, + }; - let (local_transform, local_bounds) = node_geometry(intrinsic); let world_transform = parent_world.compose(&local_transform); let world_bounds = transform_rect(&local_bounds, &world_transform); let render_bounds = compute_render_bounds(node, world_bounds); @@ -345,6 +492,10 @@ impl GeometryCache { } } + pub fn get_transform(&self, id: &NodeId) -> Option { + self.entries.get(id).map(|e| e.transform) + } + pub fn get_world_transform(&self, id: &NodeId) -> Option { self.entries.get(id).map(|e| e.absolute_transform) } @@ -384,81 +535,10 @@ impl GeometryCache { } } -fn node_geometry(node: &IntrinsicSizeNode) -> (AffineTransform, Rectangle) { - match node { - IntrinsicSizeNode::Error(n) => (n.transform, n.rect()), - IntrinsicSizeNode::Container(n) => (n.transform, n.rect()), - IntrinsicSizeNode::Rectangle(n) => (n.transform, n.rect()), - IntrinsicSizeNode::Ellipse(n) => (n.transform, n.rect()), - IntrinsicSizeNode::Polygon(n) => (n.transform, polygon_bounds(&n.points)), - IntrinsicSizeNode::RegularPolygon(n) => (n.transform, n.rect()), - IntrinsicSizeNode::RegularStarPolygon(n) => (n.transform, n.rect()), - IntrinsicSizeNode::Line(n) => ( - n.transform, - Rectangle { - x: 0.0, - y: 0.0, - width: n.size.width, - height: 0.0, - }, - ), - IntrinsicSizeNode::SVGPath(n) => (n.transform, path_bounds(&n.data)), - IntrinsicSizeNode::Vector(n) => (n.transform, n.network.bounds()), - IntrinsicSizeNode::Image(n) => (n.transform, n.rect()), - } -} - fn transform_rect(rect: &Rectangle, t: &AffineTransform) -> Rectangle { rect::transform(*rect, t) } -fn polygon_bounds(points: &[CGPoint]) -> Rectangle { - let mut min_x = f32::INFINITY; - let mut min_y = f32::INFINITY; - let mut max_x = f32::NEG_INFINITY; - let mut max_y = f32::NEG_INFINITY; - for p in points { - min_x = min_x.min(p.x); - min_y = min_y.min(p.y); - max_x = max_x.max(p.x); - max_y = max_y.max(p.y); - } - if points.is_empty() { - Rectangle { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, - } - } else { - Rectangle { - x: min_x, - y: min_y, - width: max_x - min_x, - height: max_y - min_y, - } - } -} - -fn path_bounds(data: &str) -> Rectangle { - if let Some(path) = skia_safe::path::Path::from_svg(data) { - let b = path.compute_tight_bounds(); - Rectangle { - x: b.left(), - y: b.top(), - width: b.width(), - height: b.height(), - } - } else { - Rectangle { - x: 0.0, - y: 0.0, - width: 0.0, - height: 0.0, - } - } -} - fn inflate_rect(rect: Rectangle, delta: f32) -> Rectangle { if delta <= 0.0 { return rect; @@ -622,6 +702,6 @@ fn compute_render_bounds(node: &Node, world_bounds: Rectangle) -> Rectangle { &n.effects, ), Node::Error(_) => world_bounds, - Node::Group(_) | Node::BooleanOperation(_) => world_bounds, + Node::Group(_) | Node::BooleanOperation(_) | Node::InitialContainer(_) => world_bounds, } } diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs index e9fbea73d8..cdc43566c7 100644 --- a/crates/grida-canvas/src/cache/scene.rs +++ b/crates/grida-canvas/src/cache/scene.rs @@ -64,6 +64,23 @@ impl SceneCache { ); } + /// Rebuild geometry cache with layout results and viewport context + pub fn update_geometry_with_layout( + &mut self, + scene: &Scene, + fonts: &FontRepository, + layout_result: &crate::layout::cache::LayoutResult, + viewport_size: crate::node::schema::Size, + ) { + self.geometry = GeometryCache::from_scene_with_layout( + scene, + &mut self.paragraph.borrow_mut(), + fonts, + Some(layout_result), + viewport_size, + ); + } + pub fn update_layers(&mut self, scene: &Scene) { self.layers = LayerList::from_scene(scene, self); self.layers diff --git a/crates/grida-canvas/src/cg/types.rs b/crates/grida-canvas/src/cg/types.rs index 324ac79a4c..b3805bab9f 100644 --- a/crates/grida-canvas/src/cg/types.rs +++ b/crates/grida-canvas/src/cg/types.rs @@ -13,6 +13,10 @@ pub struct CGPoint { } impl CGPoint { + pub fn zero() -> Self { + Self { x: 0.0, y: 0.0 } + } + pub fn new(x: f32, y: f32) -> Self { Self { x, y } } @@ -35,6 +39,12 @@ impl CGPoint { } } +impl Default for CGPoint { + fn default() -> Self { + Self::zero() + } +} + impl Into for CGPoint { fn into(self) -> skia_safe::Point { skia_safe::Point::new(self.x, self.y) @@ -342,6 +352,12 @@ pub enum FillRule { EvenOdd, } +impl Default for FillRule { + fn default() -> Self { + FillRule::NonZero + } +} + /// Stroke alignment. /// /// - [Flutter](https://api.flutter.dev/flutter/painting/BorderSide/strokeAlign.html) @@ -546,6 +562,102 @@ impl Default for LayoutMode { } } +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub enum LayoutPositioning { + Auto, + Absolute, +} + +impl Default for LayoutPositioning { + fn default() -> Self { + LayoutPositioning::Auto + } +} + +/// Constraint positioning specifier for constraints layout. +/// +/// Defines how a node is anchored along an axis (horizontal or vertical) relative to its parent container. +/// +/// ## Horizontal Positioning +/// - [`Start`](LayoutConstraintAnchor::Start): Anchored to the left edge +/// - [`End`](LayoutConstraintAnchor::End): Anchored to the right edge +/// - [`Center`](LayoutConstraintAnchor::Center): Centered horizontally +/// - [`Stretch`](LayoutConstraintAnchor::Stretch): Anchored to both left and right edges +/// +/// ## Vertical Positioning +/// - [`Start`](LayoutConstraintAnchor::Start): Anchored to the top edge +/// - [`End`](LayoutConstraintAnchor::End): Anchored to the bottom edge +/// - [`Center`](LayoutConstraintAnchor::Center): Centered vertically +/// - [`Stretch`](LayoutConstraintAnchor::Stretch): Anchored to both top and bottom edges +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub enum LayoutConstraintAnchor { + /// Start anchor (left for horizontal, top for vertical) + Start, + /// End anchor (right for horizontal, bottom for vertical) + End, + /// Center anchor (centered along the axis) + Center, + /// Stretch anchor (anchored to both edges of the axis) + Stretch, +} + +impl Default for LayoutConstraintAnchor { + fn default() -> Self { + LayoutConstraintAnchor::Start + } +} + +/// Defines how a node is constrained relative to its parent container. +/// +/// Specifies the constraint positioning behavior for both horizontal and vertical axes, +/// determining how the node will be resized and positioned when its parent's size changes. +/// +/// ## Fields +/// - `x`: Horizontal constraint anchor (left, right, center, or stretch) +/// - `y`: Vertical constraint anchor (top, bottom, center, or stretch) +/// +/// ## Examples +/// +/// Fixed to top-left corner: +/// ```ignore +/// LayoutConstraints { +/// x: LayoutConstraintAnchor::Start, // left +/// y: LayoutConstraintAnchor::Start, // top +/// } +/// ``` +/// +/// Centered in parent: +/// ```ignore +/// LayoutConstraints { +/// x: LayoutConstraintAnchor::Center, // horizontally centered +/// y: LayoutConstraintAnchor::Center, // vertically centered +/// } +/// ``` +/// +/// Stretched to fill parent: +/// ```ignore +/// LayoutConstraints { +/// x: LayoutConstraintAnchor::Stretch, // left and right edges +/// y: LayoutConstraintAnchor::Stretch, // top and bottom edges +/// } +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Deserialize)] +pub struct LayoutConstraints { + /// Horizontal constraint anchor determining how the node is positioned/resized along the x-axis + pub x: LayoutConstraintAnchor, + /// Vertical constraint anchor determining how the node is positioned/resized along the y-axis + pub y: LayoutConstraintAnchor, +} + +impl Default for LayoutConstraints { + fn default() -> Self { + LayoutConstraints { + x: LayoutConstraintAnchor::Start, + y: LayoutConstraintAnchor::Start, + } + } +} + /// Defines whether flex items are forced into a single line or can wrap onto multiple lines. /// /// `LayoutWrap` controls the wrapping behavior of flex items within a flex container. diff --git a/crates/grida-canvas/src/fonts/embedded/mod.rs b/crates/grida-canvas/src/fonts/embedded/mod.rs index 9d87229509..d061a4ffd4 100644 --- a/crates/grida-canvas/src/fonts/embedded/mod.rs +++ b/crates/grida-canvas/src/fonts/embedded/mod.rs @@ -8,7 +8,7 @@ thread_local! { pub static TYPEFACE_GEISTMONO: Typeface = typeface(geistmono::BYTES); } -#[deprecated(note = "will be removed")] +// #[deprecated(note = "will be removed")] pub fn typeface(bytes: &[u8]) -> Typeface { // FIXME: should not make fntmgr each time let font_mgr = FontMgr::new(); diff --git a/crates/grida-canvas/src/io/io_css.rs b/crates/grida-canvas/src/io/io_css.rs index c1f8520b56..5d9bb54c39 100644 --- a/crates/grida-canvas/src/io/io_css.rs +++ b/crates/grida-canvas/src/io/io_css.rs @@ -23,9 +23,33 @@ //! // MyNode { width: CSSDimension::Auto, height: CSSDimension::LengthPX(100.0) } //! ``` +use crate::cg::types::*; use serde::Deserialize; use serde_json::Value; +#[derive(Debug, PartialEq, Deserialize)] +pub enum CSSPosition { + #[serde(rename = "relative")] + Relative, + #[serde(rename = "absolute")] + Absolute, +} + +impl Default for CSSPosition { + fn default() -> Self { + CSSPosition::Relative + } +} + +impl From for LayoutPositioning { + fn from(position: CSSPosition) -> Self { + match position { + CSSPosition::Relative => LayoutPositioning::Auto, + CSSPosition::Absolute => LayoutPositioning::Absolute, + } + } +} + /// CSS-style dimension value that can be either auto or a fixed length in pixels. #[derive(Debug, PartialEq)] pub enum CSSDimension { diff --git a/crates/grida-canvas/src/io/io_figma.rs b/crates/grida-canvas/src/io/io_figma.rs index afb8ddb168..df46f84cbb 100644 --- a/crates/grida-canvas/src/io/io_figma.rs +++ b/crates/grida-canvas/src/io/io_figma.rs @@ -9,6 +9,7 @@ use figma_api::models::type_style::{ TextDecoration as FigmaTextDecoration, }; use figma_api::models::vector::Vector; +use figma_api::models::vector_node::LayoutPositioning as FigmaLayoutPositioning; use figma_api::models::{ BooleanOperationNode as FigmaBooleanOperationNode, CanvasNode, ComponentNode, ComponentSetNode, DocumentNode, Effect, FrameNode, GroupNode, InstanceNode, LineNode as FigmaLineNode, @@ -208,6 +209,128 @@ impl From<&FigmaTextDecoration> for TextDecorationLine { } } +impl From<&FigmaLayoutPositioning> for LayoutPositioning { + fn from(position: &FigmaLayoutPositioning) -> Self { + match position { + FigmaLayoutPositioning::Auto => LayoutPositioning::Auto, + FigmaLayoutPositioning::Absolute => LayoutPositioning::Absolute, + } + } +} + +// Additional From implementations for LayoutPositioning from different Figma node types +impl From<&figma_api::models::component_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::component_node::LayoutPositioning) -> Self { + match position { + figma_api::models::component_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::component_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::instance_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::instance_node::LayoutPositioning) -> Self { + match position { + figma_api::models::instance_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::instance_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::section_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::section_node::LayoutPositioning) -> Self { + match position { + figma_api::models::section_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::section_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::frame_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::frame_node::LayoutPositioning) -> Self { + match position { + figma_api::models::frame_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::frame_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::text_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::text_node::LayoutPositioning) -> Self { + match position { + figma_api::models::text_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::text_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::star_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::star_node::LayoutPositioning) -> Self { + match position { + figma_api::models::star_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::star_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::line_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::line_node::LayoutPositioning) -> Self { + match position { + figma_api::models::line_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::line_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::ellipse_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::ellipse_node::LayoutPositioning) -> Self { + match position { + figma_api::models::ellipse_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::ellipse_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::regular_polygon_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::regular_polygon_node::LayoutPositioning) -> Self { + match position { + figma_api::models::regular_polygon_node::LayoutPositioning::Auto => { + LayoutPositioning::Auto + } + figma_api::models::regular_polygon_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + +impl From<&figma_api::models::rectangle_node::LayoutPositioning> for LayoutPositioning { + fn from(position: &figma_api::models::rectangle_node::LayoutPositioning) -> Self { + match position { + figma_api::models::rectangle_node::LayoutPositioning::Auto => LayoutPositioning::Auto, + figma_api::models::rectangle_node::LayoutPositioning::Absolute => { + LayoutPositioning::Absolute + } + } + } +} + fn map_option<'a, T, U>(value: Option<&'a T>) -> Option where U: From<&'a T>, @@ -635,8 +758,7 @@ impl FigmaConverter { opacity: Self::convert_opacity(component.visible), blend_mode: Self::convert_blend_mode(component.blend_mode), mask: None, - transform, - size, + rotation: transform.rotation(), corner_radius: Self::convert_corner_radius( component.corner_radius, component.rectangle_corner_radii.as_ref(), @@ -657,13 +779,32 @@ impl FigmaConverter { .map(|v| v.into_iter().map(|x| x as f32).collect()), effects: Self::convert_effects(&component.effects), clip: component.clips_content, - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), - layout_gap: LayoutGap::default(), - padding: EdgeInsets::default(), + position: CGPoint::new(transform.x(), transform.y()).into(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(size.width), + height: Some(size.height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: Some(LayoutChildStyle { + layout_positioning: component + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -734,8 +875,7 @@ impl FigmaConverter { opacity: Self::convert_opacity(instance.visible), blend_mode: Self::convert_blend_mode(instance.blend_mode), mask: None, - transform, - size, + rotation: transform.rotation(), corner_radius: Self::convert_corner_radius( instance.corner_radius, instance.rectangle_corner_radii.as_ref(), @@ -756,13 +896,32 @@ impl FigmaConverter { .map(|v| v.into_iter().map(|x| x as f32).collect()), effects: Self::convert_effects(&instance.effects), clip: instance.clips_content, - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), - layout_gap: LayoutGap::default(), - padding: EdgeInsets::default(), + position: CGPoint::new(transform.x(), transform.y()).into(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(size.width), + height: Some(size.height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: Some(LayoutChildStyle { + layout_positioning: instance + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -782,29 +941,51 @@ impl FigmaConverter { self.links.insert(node_id.clone(), children); } - Ok(Node::Container(ContainerNodeRec { - active: section.visible.unwrap_or(true), - opacity: Self::convert_opacity(section.visible), - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: Self::convert_transform(section.relative_transform.as_ref()), - size: Self::convert_size(section.size.as_ref()), - corner_radius: RectangularCornerRadius::zero(), - fills: self.convert_fills(Some(§ion.fills.as_ref())), - strokes: Paints::default(), - stroke_width: 0.0, - stroke_align: StrokeAlign::Inside, - stroke_dash_array: None, - effects: LayerEffects::default(), - clip: false, - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), - layout_gap: LayoutGap::default(), - padding: EdgeInsets::default(), - })) + { + let transform = Self::convert_transform(section.relative_transform.as_ref()); + let size = Self::convert_size(section.size.as_ref()); + Ok(Node::Container(ContainerNodeRec { + active: section.visible.unwrap_or(true), + opacity: Self::convert_opacity(section.visible), + blend_mode: LayerBlendMode::PassThrough, + mask: None, + rotation: transform.rotation(), + corner_radius: RectangularCornerRadius::zero(), + fills: self.convert_fills(Some(§ion.fills.as_ref())), + strokes: Paints::default(), + stroke_width: 0.0, + stroke_align: StrokeAlign::Inside, + stroke_dash_array: None, + effects: LayerEffects::default(), + clip: false, + position: CGPoint::new(transform.x(), transform.y()).into(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(size.width), + height: Some(size.height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: Some(LayoutChildStyle { + layout_positioning: section + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), + })) + } } /// Convert Figma's link to our LinkUnfurlNode @@ -912,8 +1093,7 @@ impl FigmaConverter { opacity: Self::convert_opacity(origin.visible), blend_mode: Self::convert_blend_mode(origin.blend_mode), mask: None, - transform, - size, + rotation: transform.rotation(), corner_radius: Self::convert_corner_radius( origin.corner_radius, origin.rectangle_corner_radii.as_ref(), @@ -934,13 +1114,32 @@ impl FigmaConverter { .map(|v| v.into_iter().map(|x| x as f32).collect()), effects: Self::convert_effects(&origin.effects), clip: origin.clips_content, - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), - layout_gap: LayoutGap::default(), - padding: EdgeInsets::default(), + position: CGPoint::new(transform.x(), transform.y()).into(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(size.width), + height: Some(size.height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -998,6 +1197,14 @@ impl FigmaConverter { .size .as_ref() .map_or(None, |size| Some(size.x as f32)), + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), height: origin .size .as_ref() @@ -1074,7 +1281,6 @@ impl FigmaConverter { fn convert_vector(&mut self, origin: &Box) -> Result { let mut children = Vec::new(); - let mut path_index = 0; // Convert fill geometries to path nodes if let Some(fill_geometries) = &origin.fill_geometry { @@ -1092,9 +1298,9 @@ impl FigmaConverter { stroke_width: 0.0, stroke_align: StrokeAlign::Inside, stroke_dash_array: None, + layout_child: None, }); children.push(self.repository.insert(path_node)); - path_index += 1; } } @@ -1115,36 +1321,58 @@ impl FigmaConverter { stroke_width: 0.0, stroke_align: StrokeAlign::Inside, stroke_dash_array: None, + layout_child: None, }); children.push(self.repository.insert(path_node)); - path_index += 1; } } // Create a group node containing all the path nodes - Ok(Node::Container(ContainerNodeRec { - active: origin.visible.unwrap_or(true), - opacity: Self::convert_opacity(origin.visible), - blend_mode: Self::convert_blend_mode(origin.blend_mode), - mask: None, - transform: Self::convert_transform(origin.relative_transform.as_ref()), - size: Self::convert_size(origin.size.as_ref()), - corner_radius: RectangularCornerRadius::zero(), - fills: Paints::new([TRANSPARENT]), - strokes: Paints::default(), - stroke_width: 0.0, - stroke_align: StrokeAlign::Inside, - stroke_dash_array: None, - effects: LayerEffects::default(), - clip: false, - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), - padding: EdgeInsets::default(), - layout_gap: LayoutGap::default(), - })) + { + let transform = Self::convert_transform(origin.relative_transform.as_ref()); + let size = Self::convert_size(origin.size.as_ref()); + Ok(Node::Container(ContainerNodeRec { + active: origin.visible.unwrap_or(true), + opacity: Self::convert_opacity(origin.visible), + blend_mode: Self::convert_blend_mode(origin.blend_mode), + mask: None, + rotation: transform.rotation(), + corner_radius: RectangularCornerRadius::zero(), + fills: Paints::new([TRANSPARENT]), + strokes: Paints::default(), + stroke_width: 0.0, + stroke_align: StrokeAlign::Inside, + stroke_dash_array: None, + effects: LayerEffects::default(), + clip: false, + position: CGPoint::new(transform.x(), transform.y()).into(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(size.width), + height: Some(size.height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), + })) + } } fn convert_boolean_operation( @@ -1239,6 +1467,14 @@ impl FigmaConverter { .stroke_dashes .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -1271,6 +1507,14 @@ impl FigmaConverter { .stroke_dashes .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -1312,6 +1556,14 @@ impl FigmaConverter { ), start_angle: origin.arc_data.starting_angle.to_degrees() as f32, corner_radius: None, + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -1346,6 +1598,14 @@ impl FigmaConverter { .stroke_dashes .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } @@ -1379,6 +1639,14 @@ impl FigmaConverter { .stroke_dashes .clone() .map(|v| v.into_iter().map(|x| x as f32).collect()), + layout_child: Some(LayoutChildStyle { + layout_positioning: origin + .layout_positioning + .as_ref() + .map(Into::into) + .unwrap_or_default(), + layout_grow: 0.0, + }), })) } diff --git a/crates/grida-canvas/src/io/io_grida.rs b/crates/grida-canvas/src/io/io_grida.rs index cb2c4ad259..231013e861 100644 --- a/crates/grida-canvas/src/io/io_grida.rs +++ b/crates/grida-canvas/src/io/io_grida.rs @@ -1,6 +1,6 @@ use crate::cg::varwidth::{VarWidthProfile, WidthStop}; use crate::cg::{types::*, Alignment}; -use crate::io::io_css::{de_css_dimension, default_height_css, default_width_css, CSSDimension}; +use crate::io::io_css::*; use crate::node::schema::*; use crate::vectornetwork::*; use math2::{box_fit::BoxFit, transform::AffineTransform}; @@ -231,9 +231,9 @@ pub struct CSSBorder { #[derive(Debug, Deserialize)] pub struct JSONSVGPath { pub d: String, - #[serde(rename = "fillRule")] + #[serde(rename = "fillRule", default)] pub fill_rule: FillRule, - pub fill: String, + pub fill: Option, } #[derive(Debug, Deserialize, Clone)] @@ -518,6 +518,29 @@ impl From for WidthStop { } } +/// Converts JSON positioning fields to LayoutPositioningBasis +/// - If right/bottom are present, uses Inset basis (preserves all four edges) +/// - Otherwise, uses Cartesian basis (x,y) +fn json_position_to_layout_basis( + left: Option, + top: Option, + right: Option, + bottom: Option, +) -> LayoutPositioningBasis { + if right.is_some() || bottom.is_some() { + // Inset basis: preserve all four edges (missing edges default to 0.0) + LayoutPositioningBasis::Inset(EdgeInsets { + top: top.unwrap_or(0.0), + right: right.unwrap_or(0.0), + bottom: bottom.unwrap_or(0.0), + left: left.unwrap_or(0.0), + }) + } else { + // Cartesian basis (x,y) + LayoutPositioningBasis::Cartesian(CGPoint::new(left.unwrap_or(0.0), top.unwrap_or(0.0))) + } +} + /// Utility function to merge single and multiple paint properties according to the specified logic: /// - if paint and no paints, use [paint] /// - if no paint and no paints, use [] @@ -570,6 +593,16 @@ pub struct JSONSceneNode { pub enum JSONCornerRadius { Uniform(f32), PerCorner(Vec), + PerCornerObject { + #[serde(rename = "topLeftRadius")] + top_left_radius: f32, + #[serde(rename = "topRightRadius")] + top_right_radius: f32, + #[serde(rename = "bottomRightRadius")] + bottom_right_radius: f32, + #[serde(rename = "bottomLeftRadius")] + bottom_left_radius: f32, + }, } impl JSONCornerRadius { @@ -603,6 +636,17 @@ impl JSONCornerRadius { } } } + JSONCornerRadius::PerCornerObject { + top_left_radius, + top_right_radius, + bottom_right_radius, + bottom_left_radius, + } => RectangularCornerRadius { + tl: Radius::circular(top_left_radius), + tr: Radius::circular(top_right_radius), + br: Radius::circular(bottom_right_radius), + bl: Radius::circular(bottom_left_radius), + }, } } @@ -621,6 +665,22 @@ impl JSONCornerRadius { None } } + JSONCornerRadius::PerCornerObject { + top_left_radius, + top_right_radius, + bottom_right_radius, + bottom_left_radius, + } => { + // Check if all corners have the same radius + if (top_left_radius - top_right_radius).abs() < f32::EPSILON + && (top_right_radius - bottom_right_radius).abs() < f32::EPSILON + && (bottom_right_radius - bottom_left_radius).abs() < f32::EPSILON + { + Some(top_left_radius) + } else { + None + } + } } } } @@ -645,11 +705,11 @@ pub struct JSONUnknownNodeProperties { pub z_index: i32, // css #[serde(rename = "position")] - pub position: Option, + pub position: Option, #[serde(rename = "left")] - pub left: f32, + pub left: Option, #[serde(rename = "top")] - pub top: f32, + pub top: Option, #[serde(rename = "right")] pub right: Option, #[serde(rename = "bottom")] @@ -738,7 +798,7 @@ pub struct JSONUnknownNodeProperties { pub enum JSONNode { #[serde(rename = "group")] Group(JSONGroupNode), - #[serde(rename = "container")] + #[serde(rename = "container", alias = "component")] Container(JSONContainerNode), #[serde(rename = "svgpath")] SVGPath(JSONSVGPathNode), @@ -765,6 +825,95 @@ pub enum JSONNode { Unknown(JSONUnknownNodeProperties), } +/// JSON representation of LayoutMode for deserialization +#[derive(Debug, Deserialize, Clone, Copy)] +pub enum JSONLayoutMode { + /// Legacy - will be removed, replaced with Normal + #[serde(rename = "flow")] + Flow, + #[serde(rename = "flex")] + Flex, + #[serde(rename = "normal")] + Normal, +} + +impl Default for JSONLayoutMode { + fn default() -> Self { + JSONLayoutMode::Normal + } +} + +impl From for LayoutMode { + fn from(mode: JSONLayoutMode) -> Self { + match mode { + JSONLayoutMode::Flow => LayoutMode::Normal, + JSONLayoutMode::Normal => LayoutMode::Normal, + JSONLayoutMode::Flex => LayoutMode::Flex, + } + } +} + +/// JSON representation of Axis for deserialization +#[derive(Debug, Deserialize, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub enum JSONAxis { + Horizontal, + Vertical, +} + +impl Default for JSONAxis { + fn default() -> Self { + JSONAxis::Horizontal + } +} + +impl From for Axis { + fn from(axis: JSONAxis) -> Self { + match axis { + JSONAxis::Horizontal => Axis::Horizontal, + JSONAxis::Vertical => Axis::Vertical, + } + } +} + +/// JSON representation of padding - supports both uniform and non-uniform values +#[derive(Debug, Deserialize, Clone, Copy, PartialEq)] +#[serde(untagged)] +pub enum JSONPadding { + /// Uniform padding (all sides equal) + Uniform(f32), + /// Non-uniform padding with individual sides + NonUniform { + #[serde(rename = "paddingTop")] + padding_top: f32, + #[serde(rename = "paddingRight")] + padding_right: f32, + #[serde(rename = "paddingBottom")] + padding_bottom: f32, + #[serde(rename = "paddingLeft")] + padding_left: f32, + }, +} + +impl From for EdgeInsets { + fn from(padding: JSONPadding) -> Self { + match padding { + JSONPadding::Uniform(value) => EdgeInsets::all(value), + JSONPadding::NonUniform { + padding_top, + padding_right, + padding_bottom, + padding_left, + } => EdgeInsets { + top: padding_top, + right: padding_right, + bottom: padding_bottom, + left: padding_left, + }, + } + } +} + #[derive(Debug, Deserialize)] pub struct JSONContainerNode { #[serde(flatten)] @@ -774,17 +923,21 @@ pub struct JSONContainerNode { pub expanded: Option, // layout - pub layout: Option, - pub padding: Option, - pub direction: Option, + #[serde(default)] + pub layout: JSONLayoutMode, + pub padding: Option, + #[serde(default)] + pub direction: JSONAxis, + #[serde(rename = "layoutWrap")] + pub layout_wrap: Option, #[serde(rename = "mainAxisAlignment")] - pub main_axis_alignment: Option, + pub main_axis_alignment: Option, #[serde(rename = "crossAxisAlignment")] - pub cross_axis_alignment: Option, - #[serde(rename = "mainAxisGap")] - pub main_axis_gap: Option, - #[serde(rename = "crossAxisGap")] - pub cross_axis_gap: Option, + pub cross_axis_alignment: Option, + #[serde(rename = "mainAxisGap", default)] + pub main_axis_gap: f32, + #[serde(rename = "crossAxisGap", default)] + pub cross_axis_gap: f32, } #[derive(Debug, Deserialize)] @@ -1033,8 +1186,8 @@ pub fn parse(file: &str) -> Result { impl From for GroupNodeRec { fn from(node: JSONGroupNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1054,19 +1207,25 @@ impl From for GroupNodeRec { impl From for ContainerNodeRec { fn from(node: JSONContainerNode) -> Self { + // For containers, preserve Auto vs explicit size distinction + let width = match node.base.width { + CSSDimension::Auto => None, + CSSDimension::LengthPX(length) => Some(length), + }; + let height = match node.base.height { + CSSDimension::Auto => None, + CSSDimension::LengthPX(length) => Some(length), + }; + ContainerNodeRec { active: node.base.active, - transform: AffineTransform::from_box_center( + rotation: node.base.rotation, + position: json_position_to_layout_basis( node.base.left, node.base.top, - node.base.width.length(0.0), - node.base.height.length(0.0), - node.base.rotation, + node.base.right, + node.base.bottom, ), - size: Size { - width: node.base.width.length(0.0), - height: node.base.height.length(0.0), - }, corner_radius: merge_corner_radius( node.base.corner_radius, node.base.corner_radius_top_left, @@ -1090,13 +1249,38 @@ impl From for ContainerNodeRec { // Children populated from links after conversion clip: true, mask: node.base.mask.map(|m| m.into()), - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), - layout_gap: LayoutGap::default(), - padding: EdgeInsets::default(), + layout_container: LayoutContainerStyle { + layout_mode: node.layout.into(), + layout_direction: node.direction.into(), + layout_wrap: node.layout_wrap, + layout_main_axis_alignment: node.main_axis_alignment, + layout_cross_axis_alignment: node.cross_axis_alignment, + layout_padding: node.padding.map(|p| p.into()), + layout_gap: if node.main_axis_gap > 0.0 || node.cross_axis_gap > 0.0 { + Some(LayoutGap { + main_axis_gap: node.main_axis_gap, + cross_axis_gap: node.cross_axis_gap, + }) + } else { + None + }, + }, + layout_dimensions: LayoutDimensionStyle { + width, + height, + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), } } } @@ -1117,13 +1301,21 @@ impl From for TextSpanNodeRec { TextSpanNodeRec { active: node.base.active, transform: AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, ), width, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), height, max_lines: node.max_lines, ellipsis: None, @@ -1189,8 +1381,8 @@ impl From for TextSpanNodeRec { impl From for Node { fn from(node: JSONEllipseNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1221,6 +1413,14 @@ impl From for Node { inner_radius: node.inner_radius, start_angle: node.angle_offset.unwrap_or(0.0), angle: node.angle, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), corner_radius: node .base .corner_radius @@ -1232,8 +1432,8 @@ impl From for Node { impl From for Node { fn from(node: JSONRectangleNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1261,6 +1461,14 @@ impl From for Node { stroke_width: node.base.stroke_width, stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Inside), stroke_dash_array: None, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), effects: merge_effects( node.base.fe_shadows, node.base.fe_blur, @@ -1274,8 +1482,8 @@ impl From for Node { impl From for Node { fn from(node: JSONImageNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1351,6 +1559,14 @@ impl From for Node { stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Inside), stroke_dash_array: None, image: fill.image.clone(), + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), }) } } @@ -1358,8 +1574,8 @@ impl From for Node { impl From for Node { fn from(node: JSONRegularPolygonNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1392,6 +1608,14 @@ impl From for Node { stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Inside), stroke_dash_array: None, point_count: node.point_count, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), }) } } @@ -1399,8 +1623,8 @@ impl From for Node { impl From for Node { fn from(node: JSONRegularStarPolygonNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1434,6 +1658,14 @@ impl From for Node { stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Inside), stroke_dash_array: None, point_count: node.point_count, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), }) } } @@ -1441,8 +1673,8 @@ impl From for Node { impl From for Node { fn from(node: JSONSVGPathNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1473,6 +1705,14 @@ impl From for Node { stroke_width: 0.0, stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Inside), stroke_dash_array: None, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), }) } } @@ -1480,8 +1720,8 @@ impl From for Node { impl From for Node { fn from(node: JSONLineNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1507,6 +1747,14 @@ impl From for Node { stroke_width: node.base.stroke_width, _data_stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Center), stroke_dash_array: None, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), }) } } @@ -1514,8 +1762,8 @@ impl From for Node { impl From for Node { fn from(node: JSONVectorNode) -> Self { let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1551,6 +1799,14 @@ impl From for Node { stroke_width_profile: node.base.stroke_width_profile.map(|p| p.into()), stroke_align: node.base.stroke_align.unwrap_or(StrokeAlign::Inside), stroke_dash_array: None, + layout_child: Some(LayoutChildStyle { + layout_positioning: node + .base + .position + .map(|position| position.into()) + .unwrap_or_default(), + layout_grow: 0.0, + }), }) } } @@ -1559,8 +1815,8 @@ impl From for Node { fn from(node: JSONBooleanOperationNode) -> Self { // TODO: boolean operation's transform should be handled differently let transform = AffineTransform::from_box_center( - node.base.left, - node.base.top, + node.base.left.unwrap_or(0.0), + node.base.top.unwrap_or(0.0), node.base.width.length(0.0), node.base.height.length(0.0), node.base.rotation, @@ -1753,6 +2009,134 @@ mod corner_radius_tests { } } +#[cfg(test)] +mod padding_tests { + use super::*; + use serde_json::json; + + #[test] + fn test_uniform_padding_deserialize() { + let json = json!(20.0); + let padding: JSONPadding = serde_json::from_value(json).unwrap(); + + let edge_insets: EdgeInsets = padding.into(); + assert_eq!(edge_insets.top, 20.0); + assert_eq!(edge_insets.right, 20.0); + assert_eq!(edge_insets.bottom, 20.0); + assert_eq!(edge_insets.left, 20.0); + } + + #[test] + fn test_non_uniform_padding_deserialize() { + let json = json!({ + "paddingTop": 10.0, + "paddingRight": 20.0, + "paddingBottom": 30.0, + "paddingLeft": 40.0 + }); + let padding: JSONPadding = serde_json::from_value(json).unwrap(); + + let edge_insets: EdgeInsets = padding.into(); + assert_eq!(edge_insets.top, 10.0); + assert_eq!(edge_insets.right, 20.0); + assert_eq!(edge_insets.bottom, 30.0); + assert_eq!(edge_insets.left, 40.0); + } + + #[test] + fn test_container_with_uniform_padding() { + let json = json!({ + "type": "container", + "id": "container-1", + "name": "Container", + "active": true, + "locked": false, + "opacity": 1.0, + "blendMode": "normal", + "zIndex": 0, + "position": "absolute", + "left": 0, + "top": 0, + "rotation": 0, + "width": 200, + "height": 200, + "padding": 16.0, + "layout": "flex" + }); + + let container: JSONContainerNode = serde_json::from_value(json).unwrap(); + let container_rec: ContainerNodeRec = container.into(); + + let padding = container_rec.layout_container.layout_padding.unwrap(); + assert_eq!(padding.top, 16.0); + assert_eq!(padding.right, 16.0); + assert_eq!(padding.bottom, 16.0); + assert_eq!(padding.left, 16.0); + } + + #[test] + fn test_container_with_non_uniform_padding() { + let json = json!({ + "type": "container", + "id": "container-2", + "name": "Container", + "active": true, + "locked": false, + "opacity": 1.0, + "blendMode": "normal", + "zIndex": 0, + "position": "absolute", + "left": 0, + "top": 0, + "rotation": 0, + "width": 200, + "height": 200, + "padding": { + "paddingTop": 10.0, + "paddingRight": 15.0, + "paddingBottom": 20.0, + "paddingLeft": 25.0 + }, + "layout": "flex" + }); + + let container: JSONContainerNode = serde_json::from_value(json).unwrap(); + let container_rec: ContainerNodeRec = container.into(); + + let padding = container_rec.layout_container.layout_padding.unwrap(); + assert_eq!(padding.top, 10.0); + assert_eq!(padding.right, 15.0); + assert_eq!(padding.bottom, 20.0); + assert_eq!(padding.left, 25.0); + } + + #[test] + fn test_container_without_padding() { + let json = json!({ + "type": "container", + "id": "container-3", + "name": "Container", + "active": true, + "locked": false, + "opacity": 1.0, + "blendMode": "normal", + "zIndex": 0, + "position": "absolute", + "left": 0, + "top": 0, + "rotation": 0, + "width": 200, + "height": 200, + "layout": "flex" + }); + + let container: JSONContainerNode = serde_json::from_value(json).unwrap(); + let container_rec: ContainerNodeRec = container.into(); + + assert!(container_rec.layout_container.layout_padding.is_none()); + } +} + fn merge_effects( fe_shadows: Option>, fe_blur: Option, @@ -1904,8 +2288,8 @@ mod tests { Some("Boolean Operation".to_string()) ); assert_eq!(boolean_node.op, BooleanPathOperation::Union); - assert_eq!(boolean_node.base.left, 100.0); - assert_eq!(boolean_node.base.top, 100.0); + assert_eq!(boolean_node.base.left, Some(100.0)); + assert_eq!(boolean_node.base.top, Some(100.0)); assert_eq!(boolean_node.base.width, CSSDimension::LengthPX(200.0)); assert_eq!(boolean_node.base.height, CSSDimension::LengthPX(200.0)); } @@ -3260,4 +3644,366 @@ mod tests { _ => panic!("Expected Container node"), } } + + #[test] + fn deserialize_container_with_layout_properties() { + // Test that layout and direction are correctly typed and deserialized + let json = r#"{ + "id": "container-layout", + "name": "Container with Layout", + "type": "container", + "left": 100.0, + "top": 100.0, + "width": 400.0, + "height": 300.0, + "layout": "flex", + "direction": "vertical" + }"#; + + let node: JSONNode = serde_json::from_str(json) + .expect("failed to deserialize container with layout properties"); + + match node { + JSONNode::Container(container) => { + // Verify typed enums + assert!(matches!(container.layout, JSONLayoutMode::Flex)); + assert!(matches!(container.direction, JSONAxis::Vertical)); + + // Verify conversion + let converted: ContainerNodeRec = container.into(); + assert!(matches!( + converted.layout_container.layout_mode, + LayoutMode::Flex + )); + assert!(matches!( + converted.layout_container.layout_direction, + Axis::Vertical + )); + } + _ => panic!("Expected Container node"), + } + } + + #[test] + fn deserialize_container_with_alignment_properties() { + // Test that alignment properties are correctly typed and deserialized + let json = r#"{ + "id": "container-aligned", + "name": "Container with Alignments", + "type": "container", + "left": 0.0, + "top": 0.0, + "width": 600.0, + "height": 400.0, + "layout": "flex", + "direction": "horizontal", + "mainAxisAlignment": "space-between", + "crossAxisAlignment": "center" + }"#; + + let node: JSONNode = serde_json::from_str(json) + .expect("failed to deserialize container with alignment properties"); + + match node { + JSONNode::Container(container) => { + // Verify typed enums + assert!(matches!( + container.main_axis_alignment, + Some(MainAxisAlignment::SpaceBetween) + )); + assert!(matches!( + container.cross_axis_alignment, + Some(CrossAxisAlignment::Center) + )); + + // Verify conversion + let converted: ContainerNodeRec = container.into(); + assert!(matches!( + converted.layout_container.layout_main_axis_alignment, + Some(MainAxisAlignment::SpaceBetween) + )); + assert!(matches!( + converted.layout_container.layout_cross_axis_alignment, + Some(CrossAxisAlignment::Center) + )); + } + _ => panic!("Expected Container node"), + } + } + + #[test] + fn deserialize_container_with_padding() { + // Test that padding is correctly typed and deserialized as uniform value + let json = r#"{ + "id": "container-padded", + "name": "Container with Padding", + "type": "container", + "left": 0.0, + "top": 0.0, + "width": 400.0, + "height": 300.0, + "layout": "flex", + "padding": 20.0 + }"#; + + let node: JSONNode = + serde_json::from_str(json).expect("failed to deserialize container with padding"); + + match node { + JSONNode::Container(container) => { + // Verify padding field + assert_eq!(container.padding, Some(JSONPadding::Uniform(20.0))); + + // Verify conversion to EdgeInsets + let converted: ContainerNodeRec = container.into(); + assert!(converted.layout_container.layout_padding.is_some()); + + let padding = converted.layout_container.layout_padding.unwrap(); + assert_eq!(padding.top, 20.0); + assert_eq!(padding.right, 20.0); + assert_eq!(padding.bottom, 20.0); + assert_eq!(padding.left, 20.0); + } + _ => panic!("Expected Container node"), + } + } + + #[test] + fn deserialize_container_with_complete_layout() { + // Test a container with all layout properties + let json = r#"{ + "id": "container-complete", + "name": "Complete Layout Container", + "type": "container", + "left": 50.0, + "top": 50.0, + "width": 500.0, + "height": 400.0, + "layout": "flex", + "direction": "vertical", + "padding": 15.0, + "mainAxisAlignment": "center", + "crossAxisAlignment": "stretch" + }"#; + + let node: JSONNode = serde_json::from_str(json) + .expect("failed to deserialize container with complete layout"); + + match node { + JSONNode::Container(container) => { + // Verify all properties + assert!(matches!(container.layout, JSONLayoutMode::Flex)); + assert!(matches!(container.direction, JSONAxis::Vertical)); + assert_eq!(container.padding, Some(JSONPadding::Uniform(15.0))); + assert!(matches!( + container.main_axis_alignment, + Some(MainAxisAlignment::Center) + )); + assert!(matches!( + container.cross_axis_alignment, + Some(CrossAxisAlignment::Stretch) + )); + + // Verify conversion + let converted: ContainerNodeRec = container.into(); + assert!(matches!( + converted.layout_container.layout_mode, + LayoutMode::Flex + )); + assert!(matches!( + converted.layout_container.layout_direction, + Axis::Vertical + )); + assert!(converted.layout_container.layout_padding.is_some()); + + let padding = converted.layout_container.layout_padding.unwrap(); + assert_eq!(padding.top, 15.0); + assert_eq!(padding.right, 15.0); + assert_eq!(padding.bottom, 15.0); + assert_eq!(padding.left, 15.0); + } + _ => panic!("Expected Container node"), + } + } + + #[test] + fn deserialize_container_with_gap() { + // Test that gap properties are correctly deserialized + let json = r#"{ + "id": "container-gap", + "name": "Container with Gap", + "type": "container", + "left": 0.0, + "top": 0.0, + "width": 400.0, + "height": 300.0, + "layout": "flex", + "mainAxisGap": 20.0, + "crossAxisGap": 10.0 + }"#; + + let node: JSONNode = + serde_json::from_str(json).expect("failed to deserialize container with gap"); + + match node { + JSONNode::Container(container) => { + // Verify gap fields (non-optional now) + assert_eq!(container.main_axis_gap, 20.0); + assert_eq!(container.cross_axis_gap, 10.0); + + // Verify conversion to LayoutGap + let converted: ContainerNodeRec = container.into(); + assert!(converted.layout_container.layout_gap.is_some()); + + let gap = converted.layout_container.layout_gap.unwrap(); + assert_eq!(gap.main_axis_gap, 20.0); + assert_eq!(gap.cross_axis_gap, 10.0); + } + _ => panic!("Expected Container node"), + } + } + + #[test] + fn deserialize_container_with_layout_wrap() { + // Test that layoutWrap is correctly deserialized + let json_wrap = r#"{ + "id": "container-wrap", + "name": "Container with Wrap", + "type": "container", + "left": 0.0, + "top": 0.0, + "width": 400.0, + "height": 300.0, + "layout": "flex", + "layoutWrap": "wrap" + }"#; + + let node: JSONNode = + serde_json::from_str(json_wrap).expect("failed to deserialize container with wrap"); + + match node { + JSONNode::Container(container) => { + assert!(matches!(container.layout_wrap, Some(LayoutWrap::Wrap))); + + let converted: ContainerNodeRec = container.into(); + assert!(matches!( + converted.layout_container.layout_wrap, + Some(LayoutWrap::Wrap) + )); + } + _ => panic!("Expected Container node"), + } + + // Test nowrap + let json_nowrap = r#"{ + "id": "container-nowrap", + "name": "Container with NoWrap", + "type": "container", + "left": 0.0, + "top": 0.0, + "width": 400.0, + "height": 300.0, + "layout": "flex", + "layoutWrap": "nowrap" + }"#; + + let node: JSONNode = + serde_json::from_str(json_nowrap).expect("failed to deserialize container with nowrap"); + + match node { + JSONNode::Container(container) => { + assert!(matches!(container.layout_wrap, Some(LayoutWrap::NoWrap))); + + let converted: ContainerNodeRec = container.into(); + assert!(matches!( + converted.layout_container.layout_wrap, + Some(LayoutWrap::NoWrap) + )); + } + _ => panic!("Expected Container node"), + } + } + + #[test] + fn deserialize_container_with_all_layout_properties() { + // Test a container with all layout properties including gap and wrap + let json = r#"{ + "id": "container-all", + "name": "All Layout Properties", + "type": "container", + "left": 100.0, + "top": 100.0, + "width": 600.0, + "height": 500.0, + "layout": "flex", + "direction": "horizontal", + "layoutWrap": "wrap", + "padding": 20.0, + "mainAxisGap": 30.0, + "crossAxisGap": 15.0, + "mainAxisAlignment": "space-between", + "crossAxisAlignment": "center" + }"#; + + let node: JSONNode = serde_json::from_str(json) + .expect("failed to deserialize container with all layout properties"); + + match node { + JSONNode::Container(container) => { + // Verify all properties + assert!(matches!(container.layout, JSONLayoutMode::Flex)); + assert!(matches!(container.direction, JSONAxis::Horizontal)); + assert!(matches!(container.layout_wrap, Some(LayoutWrap::Wrap))); + assert_eq!(container.padding, Some(JSONPadding::Uniform(20.0))); + assert_eq!(container.main_axis_gap, 30.0); + assert_eq!(container.cross_axis_gap, 15.0); + assert!(matches!( + container.main_axis_alignment, + Some(MainAxisAlignment::SpaceBetween) + )); + assert!(matches!( + container.cross_axis_alignment, + Some(CrossAxisAlignment::Center) + )); + + // Verify conversion + let converted: ContainerNodeRec = container.into(); + assert!(matches!( + converted.layout_container.layout_mode, + LayoutMode::Flex + )); + assert!(matches!( + converted.layout_container.layout_direction, + Axis::Horizontal + )); + assert!(matches!( + converted.layout_container.layout_wrap, + Some(LayoutWrap::Wrap) + )); + + // Verify padding + let padding = converted.layout_container.layout_padding.unwrap(); + assert_eq!(padding.top, 20.0); + assert_eq!(padding.right, 20.0); + assert_eq!(padding.bottom, 20.0); + assert_eq!(padding.left, 20.0); + + // Verify gap + let gap = converted.layout_container.layout_gap.unwrap(); + assert_eq!(gap.main_axis_gap, 30.0); + assert_eq!(gap.cross_axis_gap, 15.0); + + // Verify alignments + assert!(matches!( + converted.layout_container.layout_main_axis_alignment, + Some(MainAxisAlignment::SpaceBetween) + )); + assert!(matches!( + converted.layout_container.layout_cross_axis_alignment, + Some(CrossAxisAlignment::Center) + )); + } + _ => panic!("Expected Container node"), + } + } } diff --git a/crates/grida-canvas/src/layout/cache.rs b/crates/grida-canvas/src/layout/cache.rs new file mode 100644 index 0000000000..06ba13745b --- /dev/null +++ b/crates/grida-canvas/src/layout/cache.rs @@ -0,0 +1,46 @@ +use crate::layout::ComputedLayout; +use crate::node::schema::NodeId; +use std::collections::HashMap; + +/// Immutable layout computation result +/// +/// Maps NodeId to computed position/size. Represents the output of a layout +/// computation phase. Cached between frames for performance and change detection. +#[derive(Debug, Clone, PartialEq)] +pub struct LayoutResult { + layouts: HashMap, +} + +impl LayoutResult { + pub fn new() -> Self { + Self { + layouts: HashMap::new(), + } + } + + pub fn insert(&mut self, id: NodeId, layout: ComputedLayout) { + self.layouts.insert(id, layout); + } + + pub fn get(&self, id: &NodeId) -> Option<&ComputedLayout> { + self.layouts.get(id) + } + + pub fn len(&self) -> usize { + self.layouts.len() + } + + pub fn is_empty(&self) -> bool { + self.layouts.is_empty() + } + + pub fn clear(&mut self) { + self.layouts.clear(); + } +} + +impl Default for LayoutResult { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs new file mode 100644 index 0000000000..48bac3956a --- /dev/null +++ b/crates/grida-canvas/src/layout/engine.rs @@ -0,0 +1,1447 @@ +//! Universal Layout Engine +//! +//! This module provides a universal layout computation engine that handles all node types +//! without switch-case logic. Every node in the scene graph participates in layout computation +//! with appropriate Taffy styles based on their type and properties. +//! +//! ## Universal Design +//! +//! - **No switch-case logic**: All node types are handled uniformly through `node_to_taffy_style()` +//! - **Extensible**: Adding layout support to new node types requires only adding a case to the style mapping +//! - **ICB-less support**: Works with or without InitialContainerBlock, supports any node as root +//! - **Future-proof**: Clear path to support layout on all node types +//! +//! ## Infinite Canvas Support +//! +//! Grida is an infinite canvas with layout capabilities. Root nodes can be positioned anywhere +//! in the viewport (like artboards in design tools), while their children participate in flex layout. +//! +//! ### The Challenge +//! +//! Taffy (our layout library) cannot position tree roots using `position: absolute` with `inset` +//! because roots have no containing block. Taffy always computes root nodes at (0, 0). +//! +//! ### The Solution +//! +//! **Post-processing in `extract_all_layouts()`**: +//! 1. Taffy computes all layouts (roots at 0,0, children correctly positioned) +//! 2. `extract_all_layouts()` detects root nodes via `graph.is_root()` +//! 3. Root positions are overridden with schema positions via `get_schema_position()` +//! 4. GeometryCache consumes corrected layout results (no special cases needed) +//! +//! This keeps **all infinite canvas logic in LayoutEngine**, maintaining clean separation: +//! - **LayoutEngine**: Owns layout computation AND infinite canvas positioning +//! - **GeometryCache**: Transforms layout results to geometry (no layout concerns) +//! +//! See `test_root_positioning_integration()` for the full pipeline verification. +//! +//! ## Pipeline Guarantees +//! +//! This module guarantees: +//! - Every node gets a layout result (either computed or static) +//! - Root node positions are corrected to schema positions (infinite canvas) +//! - Layout results are complete before Geometry phase begins +//! - Missing layout results indicate a bug in the layout engine + +use crate::layout::cache::LayoutResult; +use crate::layout::tree::LayoutTree; +use crate::layout::ComputedLayout; +use crate::node::scene_graph::SceneGraph; +use crate::node::schema::{Node, NodeId, NodeRectMixin, Size}; +use taffy::prelude::*; + +/// Universal Layout Engine +/// +/// Responsible for computing layouts for all node types in the scene graph. +/// Uses a universal approach where every node participates in layout computation +/// with appropriate Taffy styles. Owns the LayoutTree (taffy integration) and +/// caches results between frames. +pub struct LayoutEngine { + tree: LayoutTree, + result: LayoutResult, +} + +impl LayoutEngine { + pub fn new() -> Self { + Self { + tree: LayoutTree::new(), + result: LayoutResult::new(), + } + } + + /// Compute layouts for all nodes in the scene + /// + /// This universal approach processes all node types without switch-case logic. + /// Each root node is laid out independently, supporting both ICB and non-ICB scenarios. + pub fn compute( + &mut self, + scene: &crate::node::schema::Scene, + viewport_size: Size, + ) -> &LayoutResult { + // Clear previous state + self.tree.clear(); + self.result.clear(); + + let graph = &scene.graph; + let roots: Vec = graph.roots().to_vec(); + + // Build and compute layout for each root subtree + for root_id in &roots { + if let Some(root_taffy_id) = self.build_taffy_subtree(root_id, graph, viewport_size) { + // Compute layout with viewport as available space + let _ = self.tree.compute_layout( + root_taffy_id, + taffy::Size { + width: AvailableSpace::Definite(viewport_size.width), + height: AvailableSpace::Definite(viewport_size.height), + }, + ); + + // Extract all computed layouts + self.extract_all_layouts(root_id, graph); + } + } + + &self.result + } + + /// Get the full layout result + pub fn result(&self) -> &LayoutResult { + &self.result + } + + /// Extract schema width, height from any node type + fn get_schema_size(node: &Node) -> (f32, f32) { + match node { + Node::Container(n) => ( + n.layout_dimensions.width.unwrap_or(0.0), + n.layout_dimensions.height.unwrap_or(0.0), + ), + Node::Rectangle(n) => (n.size.width, n.size.height), + Node::Ellipse(n) => (n.size.width, n.size.height), + Node::Image(n) => (n.size.width, n.size.height), + Node::Line(n) => (n.size.width, n.size.height), + Node::Polygon(n) => { + let rect = n.rect(); + (rect.width, rect.height) + } + Node::RegularPolygon(n) => (n.size.width, n.size.height), + Node::RegularStarPolygon(n) => (n.size.width, n.size.height), + Node::TextSpan(n) => (n.width.unwrap_or(0.0), n.height.unwrap_or(0.0)), + Node::Vector(n) => { + let rect = n.network.bounds(); + (rect.width, rect.height) + } + Node::SVGPath(n) => { + // Use NodeRectMixin::rect() to compute bounds from path data + // Note: This involves SVG parsing and is not cached - avoid in tight loops + let rect = n.rect(); + (rect.width, rect.height) + } + Node::Group(_) | Node::BooleanOperation(_) => { + // Size derived from children bounds (dynamic) + (0.0, 0.0) + } + Node::Error(n) => (n.size.width, n.size.height), + Node::InitialContainer(_) => (0.0, 0.0), // Size set by viewport + } + } + + /// Extract schema x, y position from any node type + /// + /// Used for infinite canvas support: root nodes use their schema positions + /// instead of Taffy's computed (0, 0) position. + /// + /// - Container nodes use `position.x()` / `position.y()` + /// - Leaf nodes (Rectangle, Ellipse, etc.) use `transform.x()` / `transform.y()` + /// - ICB (InitialContainerBlock) always returns (0, 0) + fn get_schema_position(node: &Node) -> (f32, f32) { + match node { + // Container nodes use position field + Node::InitialContainer(_) => (0.0, 0.0), // ICB always at origin + Node::Container(n) => (n.position.x().unwrap_or(0.0), n.position.y().unwrap_or(0.0)), + + // Leaf nodes with transform field + Node::Rectangle(n) => (n.transform.x(), n.transform.y()), + Node::Ellipse(n) => (n.transform.x(), n.transform.y()), + Node::Image(n) => (n.transform.x(), n.transform.y()), + Node::Line(n) => (n.transform.x(), n.transform.y()), + Node::Polygon(n) => (n.transform.x(), n.transform.y()), + Node::RegularPolygon(n) => (n.transform.x(), n.transform.y()), + Node::RegularStarPolygon(n) => (n.transform.x(), n.transform.y()), + Node::TextSpan(n) => (n.transform.x(), n.transform.y()), + Node::Vector(n) => (n.transform.x(), n.transform.y()), + Node::SVGPath(n) => (n.transform.x(), n.transform.y()), + Node::Error(n) => (n.transform.x(), n.transform.y()), + + // Complex nodes with optional transform + Node::Group(n) => { + let t = n.transform.unwrap_or_default(); + (t.x(), t.y()) + } + Node::BooleanOperation(n) => { + let t = n.transform.unwrap_or_default(); + (t.x(), t.y()) + } + } + } + + /// Check if a node should participate in Taffy layout + /// + /// Nodes that should be in Taffy tree: + /// - Layout containers (Container, ICB) - need to lay out children + /// - Nodes with layout_child field - can participate as flex children + /// + /// Nodes skipped from Taffy (use manual schema layout): + /// - Group, BooleanOperation - no layout_child support (size derived from children) + fn should_participate_in_taffy(node: &Node) -> bool { + matches!( + node, + Node::Container(_) + | Node::InitialContainer(_) + | Node::Rectangle(_) + | Node::Ellipse(_) + | Node::Image(_) + | Node::Line(_) + | Node::Polygon(_) + | Node::RegularPolygon(_) + | Node::RegularStarPolygon(_) + | Node::TextSpan(_) + | Node::Error(_) + | Node::Vector(_) + | Node::SVGPath(_) + ) + } + + /// Recursively build Taffy tree for a node and its descendants + /// + /// This universal method handles all node types without switch-case logic. + /// Each node gets an appropriate Taffy style based on its type and properties. + /// + /// Nodes without layout_child support are skipped from Taffy tree but still + /// get layout results created manually from their schema. + fn build_taffy_subtree( + &mut self, + node_id: &NodeId, + graph: &SceneGraph, + viewport_size: Size, + ) -> Option { + let node = graph.get_node(node_id).ok()?; + + // Nodes that don't participate in Taffy layout (Vector, SVGPath, Group, etc.) + // are skipped and get manual layout results created in extract_all_layouts() + if !Self::should_participate_in_taffy(node) { + return None; // Skip Taffy, use manual layout result from schema + } + + // Get style for this node (universal mapping) + // Note: Absolutely positioned children are still included in the tree, + // Taffy handles them specially (removes them from flex flow but computes their position) + let mut style = crate::layout::into_taffy::node_to_taffy_style(node, graph, node_id); + + // Note: Root nodes are laid out by Taffy at (0,0) + // extract_all_layouts() post-processes to apply schema positions + + // Special handling for root ICB nodes - use viewport size + if let Node::InitialContainer(_) = node { + style.size = taffy::Size { + width: Dimension::length(viewport_size.width), + height: Dimension::length(viewport_size.height), + }; + } + + // Check if node has children + let children = graph.get_children(node_id); + + if let Some(children) = children { + if !children.is_empty() { + // Build children recursively, filtering out those that shouldn't participate + let taffy_children: Vec = children + .iter() + .filter_map(|child_id| self.build_taffy_subtree(child_id, graph, viewport_size)) + .collect(); + + // Create parent with children + return self + .tree + .new_with_children(*node_id, style, &taffy_children) + .ok(); + } + } + + // Leaf node + self.tree.new_leaf(*node_id, style).ok() + } + + /// Recursively extract all computed layouts from the Taffy tree + /// + /// **Infinite Canvas Support**: Root nodes have their positions corrected here. + /// + /// Taffy computes all tree roots at (0, 0) because they have no containing block. + /// For infinite canvas support, we override root positions with their schema positions + /// so multiple artboards/nodes can be positioned anywhere in the viewport. + /// + /// **Non-Layout Nodes**: Nodes without layout_child field (Vector, SVGPath, Group, etc.) + /// are skipped from Taffy tree. We create manual layout results from their schema here. + /// + /// Child nodes use Taffy's computed positions unchanged (correct flex/absolute layout). + fn extract_all_layouts(&mut self, id: &NodeId, graph: &SceneGraph) { + // Extract this node's layout if it exists in the Taffy tree + if let Some(layout) = self.tree.get_layout(id) { + let mut computed = ComputedLayout::from(layout); + + // Apply schema position for root nodes (Taffy computes roots at 0,0) + if graph.is_root(id) { + if let Ok(node) = graph.get_node(id) { + let (schema_x, schema_y) = Self::get_schema_position(node); + computed.x = schema_x; + computed.y = schema_y; + } + } + + self.result.insert(*id, computed); + } else { + // Node not in Taffy tree (skipped due to no layout_child support) + // Create manual layout result from schema + if let Ok(node) = graph.get_node(id) { + let (x, y) = Self::get_schema_position(node); + let (width, height) = Self::get_schema_size(node); + + self.result.insert( + *id, + ComputedLayout { + x, + y, + width, + height, + }, + ); + } + } + + // Recurse for children + if let Some(children) = graph.get_children(id) { + for child_id in children { + self.extract_all_layouts(child_id, graph); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::cg::types::{ + Axis, CGPoint, CrossAxisAlignment, LayoutGap, LayoutMode, LayoutWrap, MainAxisAlignment, + }; + use crate::node::factory::NodeFactory; + use crate::node::scene_graph::{Parent, SceneGraph}; + use crate::node::schema::*; + use math2::transform::AffineTransform; + + /// Test 1: Flex container with mixed node types + #[test] + fn test_universal_layout_mixed_nodes() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a flex container + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + layout_gap: Some(LayoutGap::uniform(10.0)), + ..Default::default() + }; + container.layout_dimensions.width = Some(300.0); + container.layout_dimensions.height = Some(200.0); + + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add a rectangle child + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 50.0, + height: 50.0, + }; + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + + // Add an ellipse child + let mut ellipse = nf.create_ellipse_node(); + ellipse.size = Size { + width: 60.0, + height: 60.0, + }; + let ellipse_id = graph.append_child(Node::Ellipse(ellipse), Parent::NodeId(container_id)); + + let scene = Scene { + name: "Mixed nodes test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify all nodes get layout results + assert!(result.get(&container_id).is_some()); + assert!(result.get(&rect_id).is_some()); + assert!(result.get(&ellipse_id).is_some()); + + // Verify layout positions (rect should be at 0,0, ellipse at 60,0 due to gap) + let rect_layout = result.get(&rect_id).unwrap(); + let ellipse_layout = result.get(&ellipse_id).unwrap(); + + assert_eq!(rect_layout.x, 0.0); + assert_eq!(rect_layout.y, 0.0); + assert_eq!(ellipse_layout.x, 60.0); // 50 + 10 gap + assert_eq!(ellipse_layout.y, 0.0); + } + + /// Test 2: Root container without ICB + #[test] + fn test_root_container_no_icb() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a container as root (no ICB) + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Vertical, + ..Default::default() + }; + container.layout_dimensions.width = Some(200.0); + container.layout_dimensions.height = Some(150.0); + + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add a child rectangle + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 50.0, + }; + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + + let scene = Scene { + name: "Root container test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify layout works without ICB + assert!(result.get(&container_id).is_some()); + assert!(result.get(&rect_id).is_some()); + + let container_layout = result.get(&container_id).unwrap(); + let rect_layout = result.get(&rect_id).unwrap(); + + // Container should be positioned at its specified size + assert_eq!(container_layout.width, 200.0); + assert_eq!(container_layout.height, 150.0); + + // Rectangle should be positioned within the container + assert_eq!(rect_layout.width, 100.0); + assert_eq!(rect_layout.height, 50.0); + } + + /// Test 3: Root rectangle (non-layout node) + #[test] + fn test_root_static_node() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a rectangle as root + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 80.0, + }; + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "Root rectangle test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify rectangle gets layout result with its dimensions + assert!(result.get(&rect_id).is_some()); + + let rect_layout = result.get(&rect_id).unwrap(); + assert_eq!(rect_layout.width, 100.0); + assert_eq!(rect_layout.height, 80.0); + assert_eq!(rect_layout.x, 0.0); + assert_eq!(rect_layout.y, 0.0); + } + + /// Test 4: Nested flex containers + #[test] + fn test_nested_flex_containers() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Outer container (horizontal) + let mut outer = nf.create_container_node(); + outer.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + ..Default::default() + }; + outer.layout_dimensions.width = Some(400.0); + outer.layout_dimensions.height = Some(200.0); + let outer_id = graph.append_child(Node::Container(outer), Parent::Root); + + // Inner container (vertical) + let mut inner = nf.create_container_node(); + inner.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Vertical, + ..Default::default() + }; + inner.layout_dimensions.width = Some(200.0); + inner.layout_dimensions.height = Some(150.0); + let inner_id = graph.append_child(Node::Container(inner), Parent::NodeId(outer_id)); + + // Rectangle in inner container + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 50.0, + height: 30.0, + }; + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(inner_id)); + + let scene = Scene { + name: "Nested containers test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify nested layout computation + assert!(result.get(&outer_id).is_some()); + assert!(result.get(&inner_id).is_some()); + assert!(result.get(&rect_id).is_some()); + + let rect_layout = result.get(&rect_id).unwrap(); + assert_eq!(rect_layout.width, 50.0); + assert_eq!(rect_layout.height, 30.0); + } + + /// Test 5: ICB with direct children (existing behavior) + #[test] + fn test_icb_flex_layout() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create ICB + let mut icb = nf.create_initial_container_node(); + icb.layout_mode = LayoutMode::Flex; + icb.layout_direction = Axis::Horizontal; + icb.layout_gap = LayoutGap { + main_axis_gap: 20.0, + cross_axis_gap: 20.0, + }; + let icb_id = graph.append_child(Node::InitialContainer(icb), Parent::Root); + + // Add container children + let mut container1 = nf.create_container_node(); + container1.layout_dimensions.width = Some(100.0); + container1.layout_dimensions.height = Some(100.0); + let container1_id = graph.append_child(Node::Container(container1), Parent::NodeId(icb_id)); + + let mut container2 = nf.create_container_node(); + container2.layout_dimensions.width = Some(100.0); + container2.layout_dimensions.height = Some(100.0); + let container2_id = graph.append_child(Node::Container(container2), Parent::NodeId(icb_id)); + + let scene = Scene { + name: "ICB flex test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify ICB fills viewport and lays out children + assert!(result.get(&icb_id).is_some()); + assert!(result.get(&container1_id).is_some()); + assert!(result.get(&container2_id).is_some()); + + let icb_layout = result.get(&icb_id).unwrap(); + let container1_layout = result.get(&container1_id).unwrap(); + let container2_layout = result.get(&container2_id).unwrap(); + + // ICB should fill viewport + assert_eq!(icb_layout.width, 800.0); + assert_eq!(icb_layout.height, 600.0); + + // Children should be positioned horizontally with gap + assert_eq!(container1_layout.x, 0.0); + assert_eq!(container2_layout.x, 120.0); // 100 + 20 gap + } + + /// Test 6: Text nodes in flex layout + #[test] + fn test_text_in_flex() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create flex container + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + ..Default::default() + }; + container.layout_dimensions.width = Some(300.0); + container.layout_dimensions.height = Some(100.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add text child + let mut text = nf.create_text_span_node(); + text.width = Some(150.0); + let text_id = graph.append_child(Node::TextSpan(text), Parent::NodeId(container_id)); + + let scene = Scene { + name: "Text in flex test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify text participates in layout + assert!(result.get(&container_id).is_some()); + assert!(result.get(&text_id).is_some()); + + let text_layout = result.get(&text_id).unwrap(); + assert_eq!(text_layout.width, 150.0); + } + + /// Test 7: Multiple root nodes + #[test] + fn test_multiple_roots() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // First root container + let mut container1 = nf.create_container_node(); + container1.layout_dimensions.width = Some(200.0); + container1.layout_dimensions.height = Some(100.0); + let container1_id = graph.append_child(Node::Container(container1), Parent::Root); + + // Second root container + let mut container2 = nf.create_container_node(); + container2.layout_dimensions.width = Some(150.0); + container2.layout_dimensions.height = Some(80.0); + let container2_id = graph.append_child(Node::Container(container2), Parent::Root); + + let scene = Scene { + name: "Multiple roots test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify each root is laid out independently + assert!(result.get(&container1_id).is_some()); + assert!(result.get(&container2_id).is_some()); + + let container1_layout = result.get(&container1_id).unwrap(); + let container2_layout = result.get(&container2_id).unwrap(); + + assert_eq!(container1_layout.width, 200.0); + assert_eq!(container1_layout.height, 100.0); + assert_eq!(container2_layout.width, 150.0); + assert_eq!(container2_layout.height, 80.0); + } + + /// Test 8: Empty container + #[test] + fn test_empty_container() { + let mut engine = LayoutEngine::new(); + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create container with no children + let mut container = nf.create_container_node(); + container.layout_dimensions.width = Some(200.0); + container.layout_dimensions.height = Some(100.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + let scene = Scene { + name: "Empty container test".to_string(), + graph, + background_color: None, + }; + + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify it still gets layout result + assert!(result.get(&container_id).is_some()); + + let container_layout = result.get(&container_id).unwrap(); + assert_eq!(container_layout.width, 200.0); + assert_eq!(container_layout.height, 100.0); + } + + #[test] + fn test_grida_style_has_zero_flex_shrink() { + // Verify that Grida's default style has flex_shrink: 0.0 + use crate::layout::into_taffy::node_to_taffy_style; + + let nf = NodeFactory::new(); + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 100.0, + }; + + let node = Node::Rectangle(rect); + let graph = SceneGraph::new(); + let node_id: NodeId = 0; + let style = node_to_taffy_style(&node, &graph, &node_id); + + // Verify flex_shrink is 0.0, not Taffy's default 1.0 + assert_eq!( + style.flex_shrink, 0.0, + "Grida nodes should have flex_shrink: 0.0 by default" + ); + } + + #[test] + fn test_container_children_dont_shrink_by_default() { + // Verify that children in a flex container don't shrink when overflowing + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a 200px wide flex container (horizontal) + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + ..Default::default() + }; + container.layout_dimensions.width = Some(200.0); + container.layout_dimensions.height = Some(100.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add three 100px wide children (total: 300px > 200px container) + for _ in 0..3 { + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 50.0, + }; + graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + } + + // Compute layout + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // With flex_shrink: 0.0, children should NOT shrink + // They keep their 100px width (overflow behavior) + let children = scene.graph.get_children(&container_id).unwrap(); + for child_id in children { + if let Some(layout) = result.get(child_id) { + assert_eq!( + layout.width, 100.0, + "Children should NOT shrink from 100px (flex_shrink: 0.0)" + ); + } + } + } + + #[test] + fn test_flex_wrap_gap_spacing() { + // Verify that gap spacing is correct when items wrap + // This tests that cross_axis_gap maps to row-gap correctly + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a small width container (150px) with horizontal flex and wrap + // This will force two 100px items to wrap onto separate rows + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + layout_wrap: Some(LayoutWrap::Wrap), + layout_gap: Some(LayoutGap { + main_axis_gap: 5.0, // horizontal gap (column-gap) + cross_axis_gap: 20.0, // vertical gap (row-gap) - this should be exact! + }), + ..Default::default() + }; + container.layout_dimensions.width = Some(150.0); + container.layout_dimensions.height = Some(300.0); // Tall enough to see vertical gap + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add two 100px wide items (will wrap because 200px > 150px container) + let mut rect1 = nf.create_rectangle_node(); + rect1.size = Size { + width: 100.0, + height: 50.0, + }; + let child1_id = graph.append_child(Node::Rectangle(rect1), Parent::NodeId(container_id)); + + let mut rect2 = nf.create_rectangle_node(); + rect2.size = Size { + width: 100.0, + height: 50.0, + }; + let child2_id = graph.append_child(Node::Rectangle(rect2), Parent::NodeId(container_id)); + + // Compute layout + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Get layouts + let layout1 = result.get(&child1_id).expect("Child 1 should have layout"); + let layout2 = result.get(&child2_id).expect("Child 2 should have layout"); + + // Child 1 should be at y=0 + assert_eq!(layout1.y, 0.0, "First item should be at y=0"); + + // Child 2 should wrap to next row + // y position = first item height (50) + cross_axis_gap (20) = 70 + assert_eq!( + layout2.y, 70.0, + "Second item should be at y = 50 (first item height) + 20 (cross_axis_gap)" + ); + + // Both items should be at x=0 (start of their respective rows) + assert_eq!(layout1.x, 0.0); + assert_eq!(layout2.x, 0.0); + } + + #[test] + fn test_flex_wrap_with_center_alignment() { + // Verify that wrap + center alignment works correctly + // Even with a single child that doesn't actually wrap + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a 1000x1000 container with center alignment and wrap + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + layout_wrap: Some(LayoutWrap::Wrap), + layout_main_axis_alignment: Some(MainAxisAlignment::Center), + layout_cross_axis_alignment: Some(CrossAxisAlignment::Center), + ..Default::default() + }; + container.layout_dimensions.width = Some(1000.0); + container.layout_dimensions.height = Some(1000.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add a single 100x100 child + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 100.0, + height: 100.0, + }; + let child_id = graph.append_child(Node::Rectangle(rect), Parent::NodeId(container_id)); + + // Compute layout + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 1200.0, + height: 1200.0, + }, + ); + + // Get layout + let layout = result.get(&child_id).expect("Child should have layout"); + + // Child should be centered in both axes + // Main axis (horizontal): x = (1000 - 100) / 2 = 450 + // Cross axis (vertical): y = (1000 - 100) / 2 = 450 + assert_eq!( + layout.x, 450.0, + "Child should be centered horizontally (main axis)" + ); + assert_eq!( + layout.y, 450.0, + "Child should be centered vertically (cross axis)" + ); + } + + #[test] + fn test_absolute_positioned_child_not_in_flex_flow() { + // Verify that absolutely positioned children don't affect flex layout flow + // but still get positioned by Taffy + use crate::cg::types::LayoutPositioning; + use crate::node::schema::LayoutChildStyle; + + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a flex container + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Horizontal, + layout_gap: Some(LayoutGap::uniform(10.0)), + ..Default::default() + }; + container.layout_dimensions.width = Some(400.0); + container.layout_dimensions.height = Some(200.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Add a normal (relative) child + let mut rect1 = nf.create_rectangle_node(); + rect1.size = Size { + width: 100.0, + height: 100.0, + }; + let child1_id = graph.append_child(Node::Rectangle(rect1), Parent::NodeId(container_id)); + + // Add an absolutely positioned child at (50, 75) + let mut rect2 = nf.create_rectangle_node(); + rect2.size = Size { + width: 100.0, + height: 100.0, + }; + rect2.transform = AffineTransform::new(50.0, 75.0, 0.0); + rect2.layout_child = Some(LayoutChildStyle { + layout_positioning: LayoutPositioning::Absolute, + layout_grow: 0.0, + }); + let child2_id = graph.append_child(Node::Rectangle(rect2), Parent::NodeId(container_id)); + + // Add another normal child + let mut rect3 = nf.create_rectangle_node(); + rect3.size = Size { + width: 100.0, + height: 100.0, + }; + let child3_id = graph.append_child(Node::Rectangle(rect3), Parent::NodeId(container_id)); + + // Compute layout + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify all children get layout results (Taffy computes absolute positioned ones too) + let layout1 = result + .get(&child1_id) + .expect("Relative child 1 should have layout"); + let layout2 = result + .get(&child2_id) + .expect("Absolute child 2 should have layout from Taffy"); + let layout3 = result + .get(&child3_id) + .expect("Relative child 3 should have layout"); + + // Verify that relative children are positioned as if absolute child doesn't exist in flex flow + // child1 at x=0, child3 at x=110 (100 + 10 gap) + assert_eq!(layout1.x, 0.0, "First relative child at x=0"); + assert_eq!( + layout3.x, 110.0, + "Second relative child at x=110 (ignoring absolute child in flex flow)" + ); + + // Verify absolute child is positioned at its inset coordinates (50, 75) + assert_eq!(layout2.x, 50.0, "Absolute child at x=50 (from inset)"); + assert_eq!(layout2.y, 75.0, "Absolute child at y=75 (from inset)"); + } + + #[test] + fn test_root_container_respects_position() { + // Verify that root containers at non-zero positions work correctly + // LayoutEngine post-processes Taffy results to apply schema positions + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a root container at position (100, 50) + let mut container = nf.create_container_node(); + container.position = LayoutPositioningBasis::Cartesian(CGPoint::new(100.0, 50.0)); + container.layout_dimensions.width = Some(200.0); + container.layout_dimensions.height = Some(150.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + let layout = result + .get(&container_id) + .expect("Root container should have layout"); + + // LayoutEngine corrects root positions after Taffy computation + assert_eq!(layout.x, 100.0, "Root container x from schema"); + assert_eq!(layout.y, 50.0, "Root container y from schema"); + assert_eq!(layout.width, 200.0); + assert_eq!(layout.height, 150.0); + } + + #[test] + fn test_root_node_always_gets_layout_even_if_marked_absolute() { + // Verify that root nodes always participate in layout, + // even if they somehow have layout_child with Absolute positioning + use crate::cg::types::LayoutPositioning; + use crate::node::schema::LayoutChildStyle; + + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create a root rectangle marked as "absolute" (shouldn't matter for roots) + let mut rect = nf.create_rectangle_node(); + rect.size = Size { + width: 200.0, + height: 150.0, + }; + rect.transform = AffineTransform::new(100.0, 50.0, 0.0); + rect.layout_child = Some(LayoutChildStyle { + layout_positioning: LayoutPositioning::Absolute, + layout_grow: 0.0, + }); + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); + + // Compute layout + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Root node MUST get layout result, even if marked absolute + let layout = result + .get(&rect_id) + .expect("Root node must ALWAYS have layout result, even if marked absolute"); + + // Verify it has its dimensions + assert_eq!(layout.width, 200.0); + assert_eq!(layout.height, 150.0); + } + + #[test] + fn test_mixed_absolute_and_relative_children() { + // Complex scenario: flex container with mix of absolute and relative children + use crate::cg::types::LayoutPositioning; + use crate::node::schema::LayoutChildStyle; + + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create flex container + let mut container = nf.create_container_node(); + container.layout_container = LayoutContainerStyle { + layout_mode: LayoutMode::Flex, + layout_direction: Axis::Vertical, + layout_gap: Some(LayoutGap::uniform(20.0)), + ..Default::default() + }; + container.layout_dimensions.width = Some(300.0); + container.layout_dimensions.height = Some(500.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Relative child 1 + let mut rect1 = nf.create_rectangle_node(); + rect1.size = Size { + width: 100.0, + height: 50.0, + }; + let child1_id = graph.append_child(Node::Rectangle(rect1), Parent::NodeId(container_id)); + + // Absolute child (should be excluded) + let mut rect2 = nf.create_rectangle_node(); + rect2.size = Size { + width: 80.0, + height: 80.0, + }; + rect2.layout_child = Some(LayoutChildStyle { + layout_positioning: LayoutPositioning::Absolute, + layout_grow: 0.0, + }); + let child2_id = graph.append_child(Node::Rectangle(rect2), Parent::NodeId(container_id)); + + // Relative child 2 + let mut rect3 = nf.create_rectangle_node(); + rect3.size = Size { + width: 100.0, + height: 50.0, + }; + let child3_id = graph.append_child(Node::Rectangle(rect3), Parent::NodeId(container_id)); + + // Absolute child (should be excluded) + let mut rect4 = nf.create_rectangle_node(); + rect4.size = Size { + width: 60.0, + height: 60.0, + }; + rect4.layout_child = Some(LayoutChildStyle { + layout_positioning: LayoutPositioning::Absolute, + layout_grow: 0.0, + }); + let child4_id = graph.append_child(Node::Rectangle(rect4), Parent::NodeId(container_id)); + + // Relative child 3 + let mut rect5 = nf.create_rectangle_node(); + rect5.size = Size { + width: 100.0, + height: 50.0, + }; + let child5_id = graph.append_child(Node::Rectangle(rect5), Parent::NodeId(container_id)); + + // Compute layout + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify all children get layout results (Taffy handles both relative and absolute) + assert!( + result.get(&child1_id).is_some(), + "Relative child 1 should have layout" + ); + assert!( + result.get(&child2_id).is_some(), + "Absolute child 2 should have layout (Taffy positions it)" + ); + assert!( + result.get(&child3_id).is_some(), + "Relative child 3 should have layout" + ); + assert!( + result.get(&child4_id).is_some(), + "Absolute child 4 should have layout (Taffy positions it)" + ); + assert!( + result.get(&child5_id).is_some(), + "Relative child 5 should have layout" + ); + + // Verify vertical layout for relative children (absolute children don't affect flex flow) + let layout1 = result.get(&child1_id).unwrap(); + let layout3 = result.get(&child3_id).unwrap(); + let layout5 = result.get(&child5_id).unwrap(); + + // Vertical positioning: y=0, y=70 (50+20), y=140 (50+20+50+20) + // Absolute children are ignored in flex flow calculations + assert_eq!(layout1.y, 0.0, "First relative child at y=0"); + assert_eq!( + layout3.y, 70.0, + "Second relative child at y=70 (50 + 20 gap, ignoring absolute in flow)" + ); + assert_eq!( + layout5.y, 140.0, + "Third relative child at y=140 (50 + 20 + 50 + 20, ignoring absolute in flow)" + ); + } + + #[test] + fn test_root_positioning_integration() { + // Test: SceneGraph + LayoutEngine + GeometryCache integration + // Verifies root containers and rectangles respect schema positions + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Root container at (100, 50) + let mut container = nf.create_container_node(); + container.position = LayoutPositioningBasis::Cartesian(CGPoint::new(100.0, 50.0)); + container.layout_dimensions.width = Some(200.0); + container.layout_dimensions.height = Some(100.0); + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + // Root rectangle at (300, 150) + let mut rect = nf.create_rectangle_node(); + rect.transform = AffineTransform::new(300.0, 150.0, 0.0); + rect.size = Size { + width: 100.0, + height: 80.0, + }; + let rect_id = graph.append_child(Node::Rectangle(rect), Parent::Root); + + let scene = Scene { + name: "test".to_string(), + graph, + background_color: None, + }; + + // Compute layout + let mut engine = LayoutEngine::new(); + let layout_result = engine.compute( + &scene, + Size { + width: 800.0, + height: 600.0, + }, + ); + + // Verify: Layout results have corrected positions + let container_layout = layout_result.get(&container_id).unwrap(); + assert_eq!(container_layout.x, 100.0, "Root container x from schema"); + assert_eq!(container_layout.y, 50.0, "Root container y from schema"); + + let rect_layout = layout_result.get(&rect_id).unwrap(); + assert_eq!(rect_layout.x, 300.0, "Root rectangle x from schema"); + assert_eq!(rect_layout.y, 150.0, "Root rectangle y from schema"); + + // Verify: GeometryCache uses corrected positions + use crate::cache::paragraph::ParagraphCache; + use crate::resources::ByteStore; + use crate::runtime::font_repository::FontRepository; + use std::sync::{Arc, Mutex}; + + let store = Arc::new(Mutex::new(ByteStore::new())); + let fonts = FontRepository::new(store); + let mut para_cache = ParagraphCache::new(); + let geom = crate::cache::geometry::GeometryCache::from_scene_with_layout( + &scene, + &mut para_cache, + &fonts, + Some(layout_result), + Size { + width: 800.0, + height: 600.0, + }, + ); + + let container_transform = geom.get_transform(&container_id).unwrap(); + assert_eq!(container_transform.x(), 100.0); + assert_eq!(container_transform.y(), 50.0); + + let rect_transform = geom.get_transform(&rect_id).unwrap(); + assert_eq!(rect_transform.x(), 300.0); + assert_eq!(rect_transform.y(), 150.0); + } + + #[test] + fn test_svgpath_positioning() { + // Verify that SVGPath nodes without layout_child are positioned using their transform + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + + // Create an SVGPath node with transform coordinates + let mut svgpath = nf.create_path_node(); + svgpath.data = "M 0 0 L 100 0 L 100 100 L 0 100 Z".to_string(); + svgpath.transform = AffineTransform::new(200.0, 150.0, 0.0); + // layout_child is None by default + + let svgpath_id = graph.append_child(Node::SVGPath(svgpath), Parent::Root); + + let scene = Scene { + name: "SVGPath positioning test".to_string(), + graph, + background_color: None, + }; + + // Compute layout + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 1000.0, + height: 1000.0, + }, + ); + + // Verify position is correct + let layout = result.get(&svgpath_id).expect("SVGPath should have layout"); + assert_eq!( + layout.x, 200.0, + "SVGPath should be positioned at transform.x" + ); + assert_eq!( + layout.y, 150.0, + "SVGPath should be positioned at transform.y" + ); + } + + #[test] + fn test_vector_positioning() { + // Verify that Vector nodes without layout_child are positioned using their transform + use crate::node::schema::{LayerEffects, VectorNodeRec}; + use crate::vectornetwork::{VectorNetwork, VectorNetworkSegment}; + let mut graph = SceneGraph::new(); + + // Create a Vector node with transform coordinates + let vector_node = VectorNodeRec { + active: true, + opacity: 1.0, + blend_mode: crate::cg::types::LayerBlendMode::default(), + mask: None, + effects: LayerEffects::default(), + transform: AffineTransform::new(300.0, 250.0, 0.0), + network: VectorNetwork { + vertices: vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)], + segments: vec![ + VectorNetworkSegment::ab(0, 1), + VectorNetworkSegment::ab(1, 2), + VectorNetworkSegment::ab(2, 3), + VectorNetworkSegment::ab(3, 0), + ], + regions: vec![], + }, + corner_radius: 0.0, + fills: crate::cg::types::Paints::default(), + strokes: crate::cg::types::Paints::default(), + stroke_width: 0.0, + stroke_width_profile: None, + stroke_align: crate::cg::types::StrokeAlign::Inside, + stroke_dash_array: None, + layout_child: None, + }; + + let vector_id = graph.append_child(Node::Vector(vector_node), Parent::Root); + + let scene = Scene { + name: "Vector positioning test".to_string(), + graph, + background_color: None, + }; + + // Compute layout + let mut engine = LayoutEngine::new(); + let result = engine.compute( + &scene, + Size { + width: 1000.0, + height: 1000.0, + }, + ); + + // Verify position is correct + let layout = result.get(&vector_id).expect("Vector should have layout"); + assert_eq!( + layout.x, 300.0, + "Vector should be positioned at transform.x" + ); + assert_eq!( + layout.y, 250.0, + "Vector should be positioned at transform.y" + ); + } +} diff --git a/crates/grida-canvas/src/layout/into_taffy.rs b/crates/grida-canvas/src/layout/into_taffy.rs index 3c33077e02..a17c1806a3 100644 --- a/crates/grida-canvas/src/layout/into_taffy.rs +++ b/crates/grida-canvas/src/layout/into_taffy.rs @@ -1,6 +1,34 @@ use crate::cg::types::*; +use crate::node::scene_graph::SceneGraph; +use crate::node::schema::{LayoutPositioningBasis, Node, NodeId, NodeRectMixin, UniformNodeLayout}; +use math2::transform::AffineTransform; use taffy::prelude::*; +/// Create a Taffy Style with Grida's preferred defaults. +/// +/// ## Key differences from Taffy's defaults: +/// - `flex_shrink: 0.0` (instead of 1.0) - prevents children from automatically shrinking +/// when they overflow their flex container +/// +/// ## Rationale: +/// In design tools like Grida, users expect fixed-size elements to maintain their specified +/// dimensions. Taffy's default `flex_shrink: 1.0` causes elements to shrink when the container +/// is too small, which is unexpected behavior for a design canvas. Users should explicitly +/// opt-in to shrinking behavior if needed. +/// +/// This is a zero-cost abstraction - the compiler inlines this function. +#[inline] +fn grida_style_default() -> Style { + Style { + flex_shrink: 0.0, + overflow: taffy::Point { + x: taffy::Overflow::Clip, + y: taffy::Overflow::Clip, + }, + ..Style::default() + } +} + /// Convert schema Axis to Taffy FlexDirection impl From for FlexDirection { fn from(axis: Axis) -> Self { @@ -48,6 +76,19 @@ impl From for AlignItems { } } +/// Convert schema CrossAxisAlignment to Taffy AlignContent +/// This controls how wrapped flex lines are aligned in the cross axis +impl From for AlignContent { + fn from(alignment: CrossAxisAlignment) -> Self { + match alignment { + CrossAxisAlignment::Start => AlignContent::Start, + CrossAxisAlignment::End => AlignContent::End, + CrossAxisAlignment::Center => AlignContent::Center, + CrossAxisAlignment::Stretch => AlignContent::Stretch, + } + } +} + /// Convert schema EdgeInsets to Taffy Rect impl From for Rect { fn from(insets: EdgeInsets) -> Self { @@ -60,13 +101,433 @@ impl From for Rect { } } -/// Convert schema LayoutGap to Taffy Size -/// Note: In Taffy, gap.width is the main axis gap and gap.height is the cross axis gap -impl From for Size { - fn from(gap: LayoutGap) -> Self { - Size { - width: LengthPercentage::length(gap.main_axis_gap), - height: LengthPercentage::length(gap.cross_axis_gap), +/// Convert schema LayoutGap to Taffy gap based on flex direction +/// +/// **IMPORTANT**: Taffy's gap is absolute (not direction-relative): +/// - `gap.width` = column-gap (horizontal spacing) +/// - `gap.height` = row-gap (vertical spacing) +/// +/// Our LayoutGap is direction-relative (main/cross), so we need the flex direction +/// to map correctly. This function should NOT be used directly - use `layout_gap_to_taffy()` +/// with the direction parameter instead. +fn layout_gap_to_taffy(gap: LayoutGap, direction: Axis) -> Size { + match direction { + Axis::Horizontal => { + // Horizontal flex: main=horizontal, cross=vertical + Size { + width: LengthPercentage::length(gap.main_axis_gap), // column-gap + height: LengthPercentage::length(gap.cross_axis_gap), // row-gap + } + } + Axis::Vertical => { + // Vertical flex: main=vertical, cross=horizontal + Size { + width: LengthPercentage::length(gap.cross_axis_gap), // column-gap + height: LengthPercentage::length(gap.main_axis_gap), // row-gap + } + } + } +} + +impl From for taffy::Position { + fn from(position: LayoutPositioning) -> Self { + match position { + LayoutPositioning::Auto => taffy::Position::Relative, + LayoutPositioning::Absolute => taffy::Position::Absolute, + } + } +} + +impl From for Rect { + fn from(position: LayoutPositioningBasis) -> Self { + match position { + LayoutPositioningBasis::Cartesian(point) => Rect { + left: LengthPercentageAuto::length(point.x), + right: LengthPercentageAuto::auto(), + top: LengthPercentageAuto::length(point.y), + bottom: LengthPercentageAuto::auto(), + }, + LayoutPositioningBasis::Inset(inset) => Rect { + left: LengthPercentageAuto::length(inset.left), + right: LengthPercentageAuto::length(inset.right), + top: LengthPercentageAuto::length(inset.top), + bottom: LengthPercentageAuto::length(inset.bottom), + }, + LayoutPositioningBasis::Anchored => { + unreachable!("Anchored positioning is not supported") + } + } + } +} + +/// Convert schema LayoutStyle to Taffy Style +/// This provides a predictable, comprehensive mapping from our layout system to Taffy +impl From for Style { + fn from(layout: UniformNodeLayout) -> Self { + let mut style = grida_style_default(); + + // Handle layout mode - only apply flex properties if it's a flex container + match layout.layout_mode { + LayoutMode::Flex => { + // Flex direction + style.flex_direction = layout.layout_direction.into(); + + // Flex wrap + if let Some(wrap) = layout.layout_wrap { + style.flex_wrap = wrap.into(); + } + + // Main axis alignment (justify content) + if let Some(alignment) = layout.layout_main_axis_alignment { + style.justify_content = Some(alignment.into()); + } + + // Cross axis alignment (align items) + if let Some(alignment) = layout.layout_cross_axis_alignment { + style.align_items = Some(alignment.into()); + } + + // align_content: Controls how wrapped flex lines are aligned in the cross axis + // Only relevant when flex-wrap is enabled + if layout.layout_wrap == Some(LayoutWrap::Wrap) { + if let Some(alignment) = layout.layout_cross_axis_alignment { + // User specified alignment - use it for line distribution + style.align_content = Some(alignment.into()); + } else { + // No alignment specified - use Start to prevent Taffy's default + // stretch behavior from expanding gaps between wrapped lines + style.align_content = Some(AlignContent::Start); + } + } + + // Gap - convert with direction awareness + if let Some(gap) = layout.layout_gap { + style.gap = layout_gap_to_taffy(gap, layout.layout_direction); + } + + // Flex grow + if let Some(grow) = layout.layout_grow { + style.flex_grow = grow; + } + } + LayoutMode::Normal => { + // For normal layout mode, we don't set flex properties + // This allows Taffy to use its default block layout behavior + } + } + + // Size constraints + style.size = Size { + width: if let Some(w) = layout.width { + if w > 0.0 { + Dimension::length(w) + } else { + Dimension::auto() + } + } else { + Dimension::auto() + }, + height: if let Some(h) = layout.height { + if h > 0.0 { + Dimension::length(h) + } else { + Dimension::auto() + } + } else { + Dimension::auto() + }, + }; + + // Min/Max size constraints + if let Some(min_w) = layout.min_width { + style.min_size.width = Dimension::length(min_w); + } + if let Some(max_w) = layout.max_width { + style.max_size.width = Dimension::length(max_w); + } + if let Some(min_h) = layout.min_height { + style.min_size.height = Dimension::length(min_h); + } + if let Some(max_h) = layout.max_height { + style.max_size.height = Dimension::length(max_h); + } + + // Padding + if let Some(padding) = layout.layout_padding { + style.padding = padding.into(); + } + + // Position - Taffy handles positioning automatically + style.inset = layout.position.into(); + style.position = layout.layout_positioning.into(); + + style + } +} + +/// Universal node style mapping - converts any node type to Taffy Style +/// +/// This is the central entry point for converting scene graph nodes to Taffy styles. +/// All node types are handled here, providing a unified approach to layout computation. +pub fn node_to_taffy_style(node: &Node, _graph: &SceneGraph, _node_id: &NodeId) -> Style { + match node { + Node::Container(n) => n.into(), + Node::InitialContainer(n) => n.into(), + Node::Rectangle(n) => n.into(), + Node::Ellipse(n) => n.into(), + Node::TextSpan(n) => n.into(), + Node::Image(n) => n.into(), + Node::Line(n) => n.into(), + Node::Polygon(n) => n.into(), + Node::RegularPolygon(n) => n.into(), + Node::RegularStarPolygon(n) => n.into(), + Node::Vector(n) => n.into(), + Node::SVGPath(n) => n.into(), + Node::Error(n) => Style { + size: Size { + width: Dimension::length(n.size.width), + height: Dimension::length(n.size.height), + }, + ..grida_style_default() + }, + Node::Group(_) => grida_style_default(), + Node::BooleanOperation(_) => grida_style_default(), + } +} + +/// Helper to apply layout_child properties to a style +/// Also requires the node's transform to set inset for absolute positioning +fn apply_layout_child( + mut style: Style, + layout_child: &Option, + transform: AffineTransform, +) -> Style { + if let Some(child_style) = layout_child { + style.position = child_style.layout_positioning.into(); + style.flex_grow = child_style.layout_grow; + + // For absolute positioning, set inset from transform + if child_style.layout_positioning == crate::cg::types::LayoutPositioning::Absolute { + style.inset = Rect { + left: LengthPercentageAuto::length(transform.x()), + top: LengthPercentageAuto::length(transform.y()), + right: LengthPercentageAuto::auto(), + bottom: LengthPercentageAuto::auto(), + }; + } + } + style +} + +/// Convert ContainerNodeRec to Taffy Style +impl From<&crate::node::schema::ContainerNodeRec> for Style { + fn from(container: &crate::node::schema::ContainerNodeRec) -> Self { + let mut style: Style = container.layout().into(); + + // Set display based on layout mode + style.display = if container.layout().layout_mode == LayoutMode::Flex { + Display::Flex + } else { + Display::Block + }; + + style + } +} + +/// Convert InitialContainerNodeRec to Taffy Style +impl From<&crate::node::schema::InitialContainerNodeRec> for Style { + fn from(icb: &crate::node::schema::InitialContainerNodeRec) -> Self { + Style { + display: Display::Flex, + flex_direction: icb.layout_direction.into(), + flex_wrap: icb.layout_wrap.into(), + justify_content: Some(icb.layout_main_axis_alignment.into()), + align_items: Some(icb.layout_cross_axis_alignment.into()), + align_content: Some(icb.layout_cross_axis_alignment.into()), + gap: layout_gap_to_taffy(icb.layout_gap, icb.layout_direction), + padding: icb.padding.into(), + // Size will be set by the layout engine for root ICB nodes + ..grida_style_default() } } } + +/// Convert RectangleNodeRec to Taffy Style +impl From<&crate::node::schema::RectangleNodeRec> for Style { + fn from(node: &crate::node::schema::RectangleNodeRec) -> Self { + let style = Style { + size: Size { + width: Dimension::length(node.size.width), + height: Dimension::length(node.size.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert EllipseNodeRec to Taffy Style +impl From<&crate::node::schema::EllipseNodeRec> for Style { + fn from(node: &crate::node::schema::EllipseNodeRec) -> Self { + let style = Style { + size: Size { + width: Dimension::length(node.size.width), + height: Dimension::length(node.size.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert ImageNodeRec to Taffy Style +impl From<&crate::node::schema::ImageNodeRec> for Style { + fn from(node: &crate::node::schema::ImageNodeRec) -> Self { + let style = Style { + size: Size { + width: Dimension::length(node.size.width), + height: Dimension::length(node.size.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert LineNodeRec to Taffy Style +impl From<&crate::node::schema::LineNodeRec> for Style { + fn from(node: &crate::node::schema::LineNodeRec) -> Self { + let style = Style { + size: Size { + width: Dimension::length(node.size.width), + height: Dimension::length(node.size.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert PolygonNodeRec to Taffy Style +impl From<&crate::node::schema::PolygonNodeRec> for Style { + fn from(node: &crate::node::schema::PolygonNodeRec) -> Self { + let bounds = node.rect(); + let style = Style { + size: Size { + width: Dimension::length(bounds.width), + height: Dimension::length(bounds.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert RegularPolygonNodeRec to Taffy Style +impl From<&crate::node::schema::RegularPolygonNodeRec> for Style { + fn from(node: &crate::node::schema::RegularPolygonNodeRec) -> Self { + let style = Style { + size: Size { + width: Dimension::length(node.size.width), + height: Dimension::length(node.size.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert RegularStarPolygonNodeRec to Taffy Style +impl From<&crate::node::schema::RegularStarPolygonNodeRec> for Style { + fn from(node: &crate::node::schema::RegularStarPolygonNodeRec) -> Self { + let style = Style { + size: Size { + width: Dimension::length(node.size.width), + height: Dimension::length(node.size.height), + }, + ..grida_style_default() + }; + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert TextSpanNodeRec to Taffy Style +impl From<&crate::node::schema::TextSpanNodeRec> for Style { + fn from(node: &crate::node::schema::TextSpanNodeRec) -> Self { + let mut style = grida_style_default(); + + // Set width if specified, otherwise auto + if let Some(width) = node.width { + style.size.width = Dimension::length(width); + } else { + style.size.width = Dimension::auto(); + } + + // Height is auto for text (will be determined by content) + style.size.height = Dimension::auto(); + + apply_layout_child(style, &node.layout_child, node.transform) + } +} + +/// Convert VectorNodeRec to Taffy Style +impl From<&crate::node::schema::VectorNodeRec> for Style { + fn from(node: &crate::node::schema::VectorNodeRec) -> Self { + let bounds = node.network.bounds(); + let mut style = Style { + size: Size { + width: Dimension::length(bounds.width), + height: Dimension::length(bounds.height), + }, + ..grida_style_default() + }; + + // Apply layout_child if present + style = apply_layout_child(style, &node.layout_child, node.transform); + + // If no layout_child is set, apply transform position as absolute positioning + if node.layout_child.is_none() { + style.position = Position::Absolute; + style.inset = Rect { + left: LengthPercentageAuto::length(node.transform.x()), + top: LengthPercentageAuto::length(node.transform.y()), + right: LengthPercentageAuto::auto(), + bottom: LengthPercentageAuto::auto(), + }; + } + + style + } +} + +/// Convert SVGPathNodeRec to Taffy Style +impl From<&crate::node::schema::SVGPathNodeRec> for Style { + fn from(node: &crate::node::schema::SVGPathNodeRec) -> Self { + let rect = node.rect(); + let mut style = Style { + size: Size { + width: Dimension::length(rect.width), + height: Dimension::length(rect.height), + }, + ..grida_style_default() + }; + + // Apply layout_child if present + style = apply_layout_child(style, &node.layout_child, node.transform); + + // If no layout_child is set, apply transform position as absolute positioning + // This ensures SVGPath nodes with transform coordinates are positioned correctly + if node.layout_child.is_none() { + style.position = Position::Absolute; + style.inset = Rect { + left: LengthPercentageAuto::length(node.transform.x()), + top: LengthPercentageAuto::length(node.transform.y()), + right: LengthPercentageAuto::auto(), + bottom: LengthPercentageAuto::auto(), + }; + } + + style + } +} diff --git a/crates/grida-canvas/src/layout/mod.rs b/crates/grida-canvas/src/layout/mod.rs index abbeceb001..473cb25864 100644 --- a/crates/grida-canvas/src/layout/mod.rs +++ b/crates/grida-canvas/src/layout/mod.rs @@ -1,71 +1,27 @@ -use taffy::prelude::*; - +pub mod cache; +pub mod engine; mod into_taffy; -pub mod tmp_example; - -/// Simplified layout style wrapper for Taffy -/// -/// This represents child-specific layout properties that can be applied -/// to any node participating in flex layout. -#[derive(Debug, Clone)] -pub struct LayoutStyle { - pub width: Dimension, - pub height: Dimension, - pub flex_grow: f32, -} - -impl Default for LayoutStyle { - fn default() -> Self { - Self { - width: Dimension::auto(), - height: Dimension::auto(), - flex_grow: 0.0, - } - } -} - -impl LayoutStyle { - /// Convert to Taffy Style with all container properties - /// - /// This method takes container-level properties as parameters since - /// they come from the parent container's schema properties. - pub fn to_taffy_style( - &self, - padding: Rect, - gap: Size, - flex_direction: FlexDirection, - flex_wrap: FlexWrap, - justify_content: Option, - align_items: Option, - ) -> Style { - Style { - display: Display::Flex, - flex_direction, - flex_wrap, - gap, - size: Size { - width: self.width, - height: self.height, - }, - padding, - justify_content, - align_items, - flex_grow: self.flex_grow, - flex_shrink: 1.0, - flex_basis: Dimension::auto(), - ..Default::default() - } - } -} +pub mod tree; /// Computed layout result containing position and size /// /// This is the output from Taffy's layout computation, representing /// the final position and dimensions of a laid-out element. -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct ComputedLayout { pub x: f32, pub y: f32, pub width: f32, pub height: f32, } + +impl From<&taffy::Layout> for ComputedLayout { + fn from(layout: &taffy::Layout) -> Self { + Self { + x: layout.location.x, + y: layout.location.y, + width: layout.size.width, + height: layout.size.height, + } + } +} diff --git a/crates/grida-canvas/src/layout/tmp_example.rs b/crates/grida-canvas/src/layout/tmp_example.rs deleted file mode 100644 index ca987ecb3c..0000000000 --- a/crates/grida-canvas/src/layout/tmp_example.rs +++ /dev/null @@ -1,325 +0,0 @@ -/// Temporary demo/example utilities for testing flex layout integration. -/// This module contains helper structs and functions that bridge between -/// schema types and Taffy layout engine for demonstration purposes. -/// -/// **This is NOT part of the production API and will be removed/refactored -/// once layout is fully integrated into the rendering pipeline.** -use super::{ComputedLayout, LayoutStyle}; -use crate::node::schema::ContainerNodeRec; -use taffy::prelude::*; -use taffy::TaffyTree; - -/// A container node enhanced with layout properties for demo/testing -/// -/// This struct combines schema container properties with temporary layout -/// style properties to facilitate incremental testing of layout features. -#[derive(Debug, Clone)] -pub struct ContainerWithStyle { - /// Base container properties from schema - pub container: ContainerNodeRec, - - /// Layout-specific properties (demo/testing) - pub layout: LayoutStyle, -} - -impl ContainerWithStyle { - /// Create from existing container with default layout - pub fn from_container(container: ContainerNodeRec) -> Self { - Self { - container, - layout: LayoutStyle::default(), - } - } - - /// Create with specific layout properties - pub fn with_layout(mut self, layout: LayoutStyle) -> Self { - self.layout = layout; - self - } - - /// Convert to Taffy style for layout computation - pub fn to_taffy_style(&self) -> taffy::Style { - self.layout.to_taffy_style( - self.padding(), - self.gap(), - self.direction(), - self.wrap(), - self.justify_content(), - self.align_items(), - ) - } - - /// Get container size for layout computation - pub fn available_size(&self) -> (f32, f32) { - (self.container.size.width, self.container.size.height) - } - - /// Get padding as Taffy Rect - pub fn padding(&self) -> Rect { - self.container.padding.into() - } - - /// Get gap as Taffy Size - pub fn gap(&self) -> Size { - self.container.layout_gap.into() - } - - /// Check if this container uses flex layout - pub fn is_flex_layout(&self) -> bool { - use crate::cg::types::LayoutMode; - matches!(self.container.layout_mode, LayoutMode::Flex) - } - - /// Get flex direction from container's axis - pub fn direction(&self) -> FlexDirection { - self.container.layout_direction.into() - } - - /// Get flex wrap from container's layout_wrap - pub fn wrap(&self) -> FlexWrap { - self.container.layout_wrap.into() - } - - /// Get justify content from container's main axis alignment - pub fn justify_content(&self) -> Option { - Some(self.container.layout_main_axis_alignment.into()) - } - - /// Get align items from container's cross axis alignment - pub fn align_items(&self) -> Option { - Some(self.container.layout_cross_axis_alignment.into()) - } - - /// Get container ID - /// Note: ID should be provided by the caller, not stored in the node - pub fn id(&self) -> crate::node::schema::NodeId { - // TODO: Remove this method - ID should come from graph traversal - 0 - } -} - -/// Compute flex layout for a container with style and its children -/// -/// **This is a temporary demo function.** In production, layout computation -/// will be integrated into the cache/geometry pipeline. -/// -/// # Arguments -/// * `container` - The container with layout style -/// * `children` - Vector of child containers with layout styles -/// -/// # Returns -/// A vector of computed layouts for each child, in the same order as the input -pub fn compute_flex_layout_for_container( - container: &ContainerWithStyle, - children: Vec<&ContainerWithStyle>, -) -> Vec { - let mut taffy: TaffyTree<()> = TaffyTree::new(); - - // Create child nodes - let child_nodes: Vec = children - .iter() - .map(|child| taffy.new_leaf(child.to_taffy_style()).unwrap()) - .collect(); - - // Create container node - let container_node = taffy - .new_with_children(container.to_taffy_style(), &child_nodes) - .unwrap(); - - // Compute layout - let (width, height) = container.available_size(); - let available_space = Size { - width: AvailableSpace::Definite(width), - height: AvailableSpace::Definite(height), - }; - - taffy - .compute_layout(container_node, available_space) - .unwrap(); - - // Extract computed layouts - child_nodes - .iter() - .map(|&node_id| { - let layout = taffy.layout(node_id).unwrap(); - ComputedLayout { - x: layout.location.x, - y: layout.location.y, - width: layout.size.width, - height: layout.size.height, - } - }) - .collect() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_simple_row_layout() { - use crate::cg::types::*; - use crate::node::schema::ContainerNodeRec; - - let container = ContainerWithStyle::from_container(ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: math2::transform::AffineTransform::identity(), - size: crate::node::schema::Size { - width: 300.0, - height: 100.0, - }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - stroke_dash_array: None, - effects: crate::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: crate::cg::types::LayoutMode::Flex, - layout_direction: crate::cg::types::Axis::Horizontal, - layout_wrap: crate::cg::types::LayoutWrap::NoWrap, - layout_main_axis_alignment: crate::cg::types::MainAxisAlignment::Start, - layout_cross_axis_alignment: crate::cg::types::CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap::default(), - }) - .with_layout(LayoutStyle { - width: Dimension::length(300.0), - height: Dimension::auto(), - ..Default::default() - }); - - let children: Vec = (0..3) - .map(|i| { - ContainerWithStyle::from_container(ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: math2::transform::AffineTransform::identity(), - size: crate::node::schema::Size { - width: 100.0, - height: 100.0, - }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - stroke_dash_array: None, - effects: crate::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: crate::cg::types::LayoutMode::Normal, - layout_direction: crate::cg::types::Axis::Horizontal, - layout_wrap: crate::cg::types::LayoutWrap::NoWrap, - layout_main_axis_alignment: crate::cg::types::MainAxisAlignment::Start, - layout_cross_axis_alignment: crate::cg::types::CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap::default(), - }) - .with_layout(LayoutStyle { - width: Dimension::length(100.0), - height: Dimension::length(100.0), - ..Default::default() - }) - }) - .collect(); - - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); - - assert_eq!(layouts.len(), 3); - assert_eq!(layouts[0].x, 0.0); - assert_eq!(layouts[1].x, 100.0); - assert_eq!(layouts[2].x, 200.0); - } - - #[test] - fn test_wrapping_layout() { - use crate::cg::types::*; - use crate::node::schema::ContainerNodeRec; - - let container = ContainerWithStyle::from_container(ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: math2::transform::AffineTransform::identity(), - size: crate::node::schema::Size { - width: 250.0, - height: 200.0, - }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - stroke_dash_array: None, - effects: crate::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: crate::cg::types::LayoutMode::Flex, - layout_direction: crate::cg::types::Axis::Horizontal, - layout_wrap: crate::cg::types::LayoutWrap::Wrap, - layout_main_axis_alignment: crate::cg::types::MainAxisAlignment::Start, - layout_cross_axis_alignment: crate::cg::types::CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap { - main_axis_gap: 10.0, - cross_axis_gap: 10.0, - }, - }) - .with_layout(LayoutStyle { - width: Dimension::length(250.0), - height: Dimension::auto(), - ..Default::default() - }); - - let children: Vec = (0..4) - .map(|i| { - ContainerWithStyle::from_container(ContainerNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: math2::transform::AffineTransform::identity(), - size: crate::node::schema::Size { - width: 100.0, - height: 100.0, - }, - corner_radius: RectangularCornerRadius::default(), - fills: Paints::new([]), - strokes: Paints::new([]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - stroke_dash_array: None, - effects: crate::node::schema::LayerEffects::default(), - clip: ContainerClipFlag::default(), - layout_mode: crate::cg::types::LayoutMode::Normal, - layout_direction: crate::cg::types::Axis::Horizontal, - layout_wrap: crate::cg::types::LayoutWrap::NoWrap, - layout_main_axis_alignment: crate::cg::types::MainAxisAlignment::Start, - layout_cross_axis_alignment: crate::cg::types::CrossAxisAlignment::Start, - padding: EdgeInsets::default(), - layout_gap: LayoutGap::default(), - }) - .with_layout(LayoutStyle { - width: Dimension::length(100.0), - height: Dimension::length(100.0), - ..Default::default() - }) - }) - .collect(); - - let layouts = compute_flex_layout_for_container(&container, children.iter().collect()); - - assert_eq!(layouts.len(), 4); - // First two should be on the same row - assert_eq!(layouts[0].y, layouts[1].y); - // Third and fourth should be on a new row - assert_eq!(layouts[2].y, layouts[3].y); - assert!(layouts[2].y > layouts[0].y); - } -} diff --git a/crates/grida-canvas/src/layout/tree.rs b/crates/grida-canvas/src/layout/tree.rs new file mode 100644 index 0000000000..a8275a7250 --- /dev/null +++ b/crates/grida-canvas/src/layout/tree.rs @@ -0,0 +1,345 @@ +use crate::node::schema::NodeId; +use std::collections::HashMap; +use taffy::prelude::*; + +/// Integration layer between SceneGraph and TaffyTree +/// +/// Maps our NodeId (u64) to Taffy's layout system, enabling +/// flex layout computation while preserving scene graph structure. +pub(crate) struct LayoutTree { + /// Taffy tree for layout computation + taffy: TaffyTree, + /// Map from our SceneGraph NodeId to Taffy's NodeId + scene_to_taffy: HashMap, + /// Reverse map from Taffy NodeId to SceneGraph NodeId + taffy_to_scene: HashMap, +} + +impl LayoutTree { + pub(crate) fn new() -> Self { + Self { + taffy: TaffyTree::new(), + scene_to_taffy: HashMap::new(), + taffy_to_scene: HashMap::new(), + } + } + + /// Create a leaf node in the layout tree + /// + /// Maps the scene node ID to a taffy node ID and returns it + pub(crate) fn new_leaf( + &mut self, + scene_node_id: NodeId, + style: Style, + ) -> Result { + let taffy_id = self.taffy.new_leaf(style)?; + + // Clean up any existing mapping for this scene_node_id + if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + self.taffy_to_scene.remove(&old_taffy_id); + } + + // Clean up any existing mapping for this taffy_id + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } + + Ok(taffy_id) + } + + /// Create a container node with children + /// + /// Maps the scene node ID to a taffy node ID and returns it + pub(crate) fn new_with_children( + &mut self, + scene_node_id: NodeId, + style: Style, + children: &[taffy::NodeId], + ) -> Result { + let taffy_id = self.taffy.new_with_children(style, children)?; + + // Clean up any existing mapping for this scene_node_id + if let Some(old_taffy_id) = self.scene_to_taffy.insert(scene_node_id, taffy_id) { + self.taffy_to_scene.remove(&old_taffy_id); + } + + // Clean up any existing mapping for this taffy_id + if let Some(old_scene_id) = self.taffy_to_scene.insert(taffy_id, scene_node_id) { + self.scene_to_taffy.remove(&old_scene_id); + } + + Ok(taffy_id) + } + + /// Compute layout for the tree + pub(crate) fn compute_layout( + &mut self, + root: taffy::NodeId, + available_space: Size, + ) -> Result<(), taffy::TaffyError> { + self.taffy.compute_layout(root, available_space) + } + + /// Get computed layout for a scene node + pub(crate) fn get_layout(&self, scene_node_id: &NodeId) -> Option<&Layout> { + self.scene_to_taffy + .get(scene_node_id) + .and_then(|taffy_id| self.taffy.layout(*taffy_id).ok()) + } + + /// Get the taffy NodeId for a scene NodeId + #[cfg(test)] + pub(crate) fn get_taffy_id(&self, scene_node_id: &NodeId) -> Option { + self.scene_to_taffy.get(scene_node_id).copied() + } + + /// Get the scene NodeId for a taffy NodeId + #[cfg(test)] + pub(crate) fn get_scene_id(&self, taffy_id: &taffy::NodeId) -> Option { + self.taffy_to_scene.get(taffy_id).copied() + } + + /// Clear the tree + #[allow(dead_code)] + pub(crate) fn clear(&mut self) { + self.taffy.clear(); + self.scene_to_taffy.clear(); + self.taffy_to_scene.clear(); + } + + /// Get number of nodes in the layout tree + #[allow(dead_code)] + pub(crate) fn len(&self) -> usize { + self.scene_to_taffy.len() + } + + /// Check if the layout tree is empty + #[allow(dead_code)] + pub(crate) fn is_empty(&self) -> bool { + self.scene_to_taffy.is_empty() + } + + /// Access the underlying taffy tree directly + /// + /// Use this when you need to perform operations not wrapped by LayoutTree + #[allow(dead_code)] + pub(crate) fn taffy(&self) -> &TaffyTree { + &self.taffy + } + + /// Mutable access to the underlying taffy tree + #[allow(dead_code)] + pub(crate) fn taffy_mut(&mut self) -> &mut TaffyTree { + &mut self.taffy + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_layout_tree_creation() { + let mut tree = LayoutTree::new(); + + // Create a simple leaf node + let node_id: NodeId = 1; + let style = Style { + size: Size { + width: Dimension::length(100.0), + height: Dimension::length(100.0), + }, + ..Default::default() + }; + + let taffy_id = tree.new_leaf(node_id, style).unwrap(); + assert!(tree.get_taffy_id(&node_id).is_some()); + assert_eq!(tree.get_taffy_id(&node_id).unwrap(), taffy_id); + } + + #[test] + fn test_layout_tree_with_children() { + let mut tree = LayoutTree::new(); + + // Create child nodes + let child1_id: NodeId = 1; + let child2_id: NodeId = 2; + let parent_id: NodeId = 3; + + let child_style = Style { + size: Size { + width: Dimension::length(50.0), + height: Dimension::length(50.0), + }, + ..Default::default() + }; + + let child1_taffy = tree.new_leaf(child1_id, child_style.clone()).unwrap(); + let child2_taffy = tree.new_leaf(child2_id, child_style).unwrap(); + + // Create parent with children + let parent_style = Style { + display: Display::Flex, + flex_direction: FlexDirection::Row, + size: Size { + width: Dimension::length(200.0), + height: Dimension::length(100.0), + }, + ..Default::default() + }; + + let parent_taffy = tree + .new_with_children(parent_id, parent_style, &[child1_taffy, child2_taffy]) + .unwrap(); + + // Compute layout + tree.compute_layout( + parent_taffy, + Size { + width: AvailableSpace::Definite(200.0), + height: AvailableSpace::Definite(100.0), + }, + ) + .unwrap(); + + // Verify we can retrieve layouts + let child1_layout = tree.get_layout(&child1_id).unwrap(); + let child2_layout = tree.get_layout(&child2_id).unwrap(); + + assert_eq!(child1_layout.size.width, 50.0); + assert_eq!(child1_layout.size.height, 50.0); + assert_eq!(child2_layout.size.width, 50.0); + assert_eq!(child2_layout.size.height, 50.0); + + // Children should be laid out horizontally + assert_eq!(child1_layout.location.x, 0.0); + assert_eq!(child2_layout.location.x, 50.0); + } + + #[test] + fn test_layout_tree_compute_noop() { + // Test with non-responsive styles (fixed sizes) + // This validates the integration works even though styles aren't responsive yet + let mut tree = LayoutTree::new(); + + let node_id: NodeId = 100; + let style = Style { + size: Size { + width: Dimension::length(300.0), + height: Dimension::length(200.0), + }, + ..Default::default() + }; + + let taffy_id = tree.new_leaf(node_id, style).unwrap(); + + tree.compute_layout( + taffy_id, + Size { + width: AvailableSpace::Definite(1000.0), + height: AvailableSpace::Definite(1000.0), + }, + ) + .unwrap(); + + let layout = tree.get_layout(&node_id).unwrap(); + + // Fixed size should remain unchanged regardless of available space + assert_eq!(layout.size.width, 300.0); + assert_eq!(layout.size.height, 200.0); + assert_eq!(layout.location.x, 0.0); + assert_eq!(layout.location.y, 0.0); + } + + #[test] + fn test_mapping_cleanup_on_reinsertion() { + let mut tree = LayoutTree::new(); + + let scene_id: NodeId = 1; + let style = Style { + size: Size { + width: Dimension::length(100.0), + height: Dimension::length(100.0), + }, + ..Default::default() + }; + + // First insertion + let taffy_id1 = tree.new_leaf(scene_id, style.clone()).unwrap(); + + // Verify initial mapping + assert_eq!(tree.get_taffy_id(&scene_id), Some(taffy_id1)); + assert_eq!(tree.get_scene_id(&taffy_id1), Some(scene_id)); + assert_eq!(tree.len(), 1); + + // Re-insert the same scene_id with a new style + let new_style = Style { + size: Size { + width: Dimension::length(200.0), + height: Dimension::length(200.0), + }, + ..Default::default() + }; + let taffy_id2 = tree.new_leaf(scene_id, new_style).unwrap(); + + // Verify the old taffy_id is no longer mapped + assert_eq!(tree.get_scene_id(&taffy_id1), None); + + // Verify the new taffy_id is correctly mapped + assert_eq!(tree.get_taffy_id(&scene_id), Some(taffy_id2)); + assert_eq!(tree.get_scene_id(&taffy_id2), Some(scene_id)); + + // Should still have only one mapping + assert_eq!(tree.len(), 1); + } + + #[test] + fn test_mapping_cleanup_with_different_scene_ids() { + let mut tree = LayoutTree::new(); + + let scene_id1: NodeId = 1; + let scene_id2: NodeId = 2; + let style = Style { + size: Size { + width: Dimension::length(100.0), + height: Dimension::length(100.0), + }, + ..Default::default() + }; + + // Create first mapping + let taffy_id1 = tree.new_leaf(scene_id1, style.clone()).unwrap(); + + // Create second mapping with different scene_id + let taffy_id2 = tree.new_leaf(scene_id2, style.clone()).unwrap(); + + // Verify both mappings exist + assert_eq!(tree.get_taffy_id(&scene_id1), Some(taffy_id1)); + assert_eq!(tree.get_taffy_id(&scene_id2), Some(taffy_id2)); + assert_eq!(tree.len(), 2); + + // Now re-insert scene_id2 with a new style (this should clean up the old mapping for scene_id2) + let new_style = Style { + size: Size { + width: Dimension::length(200.0), + height: Dimension::length(200.0), + }, + ..Default::default() + }; + let taffy_id3 = tree.new_leaf(scene_id2, new_style).unwrap(); + + // scene_id1 should still be mapped to taffy_id1 + assert_eq!(tree.get_taffy_id(&scene_id1), Some(taffy_id1)); + assert_eq!(tree.get_scene_id(&taffy_id1), Some(scene_id1)); + + // scene_id2 should now be mapped to taffy_id3 (not taffy_id2) + assert_eq!(tree.get_taffy_id(&scene_id2), Some(taffy_id3)); + assert_eq!(tree.get_scene_id(&taffy_id3), Some(scene_id2)); + + // taffy_id2 should no longer be mapped to anything + assert_eq!(tree.get_scene_id(&taffy_id2), None); + + // Should still have two mappings + assert_eq!(tree.len(), 2); + } +} diff --git a/crates/grida-canvas/src/node/factory.rs b/crates/grida-canvas/src/node/factory.rs index 972a14d94e..5a30e0c5f9 100644 --- a/crates/grida-canvas/src/node/factory.rs +++ b/crates/grida-canvas/src/node/factory.rs @@ -65,6 +65,7 @@ impl NodeFactory { stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, effects: LayerEffects::default(), + layout_child: None, } } @@ -87,6 +88,7 @@ impl NodeFactory { stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, corner_radius: None, + layout_child: None, } } @@ -107,6 +109,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, _data_stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, + layout_child: None, } } @@ -120,6 +123,7 @@ impl NodeFactory { effects: LayerEffects::default(), transform: AffineTransform::identity(), width: None, + layout_child: None, height: None, max_lines: None, ellipsis: None, @@ -150,23 +154,52 @@ impl NodeFactory { ContainerNodeRec { active: true, opacity: Self::DEFAULT_OPACITY, - blend_mode: LayerBlendMode::default(), + blend_mode: Default::default(), mask: None, - transform: AffineTransform::identity(), - size: Self::DEFAULT_SIZE, - corner_radius: RectangularCornerRadius::zero(), + rotation: 0.0, + position: Default::default(), + corner_radius: Default::default(), fills: Paints::new([Self::default_solid_paint(Self::DEFAULT_COLOR)]), - strokes: Paints::default(), + strokes: Default::default(), stroke_width: Self::DEFAULT_STROKE_WIDTH, stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, - effects: LayerEffects::default(), + effects: Default::default(), clip: true, - layout_mode: LayoutMode::default(), - layout_direction: Axis::default(), - layout_wrap: LayoutWrap::default(), - layout_main_axis_alignment: MainAxisAlignment::default(), - layout_cross_axis_alignment: CrossAxisAlignment::default(), + layout_container: LayoutContainerStyle { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + }, + layout_dimensions: LayoutDimensionStyle { + width: Some(Self::DEFAULT_SIZE.width), + height: Some(Self::DEFAULT_SIZE.height), + min_width: None, + max_width: None, + min_height: None, + max_height: None, + }, + layout_child: None, + } + } + + /// Creates a new initial container block (ICB) node + /// + /// ICB fills viewport. By default has Normal layout (no flex). + /// Set layout_mode to Flex to enable flex layout for children. + /// No visual properties - purely structural. + pub fn create_initial_container_node(&self) -> InitialContainerNodeRec { + InitialContainerNodeRec { + active: true, + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: LayoutWrap::NoWrap, + layout_main_axis_alignment: MainAxisAlignment::Start, + layout_cross_axis_alignment: CrossAxisAlignment::Start, padding: EdgeInsets::default(), layout_gap: LayoutGap::default(), } @@ -187,6 +220,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, + layout_child: None, } } @@ -207,6 +241,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, + layout_child: None, } } @@ -227,6 +262,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, + layout_child: None, } } @@ -245,6 +281,7 @@ impl NodeFactory { stroke_width: Self::DEFAULT_STROKE_WIDTH, stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, + layout_child: None, } } @@ -265,6 +302,7 @@ impl NodeFactory { stroke_align: Self::DEFAULT_STROKE_ALIGN, stroke_dash_array: None, image: ResourceRef::RID(String::new()), + layout_child: None, } } } diff --git a/crates/grida-canvas/src/node/repository.rs b/crates/grida-canvas/src/node/repository.rs index 5b0568f4a7..57f4c90abd 100644 --- a/crates/grida-canvas/src/node/repository.rs +++ b/crates/grida-canvas/src/node/repository.rs @@ -1,4 +1,4 @@ -use crate::node::schema::{Node, NodeId, NodeTrait}; +use crate::node::schema::{Node, NodeId}; use std::collections::HashMap; /// A repository for managing nodes with automatic ID indexing. diff --git a/crates/grida-canvas/src/node/scene_graph.rs b/crates/grida-canvas/src/node/scene_graph.rs index 3f42dc5a1d..817858b4b7 100644 --- a/crates/grida-canvas/src/node/scene_graph.rs +++ b/crates/grida-canvas/src/node/scene_graph.rs @@ -207,6 +207,22 @@ impl SceneGraph { &self.roots } + /// Check if a node is a root node + pub fn is_root(&self, id: &NodeId) -> bool { + self.roots.contains(id) + } + + /// Get the parent of a node + /// Returns None if the node is a root or not found + pub fn get_parent(&self, id: &NodeId) -> Option { + for (parent_id, children) in &self.links { + if children.contains(id) { + return Some(*parent_id); + } + } + None + } + // ------------------------------------------------------------------------- // Node Data Methods // ------------------------------------------------------------------------- @@ -349,10 +365,10 @@ impl Default for SceneGraph { #[cfg(test)] mod tests { use super::*; - use crate::node::schema::{ErrorNodeRec, NodeTrait, Size}; + use crate::node::schema::{ErrorNodeRec, Size}; use math2::transform::AffineTransform; - fn create_test_node(id: u64) -> Node { + fn create_test_node() -> Node { Node::Error(ErrorNodeRec { active: true, transform: AffineTransform::identity(), @@ -369,9 +385,9 @@ mod tests { fn test_scene_graph_basic() { let mut graph = SceneGraph::new(); - let node_a = create_test_node(1); - let node_b = create_test_node(2); - let node_c = create_test_node(3); + let node_a = create_test_node(); + let node_b = create_test_node(); + let node_c = create_test_node(); let id_a = graph.append_child(node_a, Parent::Root); let id_b = graph.append_child(node_b, Parent::NodeId(id_a.clone())); @@ -386,9 +402,9 @@ mod tests { fn test_add_child() { let mut graph = SceneGraph::new(); - let node_a = create_test_node(1); - let node_b = create_test_node(2); - let node_c = create_test_node(3); + let node_a = create_test_node(); + let node_b = create_test_node(); + let node_c = create_test_node(); // Create parent with one child first let id_a = graph.append_child(node_a, Parent::Root); @@ -407,10 +423,10 @@ mod tests { fn test_add_child_at() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_a.clone())); - let id_d = graph.append_child(create_test_node(4), Parent::Root); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_d = graph.append_child(create_test_node(), Parent::Root); // Insert id_d at index 1 in id_a's children (between id_b and id_c) graph.add_child_at(&id_a, id_d.clone(), 1).unwrap(); @@ -426,9 +442,9 @@ mod tests { fn test_remove_child() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_a.clone())); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); graph.remove_child(&id_a, &id_b).unwrap(); @@ -441,9 +457,9 @@ mod tests { fn test_roots() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let _id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_b.clone())); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let _id_c = graph.append_child(create_test_node(), Parent::NodeId(id_b.clone())); let roots = graph.roots(); assert_eq!(roots.len(), 1); @@ -454,9 +470,9 @@ mod tests { fn test_walk_preorder() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_a.clone())); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); let mut visited = Vec::new(); graph @@ -470,9 +486,9 @@ mod tests { fn test_walk_postorder() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_a.clone())); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); let mut visited = Vec::new(); graph @@ -486,9 +502,9 @@ mod tests { fn test_ancestors() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_b.clone())); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node(), Parent::NodeId(id_b.clone())); let ancestors = graph.ancestors(&id_c).unwrap(); assert_eq!(ancestors, vec![id_b, id_a]); @@ -498,9 +514,9 @@ mod tests { fn test_descendants() { let mut graph = SceneGraph::new(); - let id_a = graph.append_child(create_test_node(1), Parent::Root); - let id_b = graph.append_child(create_test_node(2), Parent::NodeId(id_a.clone())); - let id_c = graph.append_child(create_test_node(3), Parent::NodeId(id_b.clone())); + let id_a = graph.append_child(create_test_node(), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::NodeId(id_a.clone())); + let id_c = graph.append_child(create_test_node(), Parent::NodeId(id_b.clone())); let descendants = graph.descendants(&id_a).unwrap(); assert_eq!(descendants.len(), 2); @@ -518,7 +534,7 @@ mod tests { #[test] fn test_error_parent_not_found() { let mut graph = SceneGraph::new(); - let id_b = graph.append_child(create_test_node(2), Parent::Root); + let id_b = graph.append_child(create_test_node(), Parent::Root); let result = graph.add_child(&9999, id_b); assert!(matches!(result, Err(SceneGraphError::ParentNotFound(_)))); } @@ -526,7 +542,7 @@ mod tests { #[test] fn test_append_child_to_root() { let mut graph = SceneGraph::new(); - let node_a = create_test_node(1); + let node_a = create_test_node(); let id_a = graph.append_child(node_a, Parent::Root); assert_eq!(graph.roots().len(), 1); @@ -537,8 +553,8 @@ mod tests { #[test] fn test_append_child_to_parent() { let mut graph = SceneGraph::new(); - let parent = create_test_node(10); - let child = create_test_node(11); + let parent = create_test_node(); + let child = create_test_node(); let parent_id = graph.append_child(parent, Parent::Root); let child_id = graph.append_child(child, Parent::NodeId(parent_id.clone())); @@ -550,9 +566,9 @@ mod tests { #[test] fn test_append_multiple_children() { let mut graph = SceneGraph::new(); - let parent = create_test_node(10); - let child1 = create_test_node(21); - let child2 = create_test_node(22); + let parent = create_test_node(); + let child1 = create_test_node(); + let child2 = create_test_node(); let parent_id = graph.append_child(parent, Parent::Root); let child1_id = graph.append_child(child1, Parent::NodeId(parent_id.clone())); @@ -567,11 +583,7 @@ mod tests { #[test] fn test_append_children_to_root() { let mut graph = SceneGraph::new(); - let nodes = vec![ - create_test_node(1), - create_test_node(2), - create_test_node(3), - ]; + let nodes = vec![create_test_node(), create_test_node(), create_test_node()]; let ids = graph.append_children(nodes, Parent::Root); assert_eq!(graph.roots().len(), 3); @@ -584,14 +596,10 @@ mod tests { #[test] fn test_append_children_to_parent() { let mut graph = SceneGraph::new(); - let parent = create_test_node(10); + let parent = create_test_node(); let parent_id = graph.append_child(parent, Parent::Root); - let children_nodes = vec![ - create_test_node(21), - create_test_node(22), - create_test_node(23), - ]; + let children_nodes = vec![create_test_node(), create_test_node(), create_test_node()]; let child_ids = graph.append_children(children_nodes, Parent::NodeId(parent_id.clone())); assert_eq!(child_ids.len(), 3); @@ -617,9 +625,9 @@ mod tests { let id_b = 2; let id_c = 3; - let node_a = create_test_node(id_a); - let node_b = create_test_node(id_b); - let node_c = create_test_node(id_c); + let node_a = create_test_node(); + let node_b = create_test_node(); + let node_c = create_test_node(); let node_pairs = vec![(id_a, node_a), (id_b, node_b), (id_c, node_c)]; let mut links = HashMap::new(); @@ -649,10 +657,10 @@ mod tests { let id_b = 2; let id_c = 3; - let node_root = create_test_node(id_root); - let node_a = create_test_node(id_a); - let node_b = create_test_node(id_b); - let node_c = create_test_node(id_c); + let node_root = create_test_node(); + let node_a = create_test_node(); + let node_b = create_test_node(); + let node_c = create_test_node(); let node_pairs = vec![ (id_root, node_root), diff --git a/crates/grida-canvas/src/node/schema.rs b/crates/grida-canvas/src/node/schema.rs index 00a63a325a..43aad1b09a 100644 --- a/crates/grida-canvas/src/node/schema.rs +++ b/crates/grida-canvas/src/node/schema.rs @@ -77,7 +77,7 @@ pub struct StrokeStyle { pub stroke_dash_array: Option>, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct Size { pub width: f32, pub height: f32, @@ -162,11 +162,498 @@ pub struct UnknownNodeProperties { pub text_align_vertical: Option, } +/// Universal **Layout Model** — geometry-first, with layout as an optional feature. +/// +/// This structure defines a flexible, engine-agnostic layout model designed for +/// 2D scene graphs, editors, and design tools (like Grida). +/// It treats **geometry** (`x`, `y`, `width`, `height`) as the source of truth, +/// while **layout behavior** (constraints, flexbox, etc.) acts as a secondary feature. +/// +/// +/// ## Design Philosophy +/// +/// - **Geometry-first:** +/// Direct manipulation (drag, resize) writes to explicit coordinates. +/// Layout only runs when explicitly enabled or attached. +/// +/// - **Layout as a feature:** +/// Layout engines (constraint, flexbox, grid, etc.) are *plugins* rather than the primary model. +/// +/// - **Universal 2D:** +/// Designed to accommodate constraint layout (AutoLayout-style), +/// flow-based layout (CSS/Flexbox), and manual placement (absolute/anchored). +/// +/// +/// ## Supported Concepts +/// +/// - **Relative positioning** +/// - Child elements positioned relative to their parent or constraints. +/// - **Inset / constraint layout** +/// - Anchors and offsets similar to Android’s ConstraintLayout or iOS AutoLayout. +/// - Auto-resizing between opposing constraints (e.g., `left + right`). +/// - **Min/Max size** +/// - Optional bounds to clamp final computed size. +/// - **Flexbox model** +/// - Horizontal or vertical layout direction, wrapping, and alignment. +/// - **Padding and gap** +/// - Internal spacing and inter-item gaps (like CSS `padding` and `gap`). +/// +/// +/// ## Why not just the CSS Box Model? +/// +/// The CSS box model is layout-first — every element participates in flow, +/// and geometry is derived from layout. +/// This model is **the inverse**: geometry exists independently, and layout +/// is applied *optionally* as a feature. +/// +/// This allows: +/// - Direct manipulation on canvas (like Figma, Sketch, Grida Canvas) +/// - Partial layout (only certain containers auto-layout) +/// - Constraint-based resizing (anchors, aspect ratios) +/// - More intuitive runtime control for graphics tools +/// +#[derive(Debug, Clone)] +pub struct UniformNodeLayout { + // position + pub position: LayoutPositioningBasis, + + // dimensions + pub width: Option, + pub height: Option, + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, + + // layout container + pub layout_mode: LayoutMode, + pub layout_direction: Axis, + pub layout_wrap: Option, + pub layout_main_axis_alignment: Option, + pub layout_cross_axis_alignment: Option, + pub layout_padding: Option, + pub layout_gap: Option, + + // layout child + pub layout_positioning: LayoutPositioning, + pub layout_grow: Option, +} + +impl UniformNodeLayout { + /// Creates a new `LayoutStyle` with default values. + pub fn new() -> Self { + Self { + layout_mode: LayoutMode::Normal, + layout_positioning: LayoutPositioning::Auto, + position: LayoutPositioningBasis::Cartesian(CGPoint::default()), + width: None, + height: None, + min_width: None, + max_width: None, + min_height: None, + max_height: None, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + layout_grow: None, + } + } + + pub fn merge_from_container_style(mut self, container_style: LayoutContainerStyle) -> Self { + self.layout_mode = container_style.layout_mode; + self.layout_direction = container_style.layout_direction; + self.layout_wrap = container_style.layout_wrap; + self.layout_main_axis_alignment = container_style.layout_main_axis_alignment; + self.layout_cross_axis_alignment = container_style.layout_cross_axis_alignment; + self.layout_padding = container_style.layout_padding; + self.layout_gap = container_style.layout_gap; + self + } + + pub fn merge_from_child_style(mut self, child_style: Option) -> Self { + if let Some(child_style) = child_style { + self.layout_grow = Some(child_style.layout_grow); + self.layout_positioning = child_style.layout_positioning; + } + self + } + + pub fn merge_from_dimensions(mut self, dimensions: LayoutDimensionStyle) -> Self { + self.width = dimensions.width; + self.height = dimensions.height; + self.min_width = dimensions.min_width; + self.max_width = dimensions.max_width; + self.min_height = dimensions.min_height; + self.max_height = dimensions.max_height; + self + } + + /// Sets the layout mode. + pub fn with_layout_mode(mut self, mode: LayoutMode) -> Self { + self.layout_mode = mode; + self + } + + /// Sets the layout positioning. + pub fn with_layout_position(mut self, position: LayoutPositioning) -> Self { + self.layout_positioning = position; + self + } + + /// Sets both x and y position. + pub fn with_position(mut self, position: LayoutPositioningBasis) -> Self { + self.position = position; + self + } + + pub fn with_position_cartesian(mut self, x: f32, y: f32) -> Self { + self.position = LayoutPositioningBasis::Cartesian(CGPoint::new(x, y)); + self + } + + pub fn with_position_inset(mut self, inset: EdgeInsets) -> Self { + self.position = LayoutPositioningBasis::Inset(inset); + self + } + + /// Sets both width and height. + pub fn with_size(mut self, width: f32, height: f32) -> Self { + self.width = Some(width); + self.height = Some(height); + self + } + + /// Sets the layout direction. + pub fn with_layout_direction(mut self, direction: Axis) -> Self { + self.layout_direction = direction; + self + } + + /// Sets the layout wrap behavior. + pub fn with_layout_wrap(mut self, wrap: LayoutWrap) -> Self { + self.layout_wrap = Some(wrap); + self + } + + /// Sets the main axis alignment. + pub fn with_layout_main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self { + self.layout_main_axis_alignment = Some(alignment); + self + } + + /// Sets the cross axis alignment. + pub fn with_layout_cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self { + self.layout_cross_axis_alignment = Some(alignment); + self + } + + /// Sets the layout padding. + pub fn with_padding(mut self, padding: EdgeInsets) -> Self { + self.layout_padding = Some(padding); + self + } + + /// Sets the layout gap. + pub fn with_gap(mut self, gap: LayoutGap) -> Self { + self.layout_gap = Some(gap); + self + } + + /// Sets the layout grow factor. + pub fn with_layout_grow(mut self, grow: f32) -> Self { + self.layout_grow = Some(grow); + self + } +} + +impl Default for UniformNodeLayout { + fn default() -> Self { + Self::new() + } +} + +/// Layout properties that define how a **container** arranges its **children**. +/// +/// This struct represents the "parent-side" layout behavior — how a container organizes +/// and positions its child elements. It is conceptually separate from how a node behaves +/// **as a child** within its parent's layout (see [`LayoutChildStyle`]). +/// +/// ## Purpose +/// +/// `LayoutContainerStyle` defines the layout algorithm and spacing rules that a container +/// applies to its children. This includes: +/// - The layout mode (flex, grid, flow, etc.) +/// - Flexbox properties (direction, wrap, alignment) +/// - Spacing between children (gap, padding) +/// +/// ## When to Use +/// +/// Apply this style to nodes that **contain** other nodes and need to control their layout. +/// Examples: +/// - A flex container arranging buttons horizontally +/// - A vertical list of cards with gaps +/// - A grid of images with padding +/// +/// ## Relationship with LayoutChildStyle +/// +/// - **`LayoutContainerStyle`**: "How should I lay out my children?" +/// - **[`LayoutChildStyle`]**: "How should I behave as a child in my parent's layout?" +/// +/// A single node can have both: +/// - As a **parent**: Uses its `LayoutContainerStyle` to arrange children +/// - As a **child**: Uses its `LayoutChildStyle` to participate in parent's layout +/// +/// ## Example +/// +/// ```rust,ignore +/// // A horizontal flex container with gaps +/// LayoutContainerStyle { +/// layout_mode: LayoutMode::Flex, +/// layout_direction: Axis::Horizontal, +/// layout_wrap: Some(LayoutWrap::Wrap), +/// layout_main_axis_alignment: Some(MainAxisAlignment::Center), +/// layout_cross_axis_alignment: Some(CrossAxisAlignment::Start), +/// layout_padding: Some(EdgeInsets::all(16.0)), +/// layout_gap: Some(LayoutGap::all(8.0)), +/// } +/// ``` +#[derive(Debug, Clone)] +pub struct LayoutContainerStyle { + /// The layout algorithm to use for arranging children. + /// + /// - `LayoutMode::Flex`: Children are arranged using flexbox rules + /// - `LayoutMode::Normal`: Children use default positioning (no layout engine) + pub layout_mode: LayoutMode, + + /// The primary axis direction for flex layout (horizontal or vertical). + /// + /// - `Axis::Horizontal`: Children flow left-to-right (or right-to-left in RTL) + /// - `Axis::Vertical`: Children flow top-to-bottom + /// + /// Only applies when `layout_mode` is `Flex`. + pub layout_direction: Axis, + + /// Whether flex children should wrap to a new line when they exceed the container width/height. + /// + /// - `Some(LayoutWrap::Wrap)`: Children wrap to next line/column + /// - `Some(LayoutWrap::NoWrap)`: All children stay on same line (may overflow) + /// - `None`: Uses default (NoWrap) + /// + /// Only applies when `layout_mode` is `Flex`. + pub layout_wrap: Option, + + /// How children should be aligned along the **main axis** (primary direction). + /// + /// Examples: + /// - `Start`: Pack children at the start + /// - `Center`: Center children along the main axis + /// - `SpaceBetween`: Distribute children with space between them + /// + /// Only applies when `layout_mode` is `Flex`. + pub layout_main_axis_alignment: Option, + + /// How children should be aligned along the **cross axis** (perpendicular direction). + /// + /// Examples: + /// - `Start`: Align children at the start of cross axis + /// - `Center`: Center children along the cross axis + /// - `Stretch`: Stretch children to fill cross axis + /// + /// Only applies when `layout_mode` is `Flex`. + pub layout_cross_axis_alignment: Option, + + /// Internal spacing (padding) between the container's edges and its children. + /// + /// Padding creates space inside the container, pushing children away from the edges. + /// Uses CSS-style edge insets (top, right, bottom, left). + /// + /// Example: `EdgeInsets::all(16.0)` adds 16px padding on all sides. + pub layout_padding: Option, + + /// Spacing between children (gap between items). + /// + /// - `main_axis_gap`: Space between children along the primary direction + /// - `cross_axis_gap`: Space between rows/columns (when wrapping) + /// + /// Unlike margin (which is per-child), gap is a container-level property that + /// uniformly spaces all children. + pub layout_gap: Option, +} + +impl Default for LayoutContainerStyle { + fn default() -> Self { + Self { + layout_mode: LayoutMode::Normal, + layout_direction: Axis::Horizontal, + layout_wrap: None, + layout_main_axis_alignment: None, + layout_cross_axis_alignment: None, + layout_padding: None, + layout_gap: None, + } + } +} + +/// Layout properties that define how a **child** behaves within its **parent's layout**. +/// +/// This struct represents the "child-side" layout behavior — how a node participates +/// and responds to its parent's layout algorithm. It is conceptually separate from +/// how a node arranges its own children (see [`LayoutContainerStyle`]). +/// +/// ## Purpose +/// +/// `LayoutChildStyle` defines how a child node should behave when placed inside a +/// layout container. This includes: +/// - Growth behavior (flex-grow) +/// - Positioning mode (absolute, relative, constraint-based) +/// - Constraint anchors for advanced positioning +/// +/// ## When to Use +/// +/// Apply this style to nodes that **are children** of a layout container and need to +/// control their participation in that layout. Examples: +/// - A button that should grow to fill available space +/// - A sidebar with fixed width while content area grows +/// - A child positioned absolutely within a flex container +/// +/// ## Relationship with LayoutContainerStyle +/// +/// - **[`LayoutContainerStyle`]**: "How should I lay out my children?" +/// - **`LayoutChildStyle`**: "How should I behave as a child in my parent's layout?" +/// +/// A single node can have both: +/// - As a **parent**: Uses its [`LayoutContainerStyle`] to arrange children +/// - As a **child**: Uses its `LayoutChildStyle` to participate in parent's layout +/// +/// ## Example +/// +/// ```rust,ignore +/// // A child that grows to fill available space +/// LayoutChildStyle { +/// layout_grow: 1.0, +/// layout_position: LayoutPositioning::Relative, +/// layout_constraints: LayoutConstraints::default(), +/// } +/// ``` +/// +/// ## Design Note +/// +/// This explicit separation (container vs child styles) aligns with CSS's model where: +/// - Container properties (`display: flex`, `flex-direction`, `gap`) affect children +/// - Child properties (`flex-grow`, `position`, `align-self`) affect the child itself +#[derive(Debug, Clone)] +pub struct LayoutChildStyle { + /// The flex growth factor — how much this child should grow relative to siblings. + /// + /// - `0.0` (default): Child doesn't grow beyond its initial size + /// - `1.0`: Child grows proportionally with other growing children + /// - `2.0`: Child grows twice as much as children with `1.0` + /// + /// Only applies when parent's `layout_mode` is `Flex` and there is available space. + /// + /// ## Example + /// + /// In a horizontal flex container with 3 children: + /// - Child A: `layout_grow: 0.0` → stays at minimum size + /// - Child B: `layout_grow: 1.0` → gets 1/3 of extra space + /// - Child C: `layout_grow: 2.0` → gets 2/3 of extra space + pub layout_grow: f32, + + /// How this child is positioned within its parent's coordinate space. + /// + /// - `Absolute`: Positioned at explicit coordinates, removed from layout flow + /// - `Relative`: Positioned by layout engine, with optional offset adjustments + /// + /// This is analogous to CSS's `position` property. + pub layout_positioning: LayoutPositioning, + /* + /// Constraint-based positioning anchors (left, right, top, bottom). + /// + /// Defines how this child is anchored to its parent's edges. Used for: + /// - Auto-sizing: Setting opposite constraints (e.g., `left + right`) makes width auto-computed + /// - Advanced positioning: Centering, edge alignment, or custom constraint layouts + /// + /// Similar to iOS AutoLayout or Android ConstraintLayout. + /// + /// ## Example + /// + /// ```rust,ignore + /// // Center child horizontally, anchor to top with 20px offset + /// LayoutConstraints { + /// horizontal: LayoutConstraintAnchor::Center, + /// vertical: LayoutConstraintAnchor::Start(20.0), + /// } + /// ``` + pub layout_constraints: LayoutConstraints, + */ +} + +#[derive(Debug, Clone)] +pub enum LayoutPositioningBasis { + /// Cartesian position mode is the default mode. + /// In this mode, the position is specified using x and y coordinates. + Cartesian(CGPoint), + /// Inset position mode is used when the position is specified using left, right, top, and bottom insets. + Inset(EdgeInsets), + /// Anchored position mode is used when the position is specified using left, right, top, and bottom insets. + /// In this mode, the position is specified using left, right, top, and bottom insets. + #[deprecated(note = "will be implemented later")] + Anchored, +} + +impl LayoutPositioningBasis { + pub fn zero() -> Self { + Self::Cartesian(CGPoint::zero()) + } + + pub fn x(&self) -> Option { + match self { + Self::Cartesian(point) => Some(point.x), + Self::Inset(inset) => Some(inset.left), + Self::Anchored => unreachable!("Anchored positioning is not supported"), + } + } + + pub fn y(&self) -> Option { + match self { + Self::Cartesian(point) => Some(point.y), + Self::Inset(inset) => Some(inset.top), + Self::Anchored => unreachable!("Anchored positioning is not supported"), + } + } +} + +impl From for LayoutPositioningBasis { + fn from(point: CGPoint) -> Self { + Self::Cartesian(point) + } +} + +impl Default for LayoutPositioningBasis { + fn default() -> Self { + Self::zero() + } +} + +#[derive(Debug, Clone)] +pub struct LayoutDimensionStyle { + pub width: Option, + pub height: Option, + pub min_width: Option, + pub max_width: Option, + pub min_height: Option, + pub max_height: Option, +} + #[derive(Debug, Clone)] pub enum Node { + InitialContainer(InitialContainerNodeRec), + Container(ContainerNodeRec), Error(ErrorNodeRec), Group(GroupNodeRec), - Container(ContainerNodeRec), Rectangle(RectangleNodeRec), Ellipse(EllipseNodeRec), Polygon(PolygonNodeRec), @@ -191,6 +678,7 @@ impl NodeTrait for Node { Node::Error(n) => n.active, Node::Group(n) => n.active, Node::Container(n) => n.active, + Node::InitialContainer(n) => n.active, Node::Rectangle(n) => n.active, Node::Ellipse(n) => n.active, Node::Polygon(n) => n.active, @@ -211,6 +699,7 @@ impl Node { match self { Node::Group(n) => n.mask, Node::Container(n) => n.mask, + Node::InitialContainer(_) => None, Node::Rectangle(n) => n.mask, Node::Ellipse(n) => n.mask, Node::Polygon(n) => n.mask, @@ -242,8 +731,11 @@ pub trait NodeTransformMixin { fn y(&self) -> f32; } +pub trait NodeLayoutChildMixin { + fn layout_child_style(&self) -> LayoutChildStyle; +} + pub trait NodeGeometryMixin { - fn rect(&self) -> Rectangle; /// if there is any valud stroke that should be taken into account for rendering, return true. /// stroke_width > 0.0 and at least one stroke with opacity > 0.0. fn has_stroke_geometry(&self) -> bool; @@ -251,28 +743,16 @@ pub trait NodeGeometryMixin { fn render_bounds_stroke_width(&self) -> f32; } +pub trait NodeRectMixin { + fn rect(&self) -> Rectangle; +} + pub trait NodeShapeMixin { fn to_shape(&self) -> Shape; fn to_path(&self) -> skia_safe::Path; fn to_vector_network(&self) -> VectorNetwork; } -/// Intrinsic size node is a node that has a fixed size, and can be rendered soley on its own. -#[derive(Debug, Clone)] -pub enum IntrinsicSizeNode { - Error(ErrorNodeRec), - Container(ContainerNodeRec), - Rectangle(RectangleNodeRec), - Ellipse(EllipseNodeRec), - Polygon(PolygonNodeRec), - RegularPolygon(RegularPolygonNodeRec), - RegularStarPolygon(RegularStarPolygonNodeRec), - Line(LineNodeRec), - SVGPath(SVGPathNodeRec), - Vector(VectorNodeRec), - Image(ImageNodeRec), -} - #[derive(Debug, Clone)] pub enum LeafNode { Error(ErrorNodeRec), @@ -355,8 +835,20 @@ pub struct ContainerNodeRec { pub blend_mode: LayerBlendMode, pub mask: Option, - pub transform: AffineTransform, - pub size: Size, + pub rotation: f32, + + /// positioning + pub position: LayoutPositioningBasis, + + /// layout style for the container. + pub layout_container: LayoutContainerStyle, + + /// Defines the width, height and its constraints + pub layout_dimensions: LayoutDimensionStyle, + + /// Layout style for this node when it is a child of a layout. + pub layout_child: Option, + pub corner_radius: RectangularCornerRadius, pub fills: Paints, pub strokes: Paints, @@ -379,29 +871,22 @@ pub struct ContainerNodeRec { /// This flag is intentionally equivalent to an **overflow/content** clip. /// If a future “shape clip (self + children)” is added, it will be modeled as a separate attribute. pub clip: ContainerClipFlag, - - // [container layout - common layout properties that is applicapable to the parent] - /// layout mode - pub layout_mode: LayoutMode, - /// layout direction - pub layout_direction: Axis, - /// layout wrap - pub layout_wrap: LayoutWrap, - /// layout main axis alignment - pub layout_main_axis_alignment: MainAxisAlignment, - /// layout cross axis alignment - pub layout_cross_axis_alignment: CrossAxisAlignment, - /// The gap of the container. - pub layout_gap: LayoutGap, - /// The padding of the container. - pub padding: EdgeInsets, } impl ContainerNodeRec { + /// Returns the effective layout style combining all layout-related fields. + pub fn layout(&self) -> UniformNodeLayout { + UniformNodeLayout::new() + .merge_from_container_style(self.layout_container.clone()) + .merge_from_child_style(self.layout_child.clone()) + .merge_from_dimensions(self.layout_dimensions.clone()) + .with_position(self.position.clone()) + } + pub fn to_own_shape(&self) -> RRectShape { RRectShape { - width: self.size.width, - height: self.size.height, + width: self.layout_dimensions.width.unwrap_or(0.0), + height: self.layout_dimensions.height.unwrap_or(0.0), corner_radius: self.corner_radius, } } @@ -417,26 +902,7 @@ impl NodeFillsMixin for ContainerNodeRec { } } -impl NodeTransformMixin for ContainerNodeRec { - fn x(&self) -> f32 { - self.transform.x() - } - - fn y(&self) -> f32 { - self.transform.y() - } -} - impl NodeGeometryMixin for ContainerNodeRec { - fn rect(&self) -> Rectangle { - Rectangle { - x: 0.0, - y: 0.0, - width: self.size.width, - height: self.size.height, - } - } - fn has_stroke_geometry(&self) -> bool { self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) } @@ -450,18 +916,25 @@ impl NodeGeometryMixin for ContainerNodeRec { } } -impl NodeShapeMixin for ContainerNodeRec { - fn to_shape(&self) -> Shape { - Shape::RRect(self.to_own_shape()) - } - - fn to_path(&self) -> skia_safe::Path { - build_rrect_path(&self.to_own_shape()) - } +/// Initial Container Block - Viewport-filling flex container +/// +/// Similar to `` in DOM. Fills viewport and positions direct children +/// using flex layout. Has no visual properties - purely structural. +/// +/// Direct children are positioned by layout engine (their transforms ignored). +/// Deeper descendants use schema geometry normally. +#[derive(Debug, Clone)] +pub struct InitialContainerNodeRec { + pub active: bool, - fn to_vector_network(&self) -> VectorNetwork { - build_rrect_vector_network(&self.to_own_shape()) - } + // Flex layout properties for children + pub layout_mode: LayoutMode, + pub layout_direction: Axis, + pub layout_wrap: LayoutWrap, + pub layout_main_axis_alignment: MainAxisAlignment, + pub layout_cross_axis_alignment: CrossAxisAlignment, + pub padding: EdgeInsets, + pub layout_gap: LayoutGap, } #[derive(Debug, Clone)] @@ -481,6 +954,9 @@ pub struct RectangleNodeRec { pub stroke_align: StrokeAlign, pub stroke_dash_array: Option>, pub effects: LayerEffects, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl RectangleNodeRec { @@ -513,7 +989,7 @@ impl NodeTransformMixin for RectangleNodeRec { } } -impl NodeGeometryMixin for RectangleNodeRec { +impl NodeRectMixin for RectangleNodeRec { fn rect(&self) -> Rectangle { Rectangle { x: 0.0, @@ -522,7 +998,9 @@ impl NodeGeometryMixin for RectangleNodeRec { height: self.size.height, } } +} +impl NodeGeometryMixin for RectangleNodeRec { fn has_stroke_geometry(&self) -> bool { self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) } @@ -565,6 +1043,9 @@ pub struct LineNodeRec { pub stroke_width: f32, pub _data_stroke_align: StrokeAlign, pub stroke_dash_array: Option>, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl LineNodeRec { @@ -603,6 +1084,9 @@ pub struct ImageNodeRec { pub stroke_align: StrokeAlign, pub stroke_dash_array: Option>, pub image: ResourceRef, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeStrokesMixin for ImageNodeRec { @@ -635,7 +1119,7 @@ impl NodeTransformMixin for ImageNodeRec { } } -impl NodeGeometryMixin for ImageNodeRec { +impl NodeRectMixin for ImageNodeRec { fn rect(&self) -> Rectangle { Rectangle { x: 0.0, @@ -644,7 +1128,9 @@ impl NodeGeometryMixin for ImageNodeRec { height: self.size.height, } } +} +impl NodeGeometryMixin for ImageNodeRec { fn has_stroke_geometry(&self) -> bool { self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) } @@ -698,6 +1184,9 @@ pub struct EllipseNodeRec { pub angle: Option, pub corner_radius: Option, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeFillsMixin for EllipseNodeRec { @@ -764,7 +1253,7 @@ impl NodeTransformMixin for EllipseNodeRec { } } -impl NodeGeometryMixin for EllipseNodeRec { +impl NodeRectMixin for EllipseNodeRec { fn rect(&self) -> Rectangle { Rectangle { x: 0.0, @@ -773,7 +1262,9 @@ impl NodeGeometryMixin for EllipseNodeRec { height: self.size.height, } } +} +impl NodeGeometryMixin for EllipseNodeRec { fn has_stroke_geometry(&self) -> bool { self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) } @@ -851,6 +1342,9 @@ pub struct VectorNodeRec { /// alignments are treated as `Center`. pub stroke_align: StrokeAlign, pub stroke_dash_array: Option>, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeFillsMixin for VectorNodeRec { @@ -917,6 +1411,9 @@ pub struct SVGPathNodeRec { pub stroke_width: f32, pub stroke_align: StrokeAlign, pub stroke_dash_array: Option>, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeFillsMixin for SVGPathNodeRec { @@ -949,6 +1446,32 @@ impl NodeTransformMixin for SVGPathNodeRec { } } +impl NodeRectMixin for SVGPathNodeRec { + /// Compute bounding rectangle from SVG path data + /// + /// **Performance Note**: This is NOT cached and involves parsing the SVG path string + /// and computing tight bounds via Skia. Avoid calling this in tight loops. + /// The result should be cached by the caller if needed repeatedly. + fn rect(&self) -> Rectangle { + if let Some(path) = skia_safe::path::Path::from_svg(&self.data) { + let bounds = path.compute_tight_bounds(); + Rectangle { + x: bounds.left(), + y: bounds.top(), + width: bounds.width(), + height: bounds.height(), + } + } else { + Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, + } + } + } +} + /// A polygon shape defined by a list of absolute 2D points, following the SVG `` model. /// /// ## Characteristics @@ -987,6 +1510,9 @@ pub struct PolygonNodeRec { pub stroke_width: f32, pub stroke_align: StrokeAlign, pub stroke_dash_array: Option>, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeFillsMixin for PolygonNodeRec { @@ -1009,6 +1535,26 @@ impl NodeStrokesMixin for PolygonNodeRec { } } +impl NodeRectMixin for PolygonNodeRec { + fn rect(&self) -> Rectangle { + polygon_bounds(&self.points) + } +} + +impl NodeGeometryMixin for PolygonNodeRec { + fn has_stroke_geometry(&self) -> bool { + self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) + } + + fn render_bounds_stroke_width(&self) -> f32 { + if self.has_stroke_geometry() { + self.stroke_width + } else { + 0.0 + } + } +} + impl PolygonNodeRec { pub fn to_own_shape(&self) -> SimplePolygonShape { SimplePolygonShape { @@ -1086,6 +1632,9 @@ pub struct RegularPolygonNodeRec { pub stroke_width: f32, pub stroke_align: StrokeAlign, pub stroke_dash_array: Option>, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeFillsMixin for RegularPolygonNodeRec { @@ -1108,7 +1657,7 @@ impl NodeTransformMixin for RegularPolygonNodeRec { } } -impl NodeGeometryMixin for RegularPolygonNodeRec { +impl NodeRectMixin for RegularPolygonNodeRec { fn rect(&self) -> Rectangle { Rectangle { x: 0.0, @@ -1117,7 +1666,9 @@ impl NodeGeometryMixin for RegularPolygonNodeRec { height: self.size.height, } } +} +impl NodeGeometryMixin for RegularPolygonNodeRec { fn has_stroke_geometry(&self) -> bool { self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) } @@ -1212,6 +1763,9 @@ pub struct RegularStarPolygonNodeRec { pub stroke_align: StrokeAlign, /// Overall node opacity (0.0–1.0) pub stroke_dash_array: Option>, + + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, } impl NodeFillsMixin for RegularStarPolygonNodeRec { @@ -1234,7 +1788,7 @@ impl NodeTransformMixin for RegularStarPolygonNodeRec { } } -impl NodeGeometryMixin for RegularStarPolygonNodeRec { +impl NodeRectMixin for RegularStarPolygonNodeRec { fn rect(&self) -> Rectangle { Rectangle { x: 0.0, @@ -1243,10 +1797,11 @@ impl NodeGeometryMixin for RegularStarPolygonNodeRec { height: self.size.height, } } +} +impl NodeGeometryMixin for RegularStarPolygonNodeRec { fn has_stroke_geometry(&self) -> bool { - // TODO: implement this - true + self.stroke_width > 0.0 && self.strokes.iter().any(|s| s.opacity() > 0.0) } fn render_bounds_stroke_width(&self) -> f32 { @@ -1300,6 +1855,9 @@ pub struct TextSpanNodeRec { /// Layout bounds (used for wrapping and alignment). pub width: Option, + /// Layout style for this node when it is a child of a layout container. + pub layout_child: Option, + /// Height of the text container box. /// /// This property defines the height of the "box" that contains the text paragraph. diff --git a/crates/grida-canvas/src/painter/geometry.rs b/crates/grida-canvas/src/painter/geometry.rs index ded28ad90a..8bbbb40f88 100644 --- a/crates/grida-canvas/src/painter/geometry.rs +++ b/crates/grida-canvas/src/painter/geometry.rs @@ -1,8 +1,19 @@ +//! Shape building for rendering +//! +//! ## Pipeline Guarantees +//! +//! This module guarantees: +//! - build_shape() requires resolved bounds from GeometryCache for all nodes +//! - V2 nodes (with auto-sizing) ALWAYS use provided bounds +//! - V1 nodes (fixed schema) use schema values (bounds parameter for future migration) +//! - Missing bounds when accessed is a PANIC (pipeline bug) + use crate::cg::types::*; use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; use crate::shape::*; use crate::{cache::geometry::GeometryCache, sk}; +use math2::rect::Rectangle; use math2::transform::AffineTransform; use skia_safe::{Path, RRect, Rect}; @@ -131,27 +142,31 @@ impl PainterShape { } } -pub fn build_shape(node: &IntrinsicSizeNode) -> PainterShape { +/// Build shape from node + resolved geometry +/// +/// All dimensions come from bounds (resolved by GeometryCache). +/// This ensures V2 auto-sized nodes and future migrations render correctly. +pub fn build_shape(node: &Node, bounds: &Rectangle) -> PainterShape { match node { - IntrinsicSizeNode::Polygon(n) => { + Node::Polygon(n) => { let shape = n.to_shape(); PainterShape::from_shape(&shape) } - IntrinsicSizeNode::RegularPolygon(n) => { + Node::RegularPolygon(n) => { let shape = n.to_shape(); PainterShape::from_shape(&shape) } - IntrinsicSizeNode::RegularStarPolygon(n) => { + Node::RegularStarPolygon(n) => { let shape = n.to_shape(); PainterShape::from_shape(&shape) } - IntrinsicSizeNode::Line(n) => { + Node::Line(n) => { let mut path = Path::new(); path.move_to((0.0, 0.0)); path.line_to((n.size.width, 0.0)); PainterShape::from_path(path) } - IntrinsicSizeNode::SVGPath(n) => { + Node::SVGPath(n) => { if let Some(path) = Path::from_svg(&n.data) { PainterShape::from_path(path) } else { @@ -159,15 +174,15 @@ pub fn build_shape(node: &IntrinsicSizeNode) -> PainterShape { PainterShape::from_rect(Rect::new(0.0, 0.0, 0.0, 0.0)) } } - IntrinsicSizeNode::Vector(n) => { + Node::Vector(n) => { let path = n.to_path(); PainterShape::from_path(path) } - IntrinsicSizeNode::Ellipse(n) => { + Node::Ellipse(n) => { let shape = n.to_shape(); PainterShape::from_shape(&shape) } - IntrinsicSizeNode::Rectangle(n) => { + Node::Rectangle(n) => { let rect = Rect::from_xywh(0.0, 0.0, n.size.width, n.size.height); let r = n.corner_radius; if !r.is_zero() { @@ -177,17 +192,27 @@ pub fn build_shape(node: &IntrinsicSizeNode) -> PainterShape { PainterShape::from_rect(rect) } } - IntrinsicSizeNode::Container(n) => { + Node::Container(n) => { + // ALWAYS use resolved bounds from GeometryCache + let width = bounds.width; + let height = bounds.height; + let r = n.corner_radius; if !r.is_zero() { - let rrect = build_rrect(&n.to_own_shape()); + // Build RRect with resolved dimensions + let shape = RRectShape { + width, + height, + corner_radius: n.corner_radius, + }; + let rrect = build_rrect(&shape); PainterShape::from_rrect(rrect) } else { - let rect = Rect::from_xywh(0.0, 0.0, n.size.width, n.size.height); + let rect = Rect::from_xywh(0.0, 0.0, width, height); PainterShape::from_rect(rect) } } - IntrinsicSizeNode::Image(n) => { + Node::Image(n) => { let r = n.corner_radius; if !r.is_zero() { let rrect = build_rrect(&n.to_own_shape()); @@ -197,10 +222,12 @@ pub fn build_shape(node: &IntrinsicSizeNode) -> PainterShape { PainterShape::from_rect(rect) } } - IntrinsicSizeNode::Error(n) => { + Node::Error(n) => { let rect = Rect::from_xywh(0.0, 0.0, n.size.width, n.size.height); PainterShape::from_rect(rect) } + // Non-shape nodes (Group, BooleanOperation, InitialContainer, TextSpan) + _ => PainterShape::from_rect(Rect::new(0.0, 0.0, 0.0, 0.0)), } } @@ -247,24 +274,6 @@ pub fn merge_shapes(shapes: &[(PainterShape, BooleanPathOperation)]) -> Path { result } -/// Build a [`PainterShape`] for a node if it has intrinsic geometry. -pub fn build_shape_from_node(node: &Node) -> Option { - match node { - Node::Rectangle(n) => Some(build_shape(&IntrinsicSizeNode::Rectangle(n.clone()))), - Node::Ellipse(n) => Some(build_shape(&IntrinsicSizeNode::Ellipse(n.clone()))), - Node::Polygon(n) => Some(build_shape(&IntrinsicSizeNode::Polygon(n.clone()))), - Node::RegularPolygon(n) => Some(build_shape(&IntrinsicSizeNode::RegularPolygon(n.clone()))), - Node::RegularStarPolygon(n) => Some(build_shape(&IntrinsicSizeNode::RegularStarPolygon( - n.clone(), - ))), - Node::Line(n) => Some(build_shape(&IntrinsicSizeNode::Line(n.clone()))), - Node::SVGPath(n) => Some(build_shape(&IntrinsicSizeNode::SVGPath(n.clone()))), - Node::Image(n) => Some(build_shape(&IntrinsicSizeNode::Image(n.clone()))), - Node::Error(n) => Some(build_shape(&IntrinsicSizeNode::Error(n.clone()))), - _ => None, - } -} - /// Compute the resulting path for a [`BooleanPathOperationNode`] in its local coordinate space. pub fn boolean_operation_path( id: &NodeId, @@ -286,7 +295,27 @@ pub fn boolean_operation_path( Node::BooleanOperation(child_bool) => { boolean_operation_path(child_id, child_bool, graph, cache)? } - _ => build_shape_from_node(child_node)?.to_path(), + _ => { + // Get bounds from geometry cache - guaranteed to exist + let bounds = cache + .get_world_bounds(child_id) + .expect("Geometry must exist for all nodes"); + let intrinsic = match child_node { + Node::Rectangle(n) => Node::Rectangle(n.clone()), + Node::Ellipse(n) => Node::Ellipse(n.clone()), + Node::Polygon(n) => Node::Polygon(n.clone()), + Node::RegularPolygon(n) => Node::RegularPolygon(n.clone()), + Node::RegularStarPolygon(n) => Node::RegularStarPolygon(n.clone()), + Node::Line(n) => Node::Line(n.clone()), + Node::SVGPath(n) => Node::SVGPath(n.clone()), + Node::Vector(n) => Node::Vector(n.clone()), + Node::Image(n) => Node::Image(n.clone()), + Node::Container(n) => Node::Container(n.clone()), + Node::Error(n) => Node::Error(n.clone()), + _ => return None, // Non-shape nodes + }; + build_shape(&intrinsic, &bounds).to_path() + } }; let child_world = cache diff --git a/crates/grida-canvas/src/painter/layer.rs b/crates/grida-canvas/src/painter/layer.rs index 6f26335d86..986a805541 100644 --- a/crates/grida-canvas/src/painter/layer.rs +++ b/crates/grida-canvas/src/painter/layer.rs @@ -287,7 +287,11 @@ impl LayerList { } Node::Container(n) => { let opacity = parent_opacity * n.opacity; - let shape = build_shape(&IntrinsicSizeNode::Container(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if !n.strokes.is_empty() && n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -327,6 +331,20 @@ impl LayerList { mask: n.mask, } } + Node::InitialContainer(_) => { + // ICB is invisible - only render children + let children = graph.get_children(id).map(|c| c.as_slice()).unwrap_or(&[]); + FlattenResult { + commands: Self::build_render_commands( + children, + graph, + scene_cache, + parent_opacity, + out, + ), + mask: None, + } + } Node::BooleanOperation(n) => { let opacity = parent_opacity * n.opacity; if let Some(shape) = boolean_operation_shape(id, n, graph, scene_cache.geometry()) { @@ -378,7 +396,11 @@ impl LayerList { } } Node::Rectangle(n) => { - let shape = build_shape(&IntrinsicSizeNode::Rectangle(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -414,7 +436,11 @@ impl LayerList { } } Node::Ellipse(n) => { - let shape = build_shape(&IntrinsicSizeNode::Ellipse(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -450,7 +476,11 @@ impl LayerList { } } Node::Polygon(n) => { - let shape = build_shape(&IntrinsicSizeNode::Polygon(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -486,7 +516,11 @@ impl LayerList { } } Node::RegularPolygon(n) => { - let shape = build_shape(&IntrinsicSizeNode::RegularPolygon(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -522,7 +556,11 @@ impl LayerList { } } Node::RegularStarPolygon(n) => { - let shape = build_shape(&IntrinsicSizeNode::RegularStarPolygon(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -558,7 +596,11 @@ impl LayerList { } } Node::Line(n) => { - let shape = build_shape(&IntrinsicSizeNode::Line(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -658,7 +700,11 @@ impl LayerList { } } Node::SVGPath(n) => { - let shape = build_shape(&IntrinsicSizeNode::SVGPath(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -694,7 +740,11 @@ impl LayerList { } } Node::Vector(n) => { - let shape = build_shape(&IntrinsicSizeNode::Vector(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let layer = PainterPictureLayer::Vector(PainterPictureVectorLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -724,7 +774,11 @@ impl LayerList { } } Node::Image(n) => { - let shape = build_shape(&IntrinsicSizeNode::Image(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let stroke_path = if n.stroke_width > 0.0 { Some(stroke_geometry( &shape.to_path(), @@ -762,7 +816,11 @@ impl LayerList { } } Node::Error(n) => { - let shape = build_shape(&IntrinsicSizeNode::Error(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let layer = PainterPictureLayer::Shape(PainterPictureShapeLayer { base: PainterPictureLayerBase { id: id.clone(), @@ -879,7 +937,11 @@ impl LayerList { .unwrap_or_else(AffineTransform::identity); // Build the shape and transform it relative to the current node - let shape = build_shape(&IntrinsicSizeNode::Container(n.clone())); + let bounds = scene_cache + .geometry() + .get_world_bounds(&id) + .expect("Geometry must exist"); + let shape = build_shape(node, &bounds); let mut path = shape.to_path(); let relative_transform = current_inv.compose(&world_transform); path.transform(&sk::sk_matrix(relative_transform.matrix)); diff --git a/crates/grida-canvas/src/painter/painter_debug_node.rs b/crates/grida-canvas/src/painter/painter_debug_node.rs index 63d69911bf..a249fec558 100644 --- a/crates/grida-canvas/src/painter/painter_debug_node.rs +++ b/crates/grida-canvas/src/painter/painter_debug_node.rs @@ -3,6 +3,16 @@ use crate::cache::geometry::GeometryCache; use crate::cg::types::*; use crate::node::scene_graph::SceneGraph; use crate::node::schema::*; +use math2::rect::Rectangle; + +/// Dummy bounds for V1 nodes that use schema sizing +/// V1 nodes ignore the bounds parameter and use their schema values +const DUMMY_BOUNDS: Rectangle = Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, +}; /// A painter specifically for drawing nodes, using the main Painter for operations. /// This separates node-specific drawing logic from the main Painter while maintaining @@ -20,7 +30,8 @@ impl<'a> NodePainter<'a> { /// Draw a RectangleNode, respecting its transform, effect, fill, stroke, blend mode, opacity pub fn draw_rect_node(&self, node: &RectangleNodeRec) { self.painter.with_transform(&node.transform.matrix, || { - let shape = build_shape(&IntrinsicSizeNode::Rectangle(node.clone())); + let node_enum = Node::Rectangle(node.clone()); + let shape = build_shape(&node_enum, &DUMMY_BOUNDS); self.painter .draw_shape_with_effects(&node.effects, &shape, || { self.painter.with_opacity(node.opacity, || { @@ -42,7 +53,8 @@ impl<'a> NodePainter<'a> { /// Draw an ImageNode, respecting transform, effect, rounded corners, blend mode, opacity pub fn draw_image_node(&self, node: &ImageNodeRec) -> bool { self.painter.with_transform(&node.transform.matrix, || { - let shape = build_shape(&IntrinsicSizeNode::Image(node.clone())); + let node_enum = Node::Image(node.clone()); + let shape = build_shape(&node_enum, &DUMMY_BOUNDS); self.painter .draw_shape_with_effects(&node.effects, &shape, || { @@ -73,7 +85,8 @@ impl<'a> NodePainter<'a> { /// Draw an EllipseNode pub fn draw_ellipse_node(&self, node: &EllipseNodeRec) { self.painter.with_transform(&node.transform.matrix, || { - let shape = build_shape(&IntrinsicSizeNode::Ellipse(node.clone())); + let node_enum = Node::Ellipse(node.clone()); + let shape = build_shape(&node_enum, &DUMMY_BOUNDS); self.painter .draw_shape_with_effects(&node.effects, &shape, || { self.painter.with_opacity(node.opacity, || { @@ -95,7 +108,8 @@ impl<'a> NodePainter<'a> { /// Draw a LineNode pub fn draw_line_node(&self, node: &LineNodeRec) { self.painter.with_transform(&node.transform.matrix, || { - let shape = build_shape(&IntrinsicSizeNode::Line(node.clone())); + let node_enum = Node::Line(node.clone()); + let shape = build_shape(&node_enum, &DUMMY_BOUNDS); self.painter.with_opacity(node.opacity, || { self.painter.with_blendmode(node.blend_mode, || { @@ -206,6 +220,7 @@ impl<'a> NodePainter<'a> { stroke_align: node.stroke_align, effects: node.effects.clone(), stroke_dash_array: node.stroke_dash_array.clone(), + layout_child: node.layout_child.clone(), }; self.draw_polygon_node(&polygon); @@ -229,6 +244,7 @@ impl<'a> NodePainter<'a> { stroke_align: node.stroke_align, effects: node.effects.clone(), stroke_dash_array: node.stroke_dash_array.clone(), + layout_child: node.layout_child.clone(), }; self.draw_polygon_node(&polygon); @@ -265,68 +281,10 @@ impl<'a> NodePainter<'a> { } /// Draw a ContainerNode (background + stroke + children) - pub fn draw_container_node_recursively( - &self, - id: &NodeId, - node: &ContainerNodeRec, - graph: &SceneGraph, - cache: &GeometryCache, - ) { - self.painter.with_transform(&node.transform.matrix, || { - self.painter.with_opacity(node.opacity, || { - let shape = build_shape(&IntrinsicSizeNode::Container(node.clone())); - - // Draw effects, fills, children (with optional clipping), then strokes last - self.painter - .draw_shape_with_effects(&node.effects, &shape, || { - self.painter.with_blendmode(node.blend_mode, || { - // Paint fills first - self.painter.draw_fills(&shape, &node.fills); - - // Children are drawn next; if `clip` is enabled we push - // a clip region for the container's shape so that - // descendants are clipped but the container's own stroke - // remains unaffected. - if let Some(children) = graph.get_children(id) { - if node.clip { - self.painter.with_clip(&shape, || { - for child_id in children { - if let Ok(child) = graph.get_node(child_id) { - self.draw_node_recursively( - child_id, child, graph, cache, - ); - } - } - }); - } else { - for child_id in children { - if let Ok(child) = graph.get_node(child_id) { - self.draw_node_recursively( - child_id, child, graph, cache, - ); - } - } - } - } - - // Finally paint the stroke so it is not clipped by the - // container's own clip and always renders above children. - self.painter.draw_strokes( - &shape, - &node.strokes, - node.stroke_width, - node.stroke_align, - node.stroke_dash_array.as_ref(), - ); - }); - }); - }); - }); - } - pub fn draw_error_node(&self, node: &ErrorNodeRec) { self.painter.with_transform(&node.transform.matrix, || { - let shape = build_shape(&IntrinsicSizeNode::Error(node.clone())); + let node_enum = Node::Error(node.clone()); + let shape = build_shape(&node_enum, &DUMMY_BOUNDS); // Create a red fill paint let fill = Paint::Solid(SolidPaint { @@ -449,7 +407,74 @@ impl<'a> NodePainter<'a> { match node { Node::Error(n) => self.draw_error_node(n), Node::Group(n) => self.draw_group_node_recursively(id, n, graph, cache), - Node::Container(n) => self.draw_container_node_recursively(id, n, graph, cache), + Node::Container(n) => { + // Get pre-computed local transform from geometry cache + let local_transform = cache + .get_transform(id) + .expect("Geometry must exist - pipeline bug"); + + self.painter.with_transform(&local_transform.matrix, || { + self.painter.with_opacity(n.opacity, || { + // Geometry guaranteed to exist - no Option + let bounds = cache + .get_world_bounds(id) + .expect("Geometry must exist - pipeline bug"); + let shape = build_shape(node, &bounds); + + // Draw effects, fills, children (with optional clipping), then strokes last + self.painter + .draw_shape_with_effects(&n.effects, &shape, || { + self.painter.with_blendmode(n.blend_mode, || { + // Paint fills first + self.painter.draw_fills(&shape, &n.fills); + + // Children are drawn next; if `clip` is enabled we push + // a clip region for the container's shape + if let Some(children) = graph.get_children(id) { + if n.clip { + self.painter.with_clip(&shape, || { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively( + child_id, child, graph, cache, + ); + } + } + }); + } else { + for child_id in children { + if let Ok(child) = graph.get_node(child_id) { + self.draw_node_recursively( + child_id, child, graph, cache, + ); + } + } + } + } + + // Finally paint the stroke + self.painter.draw_strokes( + &shape, + &n.strokes, + n.stroke_width, + n.stroke_align, + n.stroke_dash_array.as_ref(), + ); + }); + }); + }); + }); + } + Node::InitialContainer(_) => { + // ICB is invisible - only render children + if let Some(children) = graph.get_children(id) { + for child_id in children { + if let Ok(child_node) = graph.get_node(child_id) { + self.draw_node_recursively(child_id, child_node, graph, cache); + } + } + } + } Node::Rectangle(n) => self.draw_rect_node(n), Node::Ellipse(n) => self.draw_ellipse_node(n), Node::Polygon(n) => self.draw_polygon_node(n), diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 644dc0fd5f..2324bd05a2 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -150,6 +150,23 @@ impl Backend { } } +/// Window/viewport context for the renderer +/// +/// This serves as the source of truth for window state and enables +/// dependency-based layout where nodes can depend on viewport dimensions +/// (e.g., ICB sizing, future vw/vh units) +#[derive(Debug, Clone)] +pub struct RendererWindowContext { + /// Current viewport/window size + pub viewport_size: Size, +} + +impl RendererWindowContext { + pub fn new(viewport_size: Size) -> Self { + Self { viewport_size } + } +} + /// --------------------------------------------------------------------------- /// Renderer: manages backend, DPI, camera, and iterates over scene children /// --------------------------------------------------------------------------- @@ -169,6 +186,10 @@ pub struct Renderer { plan: Option, /// Runtime configuration for renderer behaviour config: RuntimeRendererConfig, + /// Layout computation engine (owns cache, detects changes) + layout_engine: crate::layout::engine::LayoutEngine, + /// Window/viewport context - source of truth for viewport state + pub window_context: RendererWindowContext, } impl Renderer { @@ -209,6 +230,7 @@ impl Renderer { } let mut image_repository = ImageRepository::new(store); system_images::register(&mut resources, &mut image_repository); + let viewport_size = *camera.get_size(); Self { backend, scene: None, @@ -221,6 +243,8 @@ impl Renderer { fc: FrameCounter::new(), plan: None, config: RuntimeRendererConfig::default(), + layout_engine: crate::layout::engine::LayoutEngine::new(), + window_context: RendererWindowContext::new(viewport_size), } } @@ -352,12 +376,30 @@ impl Renderer { /// Load a scene into the renderer. Caching will be performed lazily during /// rendering based on the configured caching strategy. pub fn load_scene(&mut self, scene: Scene) { - self.scene_cache = cache::scene::SceneCache::new(); - let requested = collect_scene_font_families(&scene); - self.fonts.set_requested_families(requested.into_iter()); - self.scene_cache.update_geometry(&scene, &self.fonts); - self.scene_cache.update_layers(&scene); self.scene = Some(scene); + + self.scene_cache = cache::scene::SceneCache::new(); + if let Some(scene) = self.scene.as_ref() { + let requested = collect_scene_font_families(scene); + self.fonts.set_requested_families(requested.into_iter()); + + let viewport_size = self.window_context.viewport_size; + + // 1. Compute layout phase + self.layout_engine.compute(scene, viewport_size); + + // 2. Build geometry with layout results + let layout_result = self.layout_engine.result(); + self.scene_cache.update_geometry_with_layout( + scene, + &self.fonts, + layout_result, + viewport_size, + ); + + // 3. Build layers + self.scene_cache.update_layers(scene); + } self.queue_stable(); } @@ -396,6 +438,37 @@ impl Renderer { self.scene_cache.invalidate(); } + /// Rebuild scene caches after scene geometry has changed. + /// Call this after modifying node sizes, positions, or other geometry properties. + pub fn rebuild_scene_caches(&mut self) { + if let Some(scene) = self.scene.as_ref() { + let viewport_size = self.window_context.viewport_size; + + // 1. Recompute layout + self.layout_engine.compute(scene, viewport_size); + + // 2. Rebuild geometry with layout results + let layout_result = self.layout_engine.result(); + self.scene_cache.update_geometry_with_layout( + scene, + &self.fonts, + layout_result, + viewport_size, + ); + + // 3. Rebuild layers + self.scene_cache.update_layers(scene); + } + } + + /// Update viewport context with new size + /// + /// This updates the source of truth for viewport/window dimensions. + /// Should be called before resolving viewport dependencies. + pub fn update_viewport_size(&mut self, width: f32, height: f32) { + self.window_context.viewport_size = Size { width, height }; + } + fn with_recording( &self, bounds: &rect::Rectangle, diff --git a/crates/grida-canvas/src/shape/polygon.rs b/crates/grida-canvas/src/shape/polygon.rs index 9bcced0f54..5e47cced73 100644 --- a/crates/grida-canvas/src/shape/polygon.rs +++ b/crates/grida-canvas/src/shape/polygon.rs @@ -1,6 +1,7 @@ use super::vn::{VectorNetwork, VectorNetworkSegment}; use super::*; use crate::cg::CGPoint; +use math2::Rectangle; use skia_safe; /// A simple (non-self-intersecting) closed polygon shape with optional corner radius. pub struct SimplePolygonShape { @@ -59,3 +60,31 @@ pub fn build_simple_polygon_vector_network(shape: &SimplePolygonShape) -> Vector regions: vec![], } } + +pub(crate) fn polygon_bounds(points: &[CGPoint]) -> Rectangle { + let mut min_x = f32::INFINITY; + let mut min_y = f32::INFINITY; + let mut max_x = f32::NEG_INFINITY; + let mut max_y = f32::NEG_INFINITY; + for p in points { + min_x = min_x.min(p.x); + min_y = min_y.min(p.y); + max_x = max_x.max(p.x); + max_y = max_y.max(p.y); + } + if points.is_empty() { + Rectangle { + x: 0.0, + y: 0.0, + width: 0.0, + height: 0.0, + } + } else { + Rectangle { + x: min_x, + y: min_y, + width: max_x - min_x, + height: max_y - min_y, + } + } +} diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 40b68063f5..b337ab177d 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -166,11 +166,21 @@ impl ApplicationApi for UnknownTargetApplication { fn resize(&mut self, width: u32, height: u32) { self.state.resize(width as i32, height as i32); self.renderer.backend = Backend::GL(self.state.surface_mut_ptr()); - self.renderer.invalidate_cache(); + + // Update viewport context (source of truth) + self.renderer + .update_viewport_size(width as f32, height as f32); + + // Update camera to match viewport self.renderer.camera.set_size(crate::node::schema::Size { width: width as f32, height: height as f32, }); + + // Rebuild caches - ICB layout computed automatically from viewport context + self.renderer.rebuild_scene_caches(); + + self.renderer.invalidate_cache(); self.queue(); } diff --git a/crates/grida-canvas/tests/geometry_cache.rs b/crates/grida-canvas/tests/geometry_cache.rs index b068e000bc..446a620b7b 100644 --- a/crates/grida-canvas/tests/geometry_cache.rs +++ b/crates/grida-canvas/tests/geometry_cache.rs @@ -1,4 +1,5 @@ use cg::cache::geometry::GeometryCache; +use cg::cg::types::*; use cg::node::{ factory::NodeFactory, scene_graph::{Parent, SceneGraph}, @@ -19,7 +20,8 @@ fn geometry_cache_builds_recursively() { let mut group1 = nf.create_group_node(); group1.transform = Some(AffineTransform::new(5.0, 5.0, 0.0)); let mut container = nf.create_container_node(); - container.transform = AffineTransform::new(10.0, 20.0, 0.0); + container.position = CGPoint::new(10.0, 20.0).into(); + container.rotation = 0.0; let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(4.0, 6.0, 0.0); @@ -55,10 +57,8 @@ fn container_world_bounds_include_children() { let mut graph = SceneGraph::new(); let mut container = nf.create_container_node(); - container.size = Size { - width: 100.0, - height: 100.0, - }; + container.layout_dimensions.width = Some(100.0); + container.layout_dimensions.height = Some(100.0); let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(50.0, 50.0, 0.0); rect.size = Size { diff --git a/crates/grida-canvas/tests/hit_test.rs b/crates/grida-canvas/tests/hit_test.rs index db2b67d143..5a7d2f3e00 100644 --- a/crates/grida-canvas/tests/hit_test.rs +++ b/crates/grida-canvas/tests/hit_test.rs @@ -15,10 +15,8 @@ fn hit_first_returns_topmost() { let nf = NodeFactory::new(); let mut container = nf.create_container_node(); - container.size = Size { - width: 40.0, - height: 40.0, - }; + container.layout_dimensions.width = Some(40.0); + container.layout_dimensions.height = Some(40.0); let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(10.0, 10.0, 0.0); rect.size = Size { @@ -83,10 +81,8 @@ fn intersects_returns_all_nodes_in_rect() { let nf = NodeFactory::new(); let mut container = nf.create_container_node(); - container.size = Size { - width: 100.0, - height: 100.0, - }; + container.layout_dimensions.width = Some(100.0); + container.layout_dimensions.height = Some(100.0); let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(50.0, 50.0, 0.0); rect.size = Size { diff --git a/crates/grida-canvas/tests/scene_cache.rs b/crates/grida-canvas/tests/scene_cache.rs index 7fd43ffb90..c3eab4b9b5 100644 --- a/crates/grida-canvas/tests/scene_cache.rs +++ b/crates/grida-canvas/tests/scene_cache.rs @@ -4,7 +4,6 @@ use cg::node::{ scene_graph::{Parent, SceneGraph}, schema::*, }; -use cg::painter::layer::Layer; use cg::resources::ByteStore; use cg::runtime::font_repository::FontRepository; use math2::rect::Rectangle; @@ -17,10 +16,8 @@ fn layers_in_rect_include_partially_visible_nested() { let mut graph = SceneGraph::new(); let mut container = nf.create_container_node(); - container.size = Size { - width: 100.0, - height: 100.0, - }; + container.layout_dimensions.width = Some(100.0); + container.layout_dimensions.height = Some(100.0); let mut rect = nf.create_rectangle_node(); rect.transform = AffineTransform::new(50.0, 50.0, 0.0); rect.size = Size { diff --git a/crates/grida-canvas/tests/vector_corner_radius.rs b/crates/grida-canvas/tests/vector_corner_radius.rs index 39185089db..797530b08e 100644 --- a/crates/grida-canvas/tests/vector_corner_radius.rs +++ b/crates/grida-canvas/tests/vector_corner_radius.rs @@ -48,6 +48,7 @@ fn collect_verbs(path: &skia_safe::Path) -> Vec { fn make_node(corner_radius: f32) -> VectorNodeRec { VectorNodeRec { active: true, + layout_child: None, opacity: 1.0, blend_mode: LayerBlendMode::default(), mask: None, diff --git a/docs/wg/feat-layout/index.md b/docs/wg/feat-layout/index.md new file mode 100644 index 0000000000..450578839c --- /dev/null +++ b/docs/wg/feat-layout/index.md @@ -0,0 +1,224 @@ +# Layout Model - `layout` + +> Universal positioning, dimensions, layout management with anchors, flex and grid. + +| feature id | status | description | PRs | +| ---------- | ------ | ---------------------- | ------------------------------------------------- | +| `layout` | draft | Universal Layout Model | [#437](https://github.com/gridaco/grida/pull/437) | + +--- + +## Abstract + +The Grida Layout Model (`layout`) introduces a unified, geometry-first foundation for all 2D layout and positioning scenarios. +It merges the conceptual clarity of **anchors**, the flexibility of **flexbox**, and the structure of **grid**—forming a single, coherent system that scales from freeform graphics design to complex UI composition. + +Unlike traditional rule-based layouts, this model prioritizes **direct manipulation, predictability, and composability**, aligning deeply with designer-first workflows and visual editing metaphors. +The result is a layout system that remains **ergonomic for creators**, **precise for engineers**, and **interoperable with web standards**, bridging the gap between WYSIWYG design and production-grade layout semantics. + +--- + +## 1. Existing Solutions and Their Problems + +### CSS Inset `ltrb` + +**Pros:** + +- Intuitive, simple, battle tested, de facto standards +- Easy to write in code + +**Cons:** + +- Not predictable +- Layout-centric, not graphics friendly (non XYWH centric) +- What you define is NOT what you get +- Fundamentally conflicts with the sizing (left + width + right => one will get dropped) + +### Constraint Layout (Android) + +Android's ConstraintLayout uses a constraint-based system that allows complex layouts with a flat view hierarchy. + +**Pros:** + +- Powerful and flexible constraint relationships (chains, barriers, guidelines) +- Flat layout hierarchy improves performance +- Rich constraint model supports complex positional relationships +- Good visual editor tooling in Android Studio + +**Cons:** + +- View-based rather than graphics-centric (not XYWH oriented) +- Difficult to translate to web standards (CSS) +- Verbose XML definitions, not concise for programmatic use +- Dangerous — when the target is removed the link becomes orphaned or error-stated +- Can't be mix-used with flex-like layout + +### XY + Constraints (Figma) + +Figma uses XY + constraints, which is designer friendly and intuitive. + +**Pros:** + +- Designer friendly +- Graphics schema friendly — the XY values are actually defined/computed with the XY field, making the API `node.x = 10` (graphics centric) +- Graphics-first, layout as feature (good) +- Can represent "center" alignment, where the position basis is center, even while not actually center positioned +- 100% works with CSS inset => Figma + +**Cons:** + +- What user sets is NOT ALWAYS what they get — if under a layout (Auto Layout), the XY will be ignored, readonly +- When you pin a node to right only, it still gets defined by `x + x_constraints = right` (where what user does is "this is right = 10", the schema does not contain the 'right is 10' but rather the current 'state' x) +- Lacks easy mapping from Figma => CSS when constraints are center + +--- + +These existing models each address parts of the layout challenge but fall short of providing a unified, intuitive, and interoperable solution. While no model can solve all problems at once—and ours will share some of the same challenges—a combined anchor + flex + grid approach bridges these gaps through a strongly-typed SDK, comprehensive documentation, and well-crafted examples that guide users toward correct usage patterns. + +--- + +## 2. Alignment with Editor + +The anchor-based layout model is designed to closely align with direct manipulation in a designer-first WYSIWYG editor: + +- **Ergonomic mapping:** Anchors correspond to intuitive "pinning" points on elements (edges, corners, centers), making positioning and resizing natural. +- **Pinning:** Users can "pin" one or more edges or centers to parent or sibling anchors, reflecting common design intentions. +- **Centering and snapping:** Anchors support easy centering and snapping behaviors without complex calculations. +- **Visual clarity:** The model exposes explicit anchor relationships, reducing guesswork and improving user comprehension. + +This approach provides a seamless bridge between visual editing and layout semantics, enabling precise, predictable control. + +--- + +## 3. Following Standards + +While conceptually new, the anchor model aligns with emerging web standards: + +- It is consistent with the CSS Anchor Positioning Working Draft (2024–2025), which introduces anchor-based layout primitives. +- The model remains fully transpile-compatible with traditional CSS inset, flexbox, and grid layouts. +- This ensures that adopting anchors does not isolate designs from existing CSS ecosystems or tooling. + +By building on standards, the model facilitates future-proof, interoperable layout workflows. + +--- + +## 4. Transpilation & Interoperability + +The relationship between CSS inset and anchor positioning is fundamentally lossless: + +- **CSS inset ↔ Anchor translation:** Conversion between inset properties and anchors can be performed without loss of layout semantics. +- **Production exports:** Anchor-based layouts can be transpiled into either CSS Anchors (where supported) or legacy inset/flex/grid code for maximum compatibility. +- This dual-path transpilation supports diverse deployment targets and progressive enhancement strategies. + +Interoperability ensures that anchor adoption integrates smoothly into existing pipelines. + +--- + +## 5. Alignment with Core Use Cases + +The anchor model comprehensively covers key layout scenarios: + +- **Free-form/graphics design:** Anchors naturally implement absolute XY positioning with respect to parent anchors, matching typical graphic design workflows. +- **UI design:** Anchors combine seamlessly with flex and grid layouts to support responsive, component-based interface design. +- **Game or 3D-parallel models:** Anchors correspond to constraint-based positioning systems used in game engines and 3D UI frameworks, enabling consistent paradigms across domains. + +This universality makes anchors a foundational primitive bridging multiple design disciplines. + +--- + +## 6. Auxiliary Use Cases + +Beyond core layout, anchors enable advanced features: + +- **Comment bubbles:** Anchors provide stable attachment points for annotations that track element movement. +- **Link lines:** Anchors facilitate dynamic connections between UI components or diagram nodes. +- **Attached annotations:** Anchors support overlays and badges that remain positioned relative to their targets. + +These auxiliary use cases demonstrate the extensibility and versatility of the anchor model. + +--- + +## 7. Conclusion (TL;DR) + +- The combined anchor + flex + grid model covers every 2D layout scenario. +- It bridges the gap between free-form graphics and structured UI design. +- It is ergonomically aligned with a designer-first, direct manipulation paradigm. +- It is standards-aligned, losslessly transpile-compatible, and interoperable. +- Anchors provide a universal, intuitive foundation for modern layout management in Grida. + +--- + +## 8. Implementation Levels + +To ensure stability and gradual adoption, the layout system will be rolled out in progressive implementation levels: + +### Level 1 — Practical Foundation (MVP) + +- **Parent-only anchors:** Support anchors referencing only the parent node (identical semantics to inset + relative positioning). +- **Flex foundations:** Initial flexbox layout support for basic responsive layouts. + +### Level 2 — Stronger, Production-Ready (Beta) + +- **Conflict resolution:** Formalize deterministic resolution rules for over-constrained anchor configurations. +- **Schema soundness:** Enforce predictable, schema-safe layout definitions for reliable serialization and round-tripping. + +### Level 3 — Grid Expansion + +- **Grid support:** Introduce grid layout capabilities alongside flex, providing full 2D layout composition. + +### Level 4 — Full Anchor Model (Advanced) + +- **User-defined anchors:** Allow anchoring to arbitrary nodes (siblings, ancestors, or named anchors). +- **Safe fallbacks:** Implement robust fallback strategies for missing or invalid anchor targets. +- **Inter-element constraints:** Enable rich relational layouts beyond parent-only anchoring. + +--- + +## 9. References + +- **CSS Anchor Positioning Module Level 1** — W3C Working Draft (2024–2025) + +- **Chrome Developers: Anchor Positioning API** — Overview and examples + +- **MDN Web Docs: CSS Anchor Positioning** — Syntax and browser support + +- **OddBird Blog: Anchor Positioning Updates (2025)** — Practical insights from spec contributors + + +--- + +### Cross‑Domain Anchoring & Constraint Examples + +- **Unity UI Anchors & RectTransform** — Unity’s `RectTransform` component supports parent‑relative anchors and stretching behavior similar to inset and anchor models, forming the foundation of its responsive UI system. + + +- **Godot Control Nodes** — Godot’s `Control` nodes implement an anchor/margin system that allows constraint‑based UI placement in both 2D and 3D scenes. + + +- **Figma Auto Layout & Constraints** — Figma’s constraint model (top, left, right, bottom, center) operates on the same principle as anchors, enabling adaptive, rule‑based positioning in a 2D design environment. + + +- **CAD / Parametric Modeling** — Tools like AutoCAD and SolveSpace use constraint graphs to define positional relationships between points, edges, and objects—an abstract yet mathematically identical anchor system. + + +These references illustrate that anchor‑style positional constraints are not unique to CSS—they represent a universal paradigm across 2D, 3D, and CAD domains. + +## 10. Performance Considerations + +The new layout engine is designed to maintain parity or exceed performance compared to existing layout paradigms: + +- **Anchor model efficiency:** + When used in simple parent-only configurations (equivalent to inset-based layouts), the anchor system introduces negligible computational overhead. + It can be optimized to perform **as fast or faster** than the traditional inset model by leveraging direct parent geometry caching and simplified constraint resolution. + +- **Flex and grid alignment:** + Flex and grid layouts are implemented following the same logical rules and constraints as CSS specifications, allowing predictable and efficient layout computation. + Flex performance should match existing browser and UI engine implementations due to similar evaluation stages and data structures. + +- **Incremental recomputation:** + Layout changes are diff-based — only affected nodes and their dependents are recomputed, minimizing the cost of updates during design or animation. + +- **Predictable scaling:** + Anchor relationships are localized; the absence of global dependency graphs ensures linear-time layout resolution in typical use cases. + +This ensures that while the model expands flexibility and expressiveness, it does not compromise runtime or editing performance. diff --git a/editor/.gitignore b/editor/.gitignore index c2a9083dde..e3c653e318 100644 --- a/editor/.gitignore +++ b/editor/.gitignore @@ -46,3 +46,5 @@ supabase/.temp # Playwright node_modules/ + +target/ \ No newline at end of file diff --git a/editor/app/(dev)/ui/components/controls-flex-align/page.tsx b/editor/app/(dev)/ui/components/controls-flex-align/page.tsx new file mode 100644 index 0000000000..5448c07b0f --- /dev/null +++ b/editor/app/(dev)/ui/components/controls-flex-align/page.tsx @@ -0,0 +1,202 @@ +"use client"; + +import React, { useState } from "react"; +import { FlexAlignControl } from "@/scaffolds/sidecontrol/controls/flex-align"; +import { ArrowRight, ArrowDown } from "lucide-react"; +import type cg from "@grida/cg"; + +type MainAxisAlignment = cg.MainAxisAlignment; +type CrossAxisAlignment = cg.CrossAxisAlignment; +type Axis = cg.Axis; + +interface FlexAlignValue { + direction: Axis; + mainAxisAlignment: MainAxisAlignment; + crossAxisAlignment: CrossAxisAlignment; +} + +export default function FlexAlignControlPage() { + const [flexAlignValue, setFlexAlignValue] = useState({ + direction: "horizontal", + mainAxisAlignment: "start", + crossAxisAlignment: "start", + }); + + return ( +
+
+

Flex Align Control

+

+ Interactive 3x3 grid for selecting flex alignment properties +

+
+ +
+ {/* Direction Toggle */} +
+ +
+ + +
+
+ + {/* Flex Align Control */} +
+ +
+ + setFlexAlignValue((prev) => ({ + ...prev, + ...value, + })) + } + /> +
+
+ + {/* Preview Section */} +
+ + + {/* Preview with varying heights */} +
+

+ Children with varying heights + + • Best for horizontal (row) direction + +

+
+
+
+
+
+
+ + {/* Preview with varying widths */} +
+

+ Children with varying widths + + • Best for vertical (column) direction + +

+
+
+
+
+
+
+
+ + {/* Current Values Display */} +
+ +
+
+ Direction:{" "} + {flexAlignValue.direction} +
+
+ Main Axis:{" "} + {flexAlignValue.mainAxisAlignment} +
+
+ Cross Axis:{" "} + {flexAlignValue.crossAxisAlignment} +
+
+
+ + {/* CSS Output */} +
+ +
+ + {`flex-direction: ${flexAlignValue.direction === "horizontal" ? "row" : "column"};`} +
+ {`justify-content: ${flexAlignValue.mainAxisAlignment};`} +
+ {`align-items: ${flexAlignValue.crossAxisAlignment};`} +
+
+
+
+
+ ); +} diff --git a/editor/grida-canvas-react-renderer-dom/template-builder/components/footers.tsx b/editor/grida-canvas-react-renderer-dom/template-builder/components/footers.tsx deleted file mode 100644 index 94f52f98ca..0000000000 --- a/editor/grida-canvas-react-renderer-dom/template-builder/components/footers.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import { Button } from "@/components/ui/button"; -import { NodeElement } from "../../nodes/node"; -import { IconWidget } from "../widgets/icon"; -import Link from "next/link"; - -export function Footer_001() { - return ( - - - - - - - - - - - - - - - - - - ); -} diff --git a/editor/grida-canvas-react-starter-kit/starterkit-preview/index.tsx b/editor/grida-canvas-react-starter-kit/starterkit-preview/index.tsx index c82fccf3b0..65680e04d2 100644 --- a/editor/grida-canvas-react-starter-kit/starterkit-preview/index.tsx +++ b/editor/grida-canvas-react-starter-kit/starterkit-preview/index.tsx @@ -61,7 +61,7 @@ export function PreviewProvider({ const getPreviewNode = (node_id?: string) => { function tryGetTopPreviewNode(node_id: string) { - const topid = dq.getTopId(document_ctx, node_id); + const topid = dq.getTopIdWithinScene(document_ctx, node_id, scene.id); if (!topid) return null; const top = document.nodes[topid]; if (!top) return null; diff --git a/editor/grida-canvas-react/provider.tsx b/editor/grida-canvas-react/provider.tsx index 14db402f94..2c81cead9e 100644 --- a/editor/grida-canvas-react/provider.tsx +++ b/editor/grida-canvas-react/provider.tsx @@ -237,8 +237,8 @@ export function useNodeActions(node_id: string | undefined) { instance.commands.changeContainerNodeLayout(node_id, value), direction: (value: cg.Axis) => instance.commands.changeFlexContainerNodeDirection(node_id, value), - // flexWrap: (value?: string) => - // changeNodeStyle(node_id, "flexWrap", value), + layoutWrap: (value: "wrap" | "nowrap") => + instance.commands.changeFlexContainerNodeWrap(node_id, value), mainAxisAlignment: (value: cg.MainAxisAlignment) => instance.commands.changeFlexContainerNodeMainAxisAlignment( node_id, @@ -604,8 +604,7 @@ export function useGestureState(): UseGestureState { const is_node_translating = gesture.type === "translate" || gesture.type === "sort" || - gesture.type === "nudge" || - gesture.type === "gap"; + gesture.type === "nudge"; const is_node_scaling = gesture.type === "scale"; return { diff --git a/editor/grida-canvas-react/ui-config.ts b/editor/grida-canvas-react/ui-config.ts index ab223f8585..49660da5b6 100644 --- a/editor/grida-canvas-react/ui-config.ts +++ b/editor/grida-canvas-react/ui-config.ts @@ -1,6 +1,33 @@ +/** + * Minimum node size in UI space (pixels) required to display the inner content overlay handle. + * This avoids showing the handle when nodes are too small or far zoomed out, + * ensuring the control stays within the node's bounds for the best user experience. + * + * applies to: + * - corner radius + * - gap + * - padding + * - ... + */ +export const MIN_NODE_OVERLAY_INNER_CONTENT_VISIBLE_UI_SIZE = 100; + /** * Minimum node size in UI space (pixels) required to display the corner radius overlay handle. * This avoids showing the handle when nodes are too small or far zoomed out, * ensuring the control stays within the node's bounds for the best user experience. */ export const MIN_NODE_OVERLAY_CORNER_RADIUS_VISIBLE_UI_SIZE = 100; + +/** + * Minimum node size in UI space (pixels) required to display the gap overlay handle. + * This avoids showing the handle when nodes are too small or far zoomed out, + * ensuring the control stays within the node's bounds for the best user experience. + */ +export const MIN_NODE_OVERLAY_GAP_VISIBLE_UI_SIZE = 100; + +/** + * Minimum node size in UI space (pixels) required to display the padding overlay handle. + * This avoids showing the handle when nodes are too small or far zoomed out, + * ensuring the control stays within the node's bounds for the best user experience. + */ +export const MIN_NODE_OVERLAY_PADDING_VISIBLE_UI_SIZE = 100; diff --git a/editor/grida-canvas-react/viewport/hooks/use-surface-gesture.ts b/editor/grida-canvas-react/viewport/hooks/use-surface-gesture.ts new file mode 100644 index 0000000000..8ec4ab6de4 --- /dev/null +++ b/editor/grida-canvas-react/viewport/hooks/use-surface-gesture.ts @@ -0,0 +1,45 @@ +import { useGesture } from "@use-gesture/react"; +import { useRef } from "react"; + +export function useSurfaceGesture( + { + onClick, + onDoubleClick, + onDragStart, + onDragEnd, + ...handlers + }: Parameters[0], + config?: Parameters[1] +) { + // click / double click triggers when drag ends (if double pointer down) - it might be a better idea to prevent it with the displacement, not by delayed flag + const should_prevent_click = useRef(false); + + return useGesture( + { + onClick: (e) => { + if (should_prevent_click.current) { + return; + } + onClick?.(e); + }, + onDoubleClick: (e) => { + if (should_prevent_click.current) { + return; + } + onDoubleClick?.(e); + }, + ...handlers, + onDragStart: (e) => { + onDragStart?.(e); + should_prevent_click.current = true; + }, + onDragEnd: (e) => { + onDragEnd?.(e); + setTimeout(() => { + should_prevent_click.current = false; + }, 100); + }, + }, + config + ); +} diff --git a/editor/grida-canvas-react/viewport/hotkeys.tsx b/editor/grida-canvas-react/viewport/hotkeys.tsx index 7aade46e8e..e1753817ca 100644 --- a/editor/grida-canvas-react/viewport/hotkeys.tsx +++ b/editor/grida-canvas-react/viewport/hotkeys.tsx @@ -379,6 +379,7 @@ export function useEditorHotKeys() { "off" ); editor.surface.surfaceConfigureRotateWithQuantizeModifier("off"); + editor.surface.surfaceConfigurePaddingWithMirroringModifier("off"); setAltKey(false); editor.surface.surfaceSetTool({ type: "cursor" }, "window blur"); }; @@ -421,6 +422,7 @@ export function useEditorHotKeys() { editor.surface.surfaceConfigureTransformWithCenterOriginModifier( "on" ); + editor.surface.surfaceConfigurePaddingWithMirroringModifier("on"); setAltKey(true); // NOTE: on some systems, the alt key focuses to the browser menu, so we need to prevent that. (e.g. alt key on windows/chrome) e.preventDefault(); @@ -462,6 +464,7 @@ export function useEditorHotKeys() { editor.surface.surfaceConfigureTransformWithCenterOriginModifier( "off" ); + editor.surface.surfaceConfigurePaddingWithMirroringModifier("off"); setAltKey(false); break; case "Shift": diff --git a/editor/grida-canvas-react/viewport/surface.tsx b/editor/grida-canvas-react/viewport/surface.tsx index ab26cd67f5..6652bc90fb 100644 --- a/editor/grida-canvas-react/viewport/surface.tsx +++ b/editor/grida-canvas-react/viewport/surface.tsx @@ -1,7 +1,8 @@ "use client"; import React, { useContext, useEffect, useMemo, useRef, useState } from "react"; -import { useGesture as __useGesture, useGesture } from "@use-gesture/react"; +import { useGesture } from "@use-gesture/react"; +import { useSurfaceGesture } from "./hooks/use-surface-gesture"; import { useBackendState, useBrushState, @@ -39,17 +40,13 @@ import { MeasurementGuide } from "./ui/measurement"; import { VectorMeasurementGuide } from "./ui/vector-measurement"; import { SnapGuide } from "./ui/snap"; import { Knob } from "./ui/knob"; -import { ColumnsIcon, RowsIcon } from "@radix-ui/react-icons"; -import cmath from "@grida/cmath"; import { cursors } from "../../components/cursor/cursor-data"; -import { FakePointerCursorSVG } from "@/components/cursor/cursor-fake"; import { SurfaceTextEditor } from "./ui/text-editor"; import { SurfaceVectorEditor } from "./ui/surface-vector-editor"; import { SurfaceGradientEditor } from "./ui/surface-gradient-editor"; import { SurfaceImageEditor } from "./ui/surface-image-editor"; import { SizeMeterLabel } from "./ui/meter"; import { RedDotHandle } from "./ui/reddot"; -import { ObjectsDistributionAnalysis } from "./ui/distribution"; import { AxisRuler, Tick } from "@grida/ruler/react"; import { PixelGrid } from "@grida/pixel-grid/react"; import { Rule } from "./ui/rule"; @@ -65,7 +62,11 @@ import { BezierCurvedLine } from "./ui/network-curve"; import type { editor } from "@/grida-canvas"; import { useFollowPlugin } from "../plugins/use-follow"; import { SurfaceVariableWidthEditor } from "./ui/surface-varwidth-editor"; -import { MIN_NODE_OVERLAY_CORNER_RADIUS_VISIBLE_UI_SIZE } from "../ui-config"; +import { + MIN_NODE_OVERLAY_CORNER_RADIUS_VISIBLE_UI_SIZE, + MIN_NODE_OVERLAY_GAP_VISIBLE_UI_SIZE, + MIN_NODE_OVERLAY_PADDING_VISIBLE_UI_SIZE, +} from "../ui-config"; import { NodeOverlayCornerRadiusHandle, NodeOverlayRectangularCornerRadiusHandles, @@ -74,6 +75,13 @@ import { FakeCursorPosition, FakeForeignCursor, } from "@/components/multiplayer/cursor"; +import { + DistributeButton, + GapOverlay, +} from "./ui/surface-distribution-overlay"; +import { PaddingOverlay } from "./ui/surface-padding-overlay"; +import cmath from "@grida/cmath"; +import { cn } from "@/components/lib/utils"; const DRAG_THRESHOLD = 2; @@ -94,64 +102,37 @@ function SurfaceTransformContextProvider({ } */ -function useSurfaceGesture( - { - onClick, - onDoubleClick, - onDragStart, - onDragEnd, - ...handlers - }: Parameters[0], - config?: Parameters[1] -) { - // click / double click triggers when drag ends (if double pointer down) - it might be a better idea to prevent it with the displacement, not by delayed flag - const should_prevent_click = useRef(false); - - return __useGesture( - { - onClick: (e) => { - if (should_prevent_click.current) { - return; - } - onClick?.(e); - }, - onDoubleClick: (e) => { - if (should_prevent_click.current) { - return; - } - onDoubleClick?.(e); - }, - ...handlers, - onDragStart: (e) => { - onDragStart?.(e); - should_prevent_click.current = true; - }, - onDragEnd: (e) => { - onDragEnd?.(e); - setTimeout(() => { - should_prevent_click.current = false; - }, 100); - }, - }, - config - ); +/** + * similar to SurfaceGroup, but for ones that should have own event target, non-blocking. + */ +function SurfaceFragmentGroup({ + children, + hidden, +}: React.PropsWithChildren<{ className?: string; hidden?: boolean }>) { + if (hidden) return null; + return <>{children}; } function SurfaceGroup({ hidden, children, dontRenderWhenHidden, + className, }: React.PropsWithChildren<{ hidden?: boolean; /** * completely remove from render tree, use this when the content is expensive and worth destroying. */ dontRenderWhenHidden?: boolean; + className?: string; }>) { return (
{hidden && dontRenderWhenHidden ? null : children}
@@ -853,6 +834,8 @@ function get_cursor_tooltip_value(gesture: editor.gesture.GestureState) { switch (gesture.type) { case "gap": return cmath.ui.formatNumber(gesture.gap, 1); + case "padding": + return cmath.ui.formatNumber(gesture.padding, 1); case "rotate": return cmath.ui.formatNumber(gesture.rotation, 1) + "°"; case "translate": @@ -890,33 +873,86 @@ function SingleSelectionOverlay({ }) { const editor = useCurrentEditor(); const { gesture, is_node_translating } = useGestureState(); + const { scaleX, scaleY } = useTransformState(); const data = useSingleSelection(node_id); if (!data) return <>; - const { node, distribution, rotation, style, boundingSurfaceRect } = data; + const { node, distribution, rotation, boundingSurfaceRect, size, object } = + data; + + // Get padding if this is a container + const padding = + node.type === "container" && "padding" in node ? node.padding : undefined; + + // Calculate measurement rect for visibility checks + const measurement_rect = { + x: 0, + y: 0, + width: size[0] * scaleX, + height: size[1] * scaleY, + }; + + const show_gap_overlay = + measurement_rect.width >= MIN_NODE_OVERLAY_GAP_VISIBLE_UI_SIZE && + measurement_rect.height >= MIN_NODE_OVERLAY_GAP_VISIBLE_UI_SIZE; + + const show_padding_overlay = + measurement_rect.width >= MIN_NODE_OVERLAY_PADDING_VISIBLE_UI_SIZE && + measurement_rect.height >= MIN_NODE_OVERLAY_PADDING_VISIBLE_UI_SIZE; return ( <>
- {node.meta.is_flex_parent && - distribution && - (gesture.type === "idle" || gesture.type === "gap") && - // TODO: support rotated surface - rotation === 0 && ( - <> - { - editor.surface.surfaceStartGapGesture(node_id, axis); - }} - /> - - )} - +
); @@ -1048,14 +1084,15 @@ function NodeOverlay({ focused, borderColor, borderWidth, -}: { + children, +}: React.PropsWithChildren<{ node_id: string; readonly?: boolean; zIndex?: number; focused?: boolean; borderColor?: string; borderWidth?: number; -}) { +}>) { const { scaleX, scaleY } = useTransformState(); const backend = useBackendState(); const tool = useToolState(); @@ -1065,21 +1102,30 @@ function NodeOverlay({ const bind = useSurfaceGesture( { + // FIXME: need better event handling - completely remove this in the future, use bbh query to handle the logic, purely mathmatical without binding events to this. + // basically, below block is required to prevent the current selection from de-selecting, when user tries to drag it. + // but this causes, + // 1. the ui (input) to not blur from panel + // 2. the inner content from being selected onPointerDown: ({ event }) => { - if (tool.type !== "insert" && tool.type !== "draw" && !event.shiftKey) { + if ( + tool.type !== "insert" && + tool.type !== "draw" && + !event.shiftKey && + !event.metaKey + ) { // prevent default to keep selection when clicking empty overlay // but allow shift+click to fall through for deselection event.preventDefault(); + + // blur inputs manually + try { + (document.activeElement as HTMLInputElement)?.blur(); + } catch {} } }, }, - { - drag: { - enabled, - threshold: DRAG_THRESHOLD, - keyboardDisplacement: 0, - }, - } + { enabled } ); const data = useSingleSelection(node_id); @@ -1102,9 +1148,7 @@ function NodeOverlay({ measurement_rect.width >= MIN_NODE_OVERLAY_CORNER_RADIUS_VISIBLE_UI_SIZE && measurement_rect.height >= MIN_NODE_OVERLAY_CORNER_RADIUS_VISIBLE_UI_SIZE; - { - /* TODO: resize for bitmap is not supported */ - } + // TODO: resize for bitmap is not supported */ const is_resizable_node = node.type !== "bitmap"; return ( @@ -1169,6 +1213,7 @@ function NodeOverlay({ className="bg-workbench-accent-sky group-data-[layer-is-component-consumer='true']:bg-workbench-accent-violet text-white" /> )} + {children} ); @@ -1242,7 +1287,7 @@ function LayerOverlayResizeHandle({ }) { const editor = useCurrentEditor(); - const zIndex = ["n", "e", "s", "w"].includes(anchor) ? 12 : 11; + const zIndex = ["n", "e", "s", "w"].includes(anchor) ? 11 : 21; const bind = useSurfaceGesture({ onPointerDown: ({ event }) => { @@ -1326,7 +1371,7 @@ function LayerOverlayResizeSide({ background: "transparent", cursor: cursors.resize_handle_cursor_map[anchor], touchAction: "none", - zIndex: 10, + zIndex: 20, ...positionalStyle, }} /> @@ -1431,229 +1476,6 @@ function RedDotSortHandle({ return ; } -function GapOverlay({ - onGapGestureStart, - offset, - style, - distribution, -}: { - distribution: ObjectsDistributionAnalysis; - offset?: cmath.Vector2; - style?: React.CSSProperties; -} & { - onGapGestureStart?: (axis: cmath.Axis) => void; -}) { - const { transform } = useTransformState(); - - const { x, y, rects: _rects } = distribution; - - // rects in surface space - const rects = useMemo( - () => _rects.map((r) => cmath.rect.transform(r, transform)), - [_rects, transform] - ); - - return ( -
-
- {_rects.length >= 2 && ( - <> - {x && x.gap !== undefined && ( - <> - {Array.from({ length: x.gaps.length }).map((_, i) => { - const axis = "x"; - const x_sorted = rects.sort((a, b) => a.x - b.x); - const a = x_sorted[i]; - const b = x_sorted[i + 1]; - - return ( - - ); - })} - - )} - {y && y.gap !== undefined && ( - <> - {Array.from({ length: y.gaps.length }).map((_, i) => { - const axis = "y"; - const y_sorted = rects.sort((a, b) => a.y - b.y); - const a = y_sorted[i]; - const b = y_sorted[i + 1]; - - return ( - - ); - })} - - )} - - )} -
-
- ); -} - -function GapWithHandle({ - a, - b, - axis, - offset = cmath.vector2.zero, - onGapGestureStart, -}: { - a: cmath.Rectangle; - b: cmath.Rectangle; - axis: cmath.Axis; - offset?: cmath.Vector2; - onGapGestureStart?: (axis: cmath.Axis) => void; -}) { - const { gesture } = useGestureState(); - - const r = useMemo(() => { - const intersection = cmath.rect.axisProjectionIntersection([a, b], axis)!; - - if (!intersection) return null; - - let rect: cmath.Rectangle; - if (axis === "x") { - const x1 = a.x + a.width; - const y1 = intersection[0]; - const x2 = b.x; - const y2 = intersection[1]; - - rect = cmath.rect.fromPoints([ - [x1, y1], - [x2, y2], - ]); - } else { - const x1 = intersection[0]; - const y1 = a.y + a.height; - const x2 = intersection[1]; - const y2 = b.y; - - rect = cmath.rect.fromPoints([ - [x1, y1], - [x2, y2], - ]); - } - - return cmath.rect.translate(rect, cmath.vector2.invert(offset)); - }, [a, b, axis, offset]); - - const is_gesture = gesture.type === "gap"; - - if (!r) return null; - - return ( - <> -
-
- -
-
- - ); -} - -function GapHandle({ - axis, - onGapGestureStart, -}: { - axis: cmath.Axis; - onGapGestureStart?: (axis: cmath.Axis) => void; -}) { - const bind = useSurfaceGesture({ - onPointerDown: ({ event }) => { - event.preventDefault(); - }, - onDragStart: ({ event }) => { - event.preventDefault(); - onGapGestureStart?.(axis); - }, - }); - - return ( - - ); -} - -function DistributeButton({ - axis, - onClick, -}: { - axis: cmath.Axis | undefined; - onClick?: (axis: cmath.Axis) => void; -}) { - if (!axis) { - return <>; - } - - return ( -
- -
- ); -} - function PixelGridOverlay() { const editor = useCurrentEditor(); const transform = useEditorState(editor, (state) => state.transform); diff --git a/editor/grida-canvas-react/viewport/ui/knob.tsx b/editor/grida-canvas-react/viewport/ui/knob.tsx index 6c554934a0..304e097502 100644 --- a/editor/grida-canvas-react/viewport/ui/knob.tsx +++ b/editor/grida-canvas-react/viewport/ui/knob.tsx @@ -60,7 +60,7 @@ export const Knob = React.forwardRef(function Knob( left: anchorPosition.left, cursor: readonly ? "default" : cursors.resize_handle_cursor_map[anchor], touchAction: "none", - zIndex: zIndex ?? 11, + zIndex: zIndex ?? 99, ...transform, }} > diff --git a/editor/grida-canvas-react/viewport/ui/surface-distribution-overlay.tsx b/editor/grida-canvas-react/viewport/ui/surface-distribution-overlay.tsx new file mode 100644 index 0000000000..14ae057a52 --- /dev/null +++ b/editor/grida-canvas-react/viewport/ui/surface-distribution-overlay.tsx @@ -0,0 +1,268 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +import { useGesture as __useGesture } from "@use-gesture/react"; +import { useGestureState, useTransformState } from "../../provider"; +import { ColumnsIcon, RowsIcon } from "@radix-ui/react-icons"; +import { ObjectsDistributionAnalysis } from "./distribution"; +import cmath from "@grida/cmath"; +import { useSurfaceGesture } from "../hooks/use-surface-gesture"; +import { SVGPatternDiagonalStripe } from "./svg-fill-patterns"; +import { cn } from "@/components/lib/utils"; + +export function GapOverlay({ + onGapGestureStart, + offset, + style, + distribution, +}: { + distribution: ObjectsDistributionAnalysis; + offset?: cmath.Vector2; + style?: React.CSSProperties; +} & { + onGapGestureStart?: (axis: cmath.Axis) => void; +}) { + const { transform } = useTransformState(); + + const { x, y, rects: _rects } = distribution; + + // either one gap is hovered, used for controlling the "highlighted" state of the gap handle + const [uxGapHover, setUxGapHover] = useState( + undefined + ); + + // rects in surface space + const rects = useMemo( + () => _rects.map((r) => cmath.rect.transform(r, transform)), + [_rects, transform] + ); + + const gaps = useMemo(() => { + if (rects.length < 2) return []; + + const result: Array<{ + axis: cmath.Axis; + a: cmath.Rectangle; + b: cmath.Rectangle; + }> = []; + + if (x && x.gaps && x.gaps.length > 0) { + const x_sorted = [...rects].sort((a, b) => a.x - b.x); + for (let i = 0; i < x.gaps.length; i++) { + result.push({ + axis: "x", + a: x_sorted[i], + b: x_sorted[i + 1], + }); + } + } + + if (y && y.gaps && y.gaps.length > 0) { + const y_sorted = [...rects].sort((a, b) => a.y - b.y); + for (let i = 0; i < y.gaps.length; i++) { + result.push({ + axis: "y", + a: y_sorted[i], + b: y_sorted[i + 1], + }); + } + } + + return result; + }, [rects, x, y]); + + return ( +
+
+ {gaps.map((gap, i) => ( + { + setUxGapHover(gap.axis); + }} + onPointerLeave={() => { + setUxGapHover(undefined); + }} + /> + ))} +
+
+ ); +} + +function GapWithHandle({ + a, + b, + axis, + offset = cmath.vector2.zero, + highlighted, + onGapGestureStart, + className, + ...props +}: React.ComponentProps<"div"> & { + a: cmath.Rectangle; + b: cmath.Rectangle; + axis: cmath.Axis; + offset?: cmath.Vector2; + highlighted?: boolean; + onGapGestureStart?: (axis: cmath.Axis) => void; +}) { + const { gesture } = useGestureState(); + + const r = useMemo(() => { + const intersection = cmath.rect.axisProjectionIntersection([a, b], axis)!; + + if (!intersection) return null; + + let rect: cmath.Rectangle; + if (axis === "x") { + const x1 = a.x + a.width; + const y1 = intersection[0]; + const x2 = b.x; + const y2 = intersection[1]; + + rect = cmath.rect.fromPoints([ + [x1, y1], + [x2, y2], + ]); + } else { + const x1 = intersection[0]; + const y1 = a.y + a.height; + const x2 = intersection[1]; + const y2 = b.y; + + rect = cmath.rect.fromPoints([ + [x1, y1], + [x2, y2], + ]); + } + + return cmath.rect.translate(rect, cmath.vector2.invert(offset)); + }, [a, b, axis, offset]); + + const is_gesture = gesture.type === "gap"; + + if (!r) return null; + + return ( + <> +
+ + + {/* highlight pattern - only shown when gap is hovered, but hidden while dragging */} + {/* while dragging, tinted fill is applied */} + + +
+ +
+
+ + ); +} + +function GapHandle({ + axis, + onGapGestureStart, +}: { + axis: cmath.Axis; + onGapGestureStart?: (axis: cmath.Axis) => void; +}) { + const bind = useSurfaceGesture({ + onPointerDown: ({ event }) => { + event.preventDefault(); + }, + onDragStart: ({ event }) => { + event.preventDefault(); + onGapGestureStart?.(axis); + }, + }); + + return ( + + ); +} + +export function DistributeButton({ + axis, + onClick, +}: { + axis: cmath.Axis | undefined; + onClick?: (axis: cmath.Axis) => void; +}) { + if (!axis) { + return <>; + } + + return ( +
+ +
+ ); +} diff --git a/editor/grida-canvas-react/viewport/ui/surface-padding-overlay.tsx b/editor/grida-canvas-react/viewport/ui/surface-padding-overlay.tsx new file mode 100644 index 0000000000..b6b28552e9 --- /dev/null +++ b/editor/grida-canvas-react/viewport/ui/surface-padding-overlay.tsx @@ -0,0 +1,267 @@ +"use client"; + +import React, { useMemo, useState } from "react"; +import { useTransformState } from "../../provider"; +import cmath from "@grida/cmath"; +import { cn } from "@/components/lib/utils"; +import { useSurfaceGesture } from "../hooks/use-surface-gesture"; +import { SVGPatternDiagonalStripe } from "./svg-fill-patterns"; +import { useCurrentEditor, useEditorState } from "../../use-editor"; + +export function PaddingOverlay({ + containerRect, + padding, + offset, + style, + onPaddingGestureStart, +}: { + containerRect: cmath.Rectangle; + padding: { + top?: number; + right?: number; + bottom?: number; + left?: number; + }; + offset?: cmath.Vector2; + style?: React.CSSProperties; + onPaddingGestureStart?: (side: cmath.RectangleSide) => void; +}) { + const editor = useCurrentEditor(); + const { transform } = useTransformState(); + const [hoveredSide, setHoveredSide] = useState< + cmath.RectangleSide | undefined + >(undefined); + + const { padding_with_axis_mirroring_on, isDragging } = useEditorState( + editor, + (state) => ({ + padding_with_axis_mirroring_on: + state.gesture_modifiers.padding_with_axis_mirroring === "on", + isDragging: state.gesture.type === "padding", + }) + ); + + const isHighlighted = (side: cmath.RectangleSide): boolean => + side === hoveredSide || + !!( + padding_with_axis_mirroring_on && + hoveredSide && + cmath.rect.getOppositeSide(hoveredSide) === side + ); + + // Transform container rect to surface space for scaling + const surfaceRect = useMemo( + () => cmath.rect.transform(containerRect, transform), + [containerRect, transform] + ); + + const paddingRects = useMemo(() => { + const rects: Array<{ + rect: cmath.Rectangle; + side: cmath.RectangleSide; + }> = []; + + const { top = 0, right = 0, bottom = 0, left = 0 } = padding; + + // When inside LayerOverlay, position relative to (0,0) + // Calculate the base rect - either use offset adjustment or position from containerRect + const baseX = offset ? surfaceRect.x - offset[0] : 0; + const baseY = offset ? surfaceRect.y - offset[1] : 0; + const width = surfaceRect.width; + const height = surfaceRect.height; + + // Only show padding if it's greater than 0 + if (top > 0) { + rects.push({ + side: "top", + rect: { + x: baseX, + y: baseY, + width: width, + height: top * transform[1][1], // Scale by transform + }, + }); + } + + if (right > 0) { + rects.push({ + side: "right", + rect: { + x: baseX + width - right * transform[0][0], + y: baseY, + width: right * transform[0][0], + height: height, + }, + }); + } + + if (bottom > 0) { + rects.push({ + side: "bottom", + rect: { + x: baseX, + y: baseY + height - bottom * transform[1][1], + width: width, + height: bottom * transform[1][1], + }, + }); + } + + if (left > 0) { + rects.push({ + side: "left", + rect: { + x: baseX, + y: baseY, + width: left * transform[0][0], + height: height, + }, + }); + } + + return rects; + }, [surfaceRect, padding, transform, offset]); + + if (paddingRects.length === 0) return null; + + return ( +
+ {/* Define pattern once for all padding edges to avoid SVG pattern ID conflicts */} + + + <> + {/* Render regions (backgrounds + patterns) - lower z-index */} + {paddingRects.map((item, i) => ( + setHoveredSide(item.side)} + onPointerLeave={() => setHoveredSide(undefined)} + /> + ))} + + + <> + {!isDragging && ( + <> + {/* Render handles - higher z-index to always be accessible */} + {paddingRects.map((item, i) => ( + setHoveredSide(item.side)} + onPointerLeave={() => setHoveredSide(undefined)} + onPaddingGestureStart={onPaddingGestureStart} + /> + ))} + + )} + +
+ ); +} + +function PaddingEdgeRegion({ + side, + rect, + isHovered, + ...props +}: React.ComponentProps<"div"> & { + side: cmath.RectangleSide; + rect: cmath.Rectangle; + isHovered: boolean; +}) { + return ( +
+ {/* SVG Pattern - only visible when hovered */} + {isHovered && ( + + + + )} +
+ ); +} + +function PaddingHandle({ + side, + rect, + isHovered, + onPaddingGestureStart, + ...props +}: React.ComponentProps<"button"> & { + side: cmath.RectangleSide; + rect: cmath.Rectangle; + isHovered?: boolean; + onPaddingGestureStart?: (side: cmath.RectangleSide) => void; +}) { + const bind = useSurfaceGesture({ + onPointerDown: ({ event }) => { + event.preventDefault(); + }, + onDragStart: ({ event }) => { + event.preventDefault(); + onPaddingGestureStart?.(side); + }, + }); + + // Calculate absolute center position of the padding rect + const centerX = rect.x + rect.width / 2; + const centerY = rect.y + rect.height / 2; + + return ( + + ); +} diff --git a/editor/grida-canvas-react/viewport/ui/svg-fill-patterns.tsx b/editor/grida-canvas-react/viewport/ui/svg-fill-patterns.tsx index 0ef1e53ff2..e41e480f35 100644 --- a/editor/grida-canvas-react/viewport/ui/svg-fill-patterns.tsx +++ b/editor/grida-canvas-react/viewport/ui/svg-fill-patterns.tsx @@ -1,20 +1,52 @@ /** + * SVGPatternDiagonalStripe - Reusable diagonal stripe pattern component + * * @see https://gist.github.com/softmarshmallow/16f4b0ff551cbe0fed3d888dc3781493 - * @returns + * + * @warning IMPORTANT: Always provide a unique `id` prop to avoid pattern ID collisions! + * Each usage site should define its own pattern with a unique ID. + * + * @example + * ```tsx + * // In your component file (e.g., surface-padding-overlay.tsx) + * const PATTERN_ID = "padding-diagonal-stripes"; + * + * // Define the pattern once + * + * + * + * + * + * + * // Reference it + * + * ``` */ -export const DiagonalStripe = ({ +export const SVGPatternDiagonalStripe = ({ + id, + patternSpacing = 8, + patternWidth = 1, ...props -}: React.SVGProps) => { +}: React.SVGProps & { + id?: string; + patternSpacing?: number; + patternWidth?: number; +}) => { return ( - + ); }; diff --git a/editor/grida-canvas-react/viewport/ui/vector-region.tsx b/editor/grida-canvas-react/viewport/ui/vector-region.tsx index 52d711a344..9dba6da1f3 100644 --- a/editor/grida-canvas-react/viewport/ui/vector-region.tsx +++ b/editor/grida-canvas-react/viewport/ui/vector-region.tsx @@ -3,7 +3,7 @@ import { useGesture } from "@use-gesture/react"; import cmath from "@grida/cmath"; import { svg } from "@/grida-canvas-utils/svg"; import { SVGPathData, SVGCommand } from "svg-pathdata"; -import { DiagonalStripe } from "./svg-fill-patterns"; +import { SVGPatternDiagonalStripe } from "./svg-fill-patterns"; import type { VectorContentEditor } from "@/grida-canvas-react/use-sub-vector-network-editor"; interface RegionSegment { @@ -95,14 +95,18 @@ export function VectorRegion({ pointerEvents: disabled ? "none" : "auto", }} > - + & { selection: string | string[]; }) + | Pick | Pick | Pick< editor.gesture.GestureCurve, diff --git a/editor/grida-canvas/editor.i.ts b/editor/grida-canvas/editor.i.ts index a6616250eb..e0fc37a4d4 100644 --- a/editor/grida-canvas/editor.i.ts +++ b/editor/grida-canvas/editor.i.ts @@ -407,6 +407,7 @@ export namespace editor.config { path_keep_projecting: "off", rotate_with_quantize: "off", curve_tangent_mirroring: "auto", + padding_with_axis_mirroring: "off", }; export const DEFAULT_BRUSH: state.CurrentBrush = { @@ -790,6 +791,18 @@ export namespace editor.state { * @default "auto" */ curve_tangent_mirroring: vn.TangentMirroringMode; + /** + * Mirror padding changes across the same axis + * + * When on, changing one padding side also updates its opposite side: + * - left ↔ right (horizontal mirroring) + * - top ↔ bottom (vertical mirroring) + * + * Typically toggled when the alt/option key is pressed + * + * @default "off" + */ + padding_with_axis_mirroring: "on" | "off"; }; export interface IViewportTransformState { @@ -1571,6 +1584,7 @@ export namespace editor.gesture { | GestureTranslate | GestureSort | GestureGap + | GesturePadding | GestureInsertAndResize | GestureScale | GestureRotate @@ -1730,6 +1744,18 @@ export namespace editor.gesture { layout: LayoutSnapshot; }; + export type GesturePadding = IGesture & { + readonly type: "padding"; + + readonly node_id: string; + readonly side: "top" | "right" | "bottom" | "left"; + + readonly min_padding: number; + readonly initial_padding: number; + + padding: number; + }; + export type GestureScale = IGesture & { // scale (resize) readonly type: "scale"; @@ -2658,7 +2684,135 @@ export namespace editor.api { // distributeEvenly(target: "selection" | NodeID[], axis: "x" | "y"): void; + + /** + * Wraps selected nodes into new flex containers with automatically detected layout properties. + * + * This command analyzes the spatial arrangement of the selected nodes and creates flex containers + * that preserve their visual appearance while enabling responsive layout behavior. Nodes are grouped + * by their parent, and each group gets its own container. + * + * **Key Behavior:** + * - **Automatic detection**: Analyzes node positions to determine flex direction, spacing, and alignment + * - **Parent grouping**: Nodes with different parents are wrapped into separate containers + * - **Visual preservation**: Maintains the exact visual appearance after wrapping + * - **Smart defaults**: Applies contextual padding (16px for single child, 0 for multiple) + * + * **Bound to**: SHIFT+A + * + * @param target - The nodes to wrap: + * - `"selection"` - Wraps currently selected nodes + * - `NodeID[]` - Wraps specific node IDs + * + * @example + * ```typescript + * // Wrap currently selected nodes into flex containers + * editor.commands.autoLayout("selection"); + * + * // Wrap specific nodes + * editor.commands.autoLayout(["node-1", "node-2", "node-3"]); + * ``` + * + * @remarks + * **Layout Detection Algorithm:** + * The command analyzes the bounding rectangles of selected nodes to determine: + * - **Direction**: Horizontal or vertical based on primary alignment axis + * - **Spacing**: Gap between nodes (mainAxisGap and crossAxisGap) + * - **Alignment**: Main and cross axis alignment based on distribution + * - **Order**: Maintains visual order of nodes within the container + * + * **Parent Grouping:** + * Nodes are automatically grouped by their parent container. For example: + * - Nodes A, B under parent X → Container 1 in X + * - Nodes C, D under parent Y → Container 2 in Y + * - Root nodes E, F → Container 3 at root + * + * **Container Properties:** + * Each created container has: + * - `layout: "flex"` + * - `width: "auto"`, `height: "auto"` + * - Auto-detected `direction`, `mainAxisGap`, `crossAxisGap` + * - Auto-detected `mainAxisAlignment`, `crossAxisAlignment` + * - `padding: 16` (single child) or `0` (multiple children) + * - `position: "absolute"` + * + * **Child Updates:** + * All wrapped children are updated to: + * - `position: "relative"` + * - `top`, `right`, `bottom`, `left` are cleared (undefined) + * + * @see {@link reLayout} - To change an existing container's layout mode + * @see {@link contain} - To wrap nodes in a basic container without auto-layout + */ autoLayout(target: "selection" | NodeID[]): void; + + /** + * Re-applies layout mode to an existing container and automatically configures its children. + * + * Similar to {@link autoLayout}, but operates on an existing container instead of creating a new one. + * While `autoLayout` wraps selected nodes into a new flex container, `reLayout` changes an + * existing container's layout and updates its children accordingly. + * + * Unlike {@link changeContainerNodeLayout}, which only updates the parent's layout property, + * this method also configures the positioning and constraints of all direct children to ensure + * visual consistency is maintained during the layout transition. + * + * **Key Behavior:** + * - **Idempotent**: No-op if the layout is already in the desired state + * - **Visual preservation**: Maintains exact visual appearance during transitions + * - **Container-only**: Requires the target node to be a container type + * + * @param node_id - The container node to re-layout (must be type "container") + * @param layout - The layout mode to apply: + * - `"normal"` - Absolute positioning (flow layout) + * - `"flex-row"` - Horizontal flexbox layout + * - `"flex-column"` - Vertical flexbox layout + * + * @throws {AssertionError} If the target node is not a container + * + * @example + * ```typescript + * // Convert a normal container to horizontal flex layout + * editor.commands.reLayout("container-id", "flex-row"); + * + * // Convert to vertical flex layout + * editor.commands.reLayout("container-id", "flex-column"); + * + * // Convert a flex container back to normal (flow) layout + * editor.commands.reLayout("container-id", "normal"); + * + * // No-op - already in desired state + * editor.commands.reLayout("container-id", "flex-row"); // Already flex-row + * editor.commands.reLayout("container-id", "flex-row"); // Does nothing + * ``` + * + * @remarks + * **When changing from normal to flex layout (`"flex-row"` or `"flex-column"`):** + * - Internally calls {@link autoLayout} on the container's children + * - Analyzes spatial arrangement to detect optimal flex properties + * - Applies auto-detected gap, alignment, and direction to the container + * - Converts children to relative positioning + * - Preserves exact visual appearance + * + * **When changing from flex to normal layout (`"normal"`):** + * - Captures current absolute positions of all children + * - Removes all flex-related properties from the container (`layout`, `direction`, `mainAxisGap`, `crossAxisGap`, `mainAxisAlignment`, `crossAxisAlignment`, `layoutWrap`) + * - Converts children to absolute positioning with calculated positions + * - Positions are relative to parent's bounding box + * - Preserves exact visual appearance + * + * **Visual Consistency:** + * Both transitions ensure that the rendered output remains visually identical + * before and after the operation. Only the internal layout mechanism changes. + * + * @see {@link autoLayout} - To wrap nodes into a new flex container + * @see {@link changeContainerNodeLayout} - To only change the layout property + */ + reLayout( + node_id: NodeID, + layout: "normal" | "flex-row" | "flex-column" + ): void; + contain(target: "selection" | NodeID[]): void; /** @@ -2834,6 +2988,7 @@ export namespace editor.api { node_id: NodeID, layout: grida.program.nodes.i.IFlexContainer["layout"] ): void; + changeFlexContainerNodeDirection(node_id: string, direction: cg.Axis): void; changeFlexContainerNodeMainAxisAlignment( node_id: string, @@ -2847,6 +3002,7 @@ export namespace editor.api { node_id: string, gap: number | { mainAxisGap: number; crossAxisGap: number } ): void; + changeFlexContainerNodeWrap(node_id: string, wrap: "wrap" | "nowrap"): void; changeNodeFilterEffects(node_id: NodeID, effects?: cg.FilterEffect[]): void; changeNodeFeShadows(node_id: NodeID, effect?: cg.FeShadow[]): void; @@ -3045,6 +3201,10 @@ export namespace editor.api { node_id: string ): void; surfaceStartGapGesture(selection: string | string[], axis: "x" | "y"): void; + surfaceStartPaddingGesture( + node_id: string, + side: "top" | "right" | "bottom" | "left" + ): void; surfaceStartCornerRadiusGesture( selection: string, anchor?: cmath.IntercardinalDirection @@ -3164,6 +3324,18 @@ export namespace editor.api { surfaceConfigurePathKeepProjectingModifier( path_keep_projecting: "on" | "off" ): void; + /** + * Toggles whether padding gestures mirror changes across the same axis. + * + * When set to `"on"`, changing one padding side also updates its opposite: + * - Changing left also changes right (horizontal mirroring) + * - Changing top also changes bottom (vertical mirroring) + * + * Typically toggled when the alt/option key is pressed during a padding gesture. + */ + surfaceConfigurePaddingWithMirroringModifier( + padding_with_axis_mirroring: "on" | "off" + ): void; // } diff --git a/editor/grida-canvas/editor.ts b/editor/grida-canvas/editor.ts index 3fc8cd943e..8269c09cac 100644 --- a/editor/grida-canvas/editor.ts +++ b/editor/grida-canvas/editor.ts @@ -1157,10 +1157,154 @@ class EditorDocumentStore public autoLayout(target: "selection" | editor.NodeID[]) { this.dispatch({ type: "autolayout", + contain: true, target, }); } + public reLayout( + node_id: string, + layout: "normal" | "flex-row" | "flex-column" + ): void { + const node = this.getNodeSnapshotById(node_id); + assert( + node.type === "container", + `reLayout requires a container node, got ${node.type}` + ); + + // cases + // none: no changes to be made + // clear: any => no layout (make things absolute) + // flex: non-flex => flex (apply flex layout, use autoLayout) + // flex-direction-switch: flex => flex { direction } (change only the direction) + type RelayoutAction = + | { type: "none" } + | { type: "clear" } + | { type: "flex"; direction: "horizontal" | "vertical" } + | { type: "flex-direction-switch"; direction: "horizontal" | "vertical" }; + + const currentLayout = (node as grida.program.nodes.ContainerNode).layout; + const currentDirection = (node as grida.program.nodes.ContainerNode) + .direction; + + // Compute the action type + const action: RelayoutAction = (() => { + // Check if layout is already applied + const isAlreadyApplied = + (layout === "normal" && currentLayout !== "flex") || + (layout === "flex-row" && + currentLayout === "flex" && + currentDirection === "horizontal") || + (layout === "flex-column" && + currentLayout === "flex" && + currentDirection === "vertical"); + + if (isAlreadyApplied) { + return { type: "none" }; + } + + // Converting to normal layout (clearing flex) + if (layout === "normal") { + return { type: "clear" }; + } + + // Converting to flex layout + const direction = layout === "flex-row" ? "horizontal" : "vertical"; + + // If already flex, just switching direction + if (currentLayout === "flex") { + return { type: "flex-direction-switch", direction }; + } + + // Converting from non-flex to flex + return { type: "flex", direction }; + })(); + + // Handle each action type + switch (action.type) { + case "none": + // No-op if already in desired state + return; + + case "clear": { + // [layout => no layout] - convert flex to absolute positioning + const children = dq.getChildren(this.state.document_ctx, node_id); + + // Get current absolute positions of all children + const childrenWithRects = children + .map((child_id) => { + const rect = this.geometry.getNodeAbsoluteBoundingRect(child_id); + return rect ? { id: child_id, rect } : null; + }) + .filter((item): item is NonNullable => item !== null); + + // Get parent's absolute position to calculate relative offsets + const parentRect = this.geometry.getNodeAbsoluteBoundingRect(node_id); + assert(parentRect, "Parent rect not found"); + + // Update parent to remove flex layout + this.dispatch([ + { + type: "node/change/*", + node_id: node_id, + layout: "flow", + direction: undefined, + mainAxisGap: undefined, + crossAxisGap: undefined, + mainAxisAlignment: undefined, + crossAxisAlignment: undefined, + layoutWrap: undefined, + }, + ]); + + // Update each child to absolute positioning with calculated positions + childrenWithRects.forEach(({ id, rect }) => { + // Calculate position relative to parent + const relativeLeft = rect.x - parentRect.x; + const relativeTop = rect.y - parentRect.y; + + this.changeNodePropertyPositioning(id, { + position: "absolute", + left: cmath.quantize(relativeLeft, 1), + top: cmath.quantize(relativeTop, 1), + right: undefined, + bottom: undefined, + }); + }); + break; + } + + case "flex": { + // [no layout => layout] - use autoLayout + const children = dq.getChildren(this.state.document_ctx, node_id); + + if (children.length === 0) { + // If no children, just set the layout properties + this.changeContainerNodeLayout(node_id, "flex"); + this.changeFlexContainerNodeDirection(node_id, action.direction); + return; + } + + // Use autoLayout with contain: false to apply layout to existing container + this.dispatch({ + type: "autolayout", + contain: false, + target: node_id, + }); + + // Ensure the direction matches the requested layout + // (autolayout guesses the direction, but we want to enforce the specific one) + this.changeFlexContainerNodeDirection(node_id, action.direction); + break; + } + + case "flex-direction-switch": + // Just switch the direction + this.changeFlexContainerNodeDirection(node_id, action.direction); + break; + } + } + public contain(target: "selection" | editor.NodeID[]) { this.dispatch({ type: "contain", @@ -1990,6 +2134,13 @@ class EditorDocumentStore crossAxisGap: typeof gap === "number" ? gap : gap.crossAxisGap, }); } + changeFlexContainerNodeWrap(node_id: string, layoutWrap: "wrap" | "nowrap") { + this.dispatch({ + type: "node/change/*", + node_id: node_id, + layoutWrap, + }); + } // changeNodePropertyMouseCursor(node_id: string, cursor: cg.SystemMouseCursor) { this.dispatch({ @@ -3358,6 +3509,15 @@ export class EditorSurface }); } + public surfaceConfigurePaddingWithMirroringModifier( + padding_with_axis_mirroring: "on" | "off" + ) { + this.dispatch({ + type: "config/modifiers/padding-with-mirroring", + padding_with_axis_mirroring, + }); + } + // #region IPixelGridActions implementation surfaceConfigurePixelGrid(state: "on" | "off") { this.dispatch({ @@ -3551,6 +3711,20 @@ export class EditorSurface }); } + surfaceStartPaddingGesture( + node_id: string, + side: "top" | "right" | "bottom" | "left" + ) { + this._editor.doc.dispatch({ + type: "surface/gesture/start", + gesture: { + type: "padding", + node_id, + side, + }, + }); + } + // #region drag resize handle surfaceStartCornerRadiusGesture( selection: string, diff --git a/editor/grida-canvas/query/__tests__/query.test.ts b/editor/grida-canvas/query/__tests__/query.test.ts index 8f2ffc6a15..8d7b248b77 100644 --- a/editor/grida-canvas/query/__tests__/query.test.ts +++ b/editor/grida-canvas/query/__tests__/query.test.ts @@ -141,4 +141,18 @@ describe("query selectors", () => { expect(dq.querySelector(ctx, ["c"], ["a", "b"])).toEqual(["a", "b"]); }); }); + + describe("getTopSceneContentNode", () => { + test("returns the direct child when node is under scene", () => { + expect(dq.getTopIdWithinScene(ctx, "a", "root")).toBe("a"); + }); + + test("returns highest ancestor that is still under the scene", () => { + expect(dq.getTopIdWithinScene(ctx, "a1", "root")).toBe("a"); + }); + + test("returns null when querying the scene itself", () => { + expect(dq.getTopIdWithinScene(ctx, "root", "root")).toBeNull(); + }); + }); }); diff --git a/editor/grida-canvas/query/index.ts b/editor/grida-canvas/query/index.ts index a011580237..ee2f632054 100644 --- a/editor/grida-canvas/query/index.ts +++ b/editor/grida-canvas/query/index.ts @@ -464,11 +464,15 @@ export namespace dq { return context.lu_parent[node_id] ?? null; } - export function getTopId( + /** + * @deprecated you won't need this. will be removed. + * use this only, when explicitly need to get the top node id, that includes the scene itself. + */ + export function getRootId( context: grida.program.document.internal.INodesRepositoryRuntimeHierarchyContext, node_id: string ): NodeID | null { - // veryfi if exists + // verify if exists if (context.lu_keys.includes(node_id)) { const ancestors = getAncestors(context, node_id); return ancestors[0] ?? node_id; @@ -477,6 +481,65 @@ export namespace dq { } } + /** + * Retrieves the top-level node within a scene's hierarchy for a given node. + * + * This function finds the highest-level ancestor of a node that is still a direct child + * of the specified scene. It's useful for identifying which root-level node within a + * scene contains a deeply nested node. + * + * @param context - The runtime hierarchy context containing node relationships. + * @param node_id - The ID of the node to query. + * @param scene_id - The ID of the scene to scope the query within. + * @returns The top-level node ID within the scene, or `null` if: + * - The node doesn't exist + * - The node is the scene itself + * + * @example + * ```ts + * // Hierarchy: scene -> containerA -> containerB -> textNode + * const context = { + * lu_keys: ["scene", "containerA", "containerB", "textNode"], + * lu_parent: { + * "scene": null, + * "containerA": "scene", + * "containerB": "containerA", + * "textNode": "containerB" + * } + * }; + * + * // Get top node for deeply nested textNode + * const topId = getTopIdWithinScene(context, "textNode", "scene"); + * console.log(topId); // "containerA" (direct child of scene) + * + * // Get top node for root-level node + * const topId = getTopIdWithinScene(context, "containerA", "scene"); + * console.log(topId); // "containerA" (already at top) + * + * // Node is the scene itself + * const topId = getTopIdWithinScene(context, "scene", "scene"); + * console.log(topId); // null + * ``` + */ + export function getTopIdWithinScene( + context: grida.program.document.internal.INodesRepositoryRuntimeHierarchyContext, + node_id: string, + scene_id: string + ): NodeID | null { + if (!context.lu_keys.includes(node_id) || node_id === scene_id) { + return null; + } + + const ancestors = getAncestors(context, node_id); + const sceneIndex = ancestors.indexOf(scene_id); + + // Not under scene: no top id within this scene + if (sceneIndex === -1) return null; + + // Return the child of the scene (next node after scene in ancestors) + return ancestors[sceneIndex + 1] ?? node_id; + } + /** * @internal * @param state - state or draft diff --git a/editor/grida-canvas/reducers/document.reducer.ts b/editor/grida-canvas/reducers/document.reducer.ts index 48fc5188ee..8be14d7b71 100644 --- a/editor/grida-canvas/reducers/document.reducer.ts +++ b/editor/grida-canvas/reducers/document.reducer.ts @@ -1149,8 +1149,12 @@ export default function documentReducer( if (target_node_ids.length === 1) { // if a single node is selected, align it with its container. (if not root) const node_id = target_node_ids[0]; - const top_id = dq.getTopId(state.document_ctx, node_id); - if (node_id !== top_id) { + const top_id = dq.getTopIdWithinScene( + state.document_ctx, + node_id, + state.scene_id + ); + if (top_id && node_id !== top_id) { const parent_node_id = dq.getParentId(state.document_ctx, node_id); assert(parent_node_id, "parent node not found"); @@ -1245,30 +1249,27 @@ export default function documentReducer( break; } case "autolayout": { - const { target } = action; - const target_node_ids = target === "selection" ? state.selection : target; + const { contain } = action; - // group by parent, including root nodes - const groups = Object.groupBy( - target_node_ids, - (node_id) => - dq.getParentId(state.document_ctx, node_id) ?? state.scene_id! - ); - - const layouts = Object.keys(groups).map((parent_id) => { - const g = groups[parent_id]!; - const is_scene = parent_id === state.scene_id; + // [contain: false] - apply layout to existing container + if (!contain) { + const container_id = action.target; + const container_node = dq.__getNodeById(state, container_id); + assert( + container_node.type === "container", + `autolayout with contain: false requires a container node, got ${container_node.type}` + ); - let delta: cmath.Vector2; - if (is_scene) { - delta = [0, 0]; - } else { - const parent_rect = - context.geometry.getNodeAbsoluteBoundingRect(parent_id)!; - delta = [-parent_rect.x, -parent_rect.y]; + const children = dq.getChildren(state.document_ctx, container_id); + if (children.length === 0) { + return state; // no-op if no children } - const rects = g + const container_rect = + context.geometry.getNodeAbsoluteBoundingRect(container_id)!; + const delta: cmath.Vector2 = [-container_rect.x, -container_rect.y]; + + const rects = children .map( (node_id) => context.geometry.getNodeAbsoluteBoundingRect(node_id)! ) @@ -1279,49 +1280,24 @@ export default function documentReducer( // guess the layout const lay = layout.flex.guess(rects); - return { - parent: is_scene ? null : parent_id, - layout: lay, - children: g, - }; - }); - - return updateState(state, (draft) => { - const insertions: grida.program.nodes.NodeID[] = []; - layouts.forEach(({ parent, layout, children }) => { - const container_prototype: grida.program.nodes.NodePrototype = { - type: "container", - // layout - layout: "flex", - width: "auto", - height: "auto", - top: cmath.quantize(layout.union.y, 1), - left: cmath.quantize(layout.union.x, 1), - direction: layout.direction, - mainAxisGap: cmath.quantize(layout.spacing, 1), - crossAxisGap: cmath.quantize(layout.spacing, 1), - mainAxisAlignment: layout.mainAxisAlignment, - crossAxisAlignment: layout.crossAxisAlignment, - padding: children.length === 1 ? 16 : 0, - // children (empty when init) - children: [], - // position - position: "absolute", - }; - - const container_id = self_insertSubDocument( + return updateState(state, (draft) => { + const container = dq.__getNodeById( draft, - parent, - grida.program.nodes.factory.create_packed_scene_document_from_prototype( - container_prototype, - () => context.idgen.next() - ) - )[0]; - - // [move children to container] - const ordered = layout.orders.map((i) => children[i]); - ordered.forEach((child_id) => { - self_moveNode(draft, child_id, container_id); + container_id + ) as grida.program.nodes.ContainerNode; + + // Apply flex layout properties to the existing container + container.layout = "flex"; + container.direction = lay.direction; + container.mainAxisGap = cmath.quantize(lay.spacing, 1); + container.crossAxisGap = cmath.quantize(lay.spacing, 1); + container.mainAxisAlignment = lay.mainAxisAlignment; + container.crossAxisAlignment = lay.crossAxisAlignment; + + // [reorder children according to guessed layout] + const ordered = lay.orders.map((i) => children[i]); + ordered.forEach((child_id, index) => { + self_moveNode(draft, child_id, container_id, index); }); // [reset children position] @@ -1339,11 +1315,113 @@ export default function documentReducer( }; }); - insertions.push(container_id); + self_selectNode(draft, "reset", container_id); }); + } + // [contain: true] - wrap nodes in new container(s) + else { + const { target } = action; + const target_node_ids = + target === "selection" ? state.selection : target; - self_selectNode(draft, "reset", ...insertions); - }); + // group by parent, including root nodes + const groups = Object.groupBy( + target_node_ids, + (node_id) => + dq.getParentId(state.document_ctx, node_id) ?? state.scene_id! + ); + + const layouts = Object.keys(groups).map((parent_id) => { + const g = groups[parent_id]!; + const is_scene = parent_id === state.scene_id; + + let delta: cmath.Vector2; + if (is_scene) { + delta = [0, 0]; + } else { + const parent_rect = + context.geometry.getNodeAbsoluteBoundingRect(parent_id)!; + delta = [-parent_rect.x, -parent_rect.y]; + } + + const rects = g + .map( + (node_id) => + context.geometry.getNodeAbsoluteBoundingRect(node_id)! + ) + // make the rects relative to the parent + .map((rect) => cmath.rect.translate(rect, delta)) + .map((rect) => cmath.rect.quantize(rect, 1)); + + // guess the layout + const lay = layout.flex.guess(rects); + + return { + parent: is_scene ? null : parent_id, + layout: lay, + children: g, + }; + }); + + return updateState(state, (draft) => { + const insertions: grida.program.nodes.NodeID[] = []; + layouts.forEach(({ parent, layout, children }) => { + const container_prototype: grida.program.nodes.NodePrototype = { + type: "container", + // layout + layout: "flex", + width: "auto", + height: "auto", + top: cmath.quantize(layout.union.y, 1), + left: cmath.quantize(layout.union.x, 1), + direction: layout.direction, + mainAxisGap: cmath.quantize(layout.spacing, 1), + crossAxisGap: cmath.quantize(layout.spacing, 1), + mainAxisAlignment: layout.mainAxisAlignment, + crossAxisAlignment: layout.crossAxisAlignment, + padding: children.length === 1 ? 16 : 0, + // children (empty when init) + children: [], + // position + position: "absolute", + }; + + const container_id = self_insertSubDocument( + draft, + parent, + grida.program.nodes.factory.create_packed_scene_document_from_prototype( + container_prototype, + () => context.idgen.next() + ) + )[0]; + + // [move children to container] + const ordered = layout.orders.map((i) => children[i]); + ordered.forEach((child_id) => { + self_moveNode(draft, child_id, container_id); + }); + + // [reset children position] + ordered.forEach((child_id) => { + const child = dq.__getNodeById(draft, child_id); + (draft.document.nodes[ + child_id + ] as grida.program.nodes.i.IPositioning) = { + ...child, + position: "relative", + top: undefined, + right: undefined, + bottom: undefined, + left: undefined, + }; + }); + + insertions.push(container_id); + }); + + self_selectNode(draft, "reset", ...insertions); + }); + } break; } diff --git a/editor/grida-canvas/reducers/event-target.reducer.ts b/editor/grida-canvas/reducers/event-target.reducer.ts index a03493c883..f719aeaff5 100644 --- a/editor/grida-canvas/reducers/event-target.reducer.ts +++ b/editor/grida-canvas/reducers/event-target.reducer.ts @@ -756,6 +756,84 @@ function __self_evt_on_drag( } } + break; + } + case "padding": { + const { node_id, side, initial_padding, min_padding } = draft.gesture; + const delta = movement[side === "top" || side === "bottom" ? 1 : 0]; + + const padding = cmath.quantize( + Math.max(initial_padding + delta, min_padding), + 1 + ); + + const container = dq.__getNodeById(draft, node_id); + if ( + container && + container.type === "container" && + "padding" in container + ) { + const currentPadding = container.padding; + let newPadding: grida.program.nodes.i.IPadding["padding"]; + + if (typeof currentPadding === "number") { + // Convert uniform padding to individual sides + newPadding = { + paddingTop: currentPadding, + paddingRight: currentPadding, + paddingBottom: currentPadding, + paddingLeft: currentPadding, + }; + } else { + // Use existing individual padding values + newPadding = { + paddingTop: currentPadding.paddingTop, + paddingRight: currentPadding.paddingRight, + paddingBottom: currentPadding.paddingBottom, + paddingLeft: currentPadding.paddingLeft, + }; + } + + const mirroringEnabled = + draft.gesture_modifiers.padding_with_axis_mirroring === "on"; + + // Update the specific side + switch (side) { + case "top": + newPadding.paddingTop = padding; + if (mirroringEnabled) { + newPadding.paddingBottom = padding; + } + break; + case "right": + newPadding.paddingRight = padding; + if (mirroringEnabled) { + newPadding.paddingLeft = padding; + } + break; + case "bottom": + newPadding.paddingBottom = padding; + if (mirroringEnabled) { + newPadding.paddingTop = padding; + } + break; + case "left": + newPadding.paddingLeft = padding; + if (mirroringEnabled) { + newPadding.paddingRight = padding; + } + break; + } + + draft.document.nodes[node_id] = nodeReducer(container, { + type: "node/change/*", + node_id: node_id, + padding: newPadding, + }); + + draft.gesture.padding = padding; + } + break; } } diff --git a/editor/grida-canvas/reducers/index.ts b/editor/grida-canvas/reducers/index.ts index 73820d0106..6b61b31064 100644 --- a/editor/grida-canvas/reducers/index.ts +++ b/editor/grida-canvas/reducers/index.ts @@ -187,6 +187,12 @@ function _reducer( action.curve_tangent_mirroring; }); } + case "config/modifiers/padding-with-mirroring": { + return updateState(state, (draft: Draft) => { + draft.gesture_modifiers.padding_with_axis_mirroring = + action.padding_with_axis_mirroring; + }); + } case "config/modifiers/path-keep-projecting": { return updateState(state, (draft: Draft) => { draft.gesture_modifiers.path_keep_projecting = diff --git a/editor/grida-canvas/reducers/methods/transform.ts b/editor/grida-canvas/reducers/methods/transform.ts index 770c39c813..c09819c847 100644 --- a/editor/grida-canvas/reducers/methods/transform.ts +++ b/editor/grida-canvas/reducers/methods/transform.ts @@ -87,6 +87,7 @@ export function self_update_gesture_transform< if (draft.gesture.type === "translate-vector-controls") return; if (draft.gesture.type === "curve") return; if (draft.gesture.type === "gap") return; + if (draft.gesture.type === "padding") return; if (draft.gesture.type === "brush") return; if (draft.gesture.type === "guide") return; diff --git a/editor/grida-canvas/reducers/node.reducer.ts b/editor/grida-canvas/reducers/node.reducer.ts index 161373104b..1b0bc02403 100644 --- a/editor/grida-canvas/reducers/node.reducer.ts +++ b/editor/grida-canvas/reducers/node.reducer.ts @@ -613,6 +613,12 @@ const safe_properties: Partial< (draft as UN).crossAxisGap = value; }, }), + layoutWrap: defineNodeProperty<"layoutWrap">({ + assert: (node) => node.type === "container" || node.type === "component", + apply: (draft, value, prev) => { + (draft as UN).layoutWrap = value; + }, + }), textAlign: defineNodeProperty<"textAlign">({ assert: (node) => node.type === "text", apply: (draft, value, prev) => { diff --git a/editor/grida-canvas/reducers/surface.reducer.ts b/editor/grida-canvas/reducers/surface.reducer.ts index 8791fc36d9..50a1b2b744 100644 --- a/editor/grida-canvas/reducers/surface.reducer.ts +++ b/editor/grida-canvas/reducers/surface.reducer.ts @@ -813,6 +813,55 @@ function __self_start_gesture( // } + break; + } + case "padding": { + const { node_id, side } = gesture; + + const container = dq.__getNodeById(draft, node_id); + assert( + container && container.type === "container", + "the selection is not a container" + ); + + const currentPadding = container.padding; + let currentValue: number = 0; // Default to 0 if padding is undefined + + if (currentPadding === undefined || currentPadding === null) { + // Padding is not defined, use default + currentValue = 0; + } else if (typeof currentPadding === "number") { + currentValue = currentPadding; + } else { + // Padding is an object with per-side values + switch (side) { + case "top": + currentValue = currentPadding.paddingTop ?? 0; + break; + case "right": + currentValue = currentPadding.paddingRight ?? 0; + break; + case "bottom": + currentValue = currentPadding.paddingBottom ?? 0; + break; + case "left": + currentValue = currentPadding.paddingLeft ?? 0; + break; + } + } + + draft.gesture = { + type: "padding", + node_id, + side, + min_padding: 0, + initial_padding: Math.max(0, currentValue), + padding: Math.max(0, currentValue), + movement: cmath.vector2.zero, + first: cmath.vector2.zero, + last: cmath.vector2.zero, + }; + break; } } diff --git a/editor/grida-canvas/reducers/tools/target.ts b/editor/grida-canvas/reducers/tools/target.ts index a7219024f8..c37c9f060f 100644 --- a/editor/grida-canvas/reducers/tools/target.ts +++ b/editor/grida-canvas/reducers/tools/target.ts @@ -41,7 +41,13 @@ export function getRayTarget( const filtered = hits .filter((node_id) => { const node = nodes[node_id]; - const top_id = dq.getTopId(context.document_ctx, node_id); + const top_id = context.scene_id + ? dq.getTopIdWithinScene( + context.document_ctx, + node_id, + context.scene_id + ) + : dq.getRootId(context.document_ctx, node_id); const maybeichildren = context.document.links[node_id]; // Check if this is a root node with children that should be ignored @@ -127,7 +133,9 @@ export function getMarqueeSelection( // 2. shall not be a locked node // 3. the parent of this node shall also be hit by the marquee (unless it's the root node) const target_node_ids = hits.filter((hit_id) => { - const root_id = dq.getTopId(document_ctx, hit_id)!; + const root_id = state.scene_id + ? dq.getTopIdWithinScene(document_ctx, hit_id, state.scene_id) + : dq.getRootId(document_ctx, hit_id); const hit = dq.__getNodeById(state, hit_id); // (1) shall not be a root node (if configured) @@ -147,19 +155,23 @@ export function getMarqueeSelection( if (hit.locked) return false; // (3). the parent of this node shall also be hit by the marquee (unless it's the root node) - const parent_id = dq.getParentId(document_ctx, hit_id)!; + const parent_id = dq.getParentId(document_ctx, hit_id); - // root node - if (parent_id === null) { + // Direct child of scene (root level) - always include + if (parent_id === null || parent_id === state.scene_id) { return true; - } else { - if (parent_id === root_id) return true; - if (!hits.includes(parent_id)) return false; } - const parent = dq.__getNodeById(state, parent_id!); - if (!parent) return false; - if (parent.locked) return false; + // Nested node - parent must also be hit + if (!hits.includes(parent_id)) { + return false; + } + + // Check if parent is locked + const parent = dq.__getNodeById(state, parent_id); + if (!parent || parent.locked) { + return false; + } return true; }); diff --git a/editor/public/examples/canvas/blank.grida b/editor/public/examples/canvas/blank.grida index 95f9433311..d19fc03112 100644 --- a/editor/public/examples/canvas/blank.grida +++ b/editor/public/examples/canvas/blank.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "blank": { diff --git a/editor/public/examples/canvas/component-01.grida b/editor/public/examples/canvas/component-01.grida index acebd02ba5..7e3749f9d8 100644 --- a/editor/public/examples/canvas/component-01.grida +++ b/editor/public/examples/canvas/component-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "component": { diff --git a/editor/public/examples/canvas/event-page-01.grida b/editor/public/examples/canvas/event-page-01.grida index 663b6dabc1..5363d69958 100644 --- a/editor/public/examples/canvas/event-page-01.grida +++ b/editor/public/examples/canvas/event-page-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "291:302": { diff --git a/editor/public/examples/canvas/globals-01.grida b/editor/public/examples/canvas/globals-01.grida index 296c45f132..fedfac208a 100644 --- a/editor/public/examples/canvas/globals-01.grida +++ b/editor/public/examples/canvas/globals-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "root": { diff --git a/editor/public/examples/canvas/helloworld.grida b/editor/public/examples/canvas/helloworld.grida index 2e6d5bc6ea..f80d861edf 100644 --- a/editor/public/examples/canvas/helloworld.grida +++ b/editor/public/examples/canvas/helloworld.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "454:341": { diff --git a/editor/public/examples/canvas/instagram-post-01.grida b/editor/public/examples/canvas/instagram-post-01.grida index 98310f1562..26f9ea2dd5 100644 --- a/editor/public/examples/canvas/instagram-post-01.grida +++ b/editor/public/examples/canvas/instagram-post-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "202:2": { @@ -307,7 +308,7 @@ "paths": [ { "d": "M8.33333 0C3.725 0 0 3.75 0 8.33333C0 10.5435 0.877974 12.6631 2.44078 14.2259C3.2146 14.9997 4.13326 15.6135 5.1443 16.0323C6.15535 16.4511 7.23898 16.6667 8.33333 16.6667C10.5435 16.6667 12.6631 15.7887 14.2259 14.2259C15.7887 12.6631 16.6667 10.5435 16.6667 8.33333C16.6667 7.23898 16.4511 6.15535 16.0323 5.1443C15.6135 4.13326 14.9997 3.2146 14.2259 2.44078C13.4521 1.66696 12.5334 1.05313 11.5224 0.634337C10.5113 0.215548 9.42768 1.11022e-15 8.33333 0L8.33333 0ZM11.25 5C11.5815 5 11.8995 5.1317 12.1339 5.36612C12.3683 5.60054 12.5 5.91848 12.5 6.25C12.5 6.58152 12.3683 6.89946 12.1339 7.13388C11.8995 7.3683 11.5815 7.5 11.25 7.5C10.9185 7.5 10.6005 7.3683 10.3661 7.13388C10.1317 6.89946 10 6.58152 10 6.25C10 5.91848 10.1317 5.60054 10.3661 5.36612C10.6005 5.1317 10.9185 5 11.25 5L11.25 5ZM5.41667 5C5.74819 5 6.06613 5.1317 6.30055 5.36612C6.53497 5.60054 6.66667 5.91848 6.66667 6.25C6.66667 6.58152 6.53497 6.89946 6.30055 7.13388C6.06613 7.3683 5.74819 7.5 5.41667 7.5C5.08515 7.5 4.7672 7.3683 4.53278 7.13388C4.29836 6.89946 4.16667 6.58152 4.16667 6.25C4.16667 5.91848 4.29836 5.60054 4.53278 5.36612C4.7672 5.1317 5.08515 5 5.41667 5ZM8.33333 12.9167C6.39167 12.9167 4.74167 11.7 4.075 10L12.5917 10C11.9167 11.7 10.275 12.9167 8.33333 12.9167Z", - "fillRile": "nonzero" + "fillRule": "nonzero" } ] }, diff --git a/editor/public/examples/canvas/layout-01.grida b/editor/public/examples/canvas/layout-01.grida index 6b9c44861f..2a1fe94d16 100644 --- a/editor/public/examples/canvas/layout-01.grida +++ b/editor/public/examples/canvas/layout-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "173:49": { diff --git a/editor/public/examples/canvas/poster-01.grida b/editor/public/examples/canvas/poster-01.grida index c2d9c61143..febbe10ec1 100644 --- a/editor/public/examples/canvas/poster-01.grida +++ b/editor/public/examples/canvas/poster-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "102:2": { @@ -753,91 +754,91 @@ "paths": [ { "d": "M0 21.8644L16.2685 21.8644L16.2685 17.6063L5.13106 17.6063L5.13106 0.301995L0 0.301995L0 21.8644Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M25.8065 5.49631C20.5547 5.49631 17.4157 8.69745 17.4157 13.8918C17.4157 19.0559 20.5547 22.257 25.8065 22.257C31.0281 22.257 34.1671 19.0559 34.1671 13.8918C34.1671 8.69745 31.0281 5.49631 25.8065 5.49631ZM25.8065 18.7539C23.6635 18.7539 22.3053 17.2439 22.3053 13.8918C22.3053 10.5396 23.6635 8.99945 25.8065 8.99945C27.9494 8.99945 29.3076 10.5396 29.3076 13.8918C29.3076 17.2439 27.9494 18.7539 25.8065 18.7539Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M45.4993 21.8644L51.2642 5.8587L46.1935 5.8587L42.7526 16.6701L42.6319 16.6701L39.2213 5.8587L34.0298 5.8587L39.7947 21.8644L45.4993 21.8644Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M62.4997 16.7607C62.228 17.7573 61.4433 19.0257 59.3607 19.0257C57.3384 19.0257 56.0406 17.3949 56.0104 14.8581L67.5402 14.8581C67.9326 8.72765 64.5521 5.49631 59.3305 5.49631C54.1391 5.49631 51.0906 8.72765 51.0906 13.8918C51.0906 19.0559 54.2296 22.257 59.3607 22.257C64.5823 22.257 66.8158 19.3277 67.3893 17.0929L62.4997 16.7607ZM59.3305 8.63705C61.3829 8.63705 62.5299 10.0866 62.6506 11.959L56.0406 11.959C56.1613 10.0866 57.3082 8.63705 59.3305 8.63705Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M75.5497 0L75.5497 3.77494L80.3487 3.77494L80.3487 0L75.5497 0ZM75.5497 5.8587L75.5497 21.8644L80.3487 21.8644L80.3487 5.8587L75.5497 5.8587Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M96.637 10.2376C96.0937 7.24788 93.166 5.49631 89.3328 5.49631C85.56 5.49631 82.2097 7.24788 82.2097 10.449C82.2097 13.9522 84.9563 14.8883 87.552 15.2809L89.3932 15.5829C91.1136 15.8547 92.2303 16.3077 92.2303 17.3043C92.2303 18.4821 91.174 19.2069 89.5139 19.2069C87.6728 19.2069 86.4654 18.3915 86.1334 16.6701L81.6362 17.2137C82.1795 20.4451 85.228 22.257 89.4837 22.257C93.7999 22.257 97.1199 20.6866 97.1199 17.0929C97.1199 13.4388 94.2526 12.563 91.3249 12.0798L89.6346 11.808C88.035 11.5664 86.9786 11.2342 86.9786 10.2074C86.9786 9.12025 87.9746 8.51626 89.363 8.51626C90.8118 8.51626 91.9285 9.33164 92.2002 10.751L96.637 10.2376Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M113.349 18.2707C111.99 18.2707 111.447 18.1197 111.447 16.7003L111.447 8.96925L114.707 8.96925L114.707 5.8587L111.447 5.8587L111.447 1.41938L106.648 1.41938L106.648 5.8587L104.143 5.8587L104.143 8.96925L106.648 8.96925L106.648 17.8781C106.648 21.2 108.097 21.9248 111.598 21.9248C112.775 21.9248 114.164 21.9248 114.616 21.8644L114.616 18.2405C114.254 18.2707 113.832 18.2707 113.349 18.2707Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M121.141 8.00286L121.141 0L116.342 0L116.342 21.8644L121.141 21.8644L121.141 13.3482C121.141 10.6906 122.65 9.33164 124.249 9.33164C126 9.33164 126.966 10.1772 126.966 12.5328L126.966 21.8644L131.765 21.8644L131.765 11.3852C131.765 7.30828 129.32 5.49631 126.242 5.49631C123.887 5.49631 122.288 6.58349 121.261 8.03306L121.141 8.00286Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M144.933 16.7607C144.661 17.7573 143.876 19.0257 141.794 19.0257C139.771 19.0257 138.474 17.3949 138.443 14.8581L149.973 14.8581C150.366 8.72765 146.985 5.49631 141.763 5.49631C136.572 5.49631 133.524 8.72765 133.524 13.8918C133.524 19.0559 136.663 22.257 141.794 22.257C147.015 22.257 149.249 19.3277 149.822 17.0929L144.933 16.7607ZM141.763 8.63705C143.816 8.63705 144.963 10.0866 145.084 11.959L138.474 11.959C138.594 10.0866 139.741 8.63705 141.763 8.63705Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M167.671 5.49631C165.408 5.49631 163.778 6.34189 162.872 7.76127L162.752 7.76127L162.752 0.0301991L157.952 0.0301991L157.952 21.8946L162.54 21.8946L162.54 19.7505L162.661 19.7505C163.687 21.5624 165.559 22.257 167.551 22.257C171.535 22.257 174.462 19.0559 174.462 13.8918C174.462 8.69745 171.535 5.49631 167.671 5.49631ZM166.041 18.6633C163.898 18.6633 162.51 17.1231 162.51 13.8918C162.51 10.6302 163.898 9.09005 166.041 9.09005C168.184 9.09005 169.573 10.6302 169.573 13.8918C169.573 17.1231 168.184 18.6633 166.041 18.6633Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M187.339 16.7607C187.068 17.7573 186.283 19.0257 184.2 19.0257C182.178 19.0257 180.88 17.3949 180.85 14.8581L192.38 14.8581C192.772 8.72765 189.392 5.49631 184.17 5.49631C178.979 5.49631 175.93 8.72765 175.93 13.8918C175.93 19.0559 179.069 22.257 184.2 22.257C189.422 22.257 191.656 19.3277 192.229 17.0929L187.339 16.7607ZM184.17 8.63705C186.223 8.63705 187.37 10.0866 187.49 11.959L180.88 11.959C181.001 10.0866 182.148 8.63705 184.17 8.63705Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M208.405 10.2376C207.862 7.24788 204.934 5.49631 201.101 5.49631C197.328 5.49631 193.978 7.24788 193.978 10.449C193.978 13.9522 196.724 14.8883 199.32 15.2809L201.161 15.5829C202.882 15.8547 203.998 16.3077 203.998 17.3043C203.998 18.4821 202.942 19.2069 201.282 19.2069C199.441 19.2069 198.233 18.3915 197.901 16.6701L193.404 17.2137C193.947 20.4451 196.996 22.257 201.252 22.257C205.568 22.257 208.888 20.6866 208.888 17.0929C208.888 13.4388 206.021 12.563 203.093 12.0798L201.403 11.808C199.803 11.5664 198.747 11.2342 198.747 10.2074C198.747 9.12025 199.743 8.51626 201.131 8.51626C202.58 8.51626 203.697 9.33164 203.968 10.751L208.405 10.2376Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M218.654 18.2707C217.296 18.2707 216.753 18.1197 216.753 16.7003L216.753 8.96925L220.013 8.96925L220.013 5.8587L216.753 5.8587L216.753 1.41938L211.954 1.41938L211.954 5.8587L209.449 5.8587L209.449 8.96925L211.954 8.96925L211.954 17.8781C211.954 21.2 213.403 21.9248 216.904 21.9248C218.081 21.9248 219.469 21.9248 219.922 21.8644L219.922 18.2405C219.56 18.2707 219.137 18.2707 218.654 18.2707Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M236.118 18.2707C234.76 18.2707 234.216 18.1197 234.216 16.7003L234.216 8.96925L237.476 8.96925L237.476 5.8587L234.216 5.8587L234.216 1.41938L229.417 1.41938L229.417 5.8587L226.912 5.8587L226.912 8.96925L229.417 8.96925L229.417 17.8781C229.417 21.2 230.866 21.9248 234.367 21.9248C235.544 21.9248 236.933 21.9248 237.386 21.8644L237.386 18.2405C237.023 18.2707 236.601 18.2707 236.118 18.2707Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M243.91 8.00286L243.91 0L239.111 0L239.111 21.8644L243.91 21.8644L243.91 13.3482C243.91 10.6906 245.419 9.33164 247.019 9.33164C248.769 9.33164 249.735 10.1772 249.735 12.5328L249.735 21.8644L254.534 21.8644L254.534 11.3852C254.534 7.30828 252.09 5.49631 249.011 5.49631C246.657 5.49631 245.057 6.58349 244.031 8.03306L243.91 8.00286Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M256.716 0L256.716 3.77494L261.515 3.77494L261.515 0L256.716 0ZM256.716 5.8587L256.716 21.8644L261.515 21.8644L261.515 5.8587L256.716 5.8587Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M268.295 8.00286L268.295 5.8587L263.708 5.8587L263.708 21.8644L268.507 21.8644L268.507 13.1972C268.507 10.5698 269.986 9.33164 271.615 9.33164C273.366 9.33164 274.332 10.1772 274.332 12.5328L274.332 21.8644L279.131 21.8644L279.131 11.3852C279.131 7.30828 276.686 5.49631 273.517 5.49631C271.042 5.49631 269.412 6.58349 268.416 8.03306L268.295 8.00286Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M297.309 5.8587L292.721 5.8587L292.721 7.82167L292.6 7.82167C291.574 6.1003 289.763 5.49631 287.771 5.49631C283.847 5.49631 280.92 8.45586 280.92 13.2576C280.92 18.0895 283.847 21.049 287.65 21.049C289.914 21.049 291.453 20.3243 292.389 18.9955L292.51 18.9955L292.51 20.5357C292.51 23.314 291.121 24.3408 289.099 24.3408C287.349 24.3408 286.262 23.6764 285.779 22.0456L281.01 22.4986C282.006 26.3038 285.175 27.8137 289.099 27.8137C293.687 27.8137 297.309 26.0018 297.309 20.6263L297.309 5.8587ZM289.28 17.5157C287.168 17.5157 285.809 15.8849 285.809 13.2576C285.809 10.6906 287.168 9.09005 289.28 9.09005C291.363 9.09005 292.751 10.6906 292.751 13.2576C292.751 15.8849 291.363 17.5157 289.28 17.5157Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M331.12 5.8587L326.05 5.8587L323.243 16.5493L323.122 16.5493L320.466 5.8587L315.214 5.8587L312.558 16.5493L312.437 16.5493L309.63 5.8587L304.379 5.8587L309.57 21.8644L314.822 21.8644L317.629 11.355L317.749 11.355L320.587 21.8644L325.929 21.8644L331.12 5.8587Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M342.369 16.7607C342.097 17.7573 341.312 19.0257 339.23 19.0257C337.208 19.0257 335.91 17.3949 335.88 14.8581L347.409 14.8581C347.802 8.72765 344.421 5.49631 339.2 5.49631C334.008 5.49631 330.96 8.72765 330.96 13.8918C330.96 19.0559 334.099 22.257 339.23 22.257C344.451 22.257 346.685 19.3277 347.258 17.0929L342.369 16.7607ZM339.2 8.63705C341.252 8.63705 342.399 10.0866 342.52 11.959L335.91 11.959C336.03 10.0866 337.177 8.63705 339.2 8.63705Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M361.818 5.49631C357.924 5.49631 354.996 8.69745 354.996 13.8918C354.996 19.0559 357.924 22.257 361.908 22.257C363.93 22.257 365.772 21.5624 366.798 19.7505L366.919 19.7505L366.919 21.8644L371.506 21.8644L371.506 0L366.737 0L366.737 7.76127L366.617 7.76127C365.681 6.34189 364.081 5.49631 361.818 5.49631ZM363.447 18.6633C361.274 18.6633 359.886 17.1231 359.886 13.8918C359.886 10.6302 361.274 9.09005 363.447 9.09005C365.56 9.09005 366.979 10.6302 366.979 13.8918C366.979 17.1231 365.56 18.6633 363.447 18.6633Z", - "fillRile": "nonzero" + "fillRule": "nonzero" }, { "d": "M381.639 5.49631C376.388 5.49631 373.249 8.69745 373.249 13.8918C373.249 19.0559 376.388 22.257 381.639 22.257C386.861 22.257 390 19.0559 390 13.8918C390 8.69745 386.861 5.49631 381.639 5.49631ZM381.639 18.7539C379.496 18.7539 378.138 17.2439 378.138 13.8918C378.138 10.5396 379.496 8.99945 381.639 8.99945C383.782 8.99945 385.141 10.5396 385.141 13.8918C385.141 17.2439 383.782 18.7539 381.639 18.7539Z", - "fillRile": "nonzero" + "fillRule": "nonzero" } ] }, diff --git a/editor/public/examples/canvas/resume-01.grida b/editor/public/examples/canvas/resume-01.grida index 00f0a7cef0..3ba6d1be60 100644 --- a/editor/public/examples/canvas/resume-01.grida +++ b/editor/public/examples/canvas/resume-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "1:475": { diff --git a/editor/public/examples/canvas/sketch-teimplate-01.grida b/editor/public/examples/canvas/sketch-teimplate-01.grida index e4d349907e..beb2177715 100644 --- a/editor/public/examples/canvas/sketch-teimplate-01.grida +++ b/editor/public/examples/canvas/sketch-teimplate-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "2": { diff --git a/editor/public/examples/canvas/slides-01.grida b/editor/public/examples/canvas/slides-01.grida index 97f09655ef..491abf2279 100644 --- a/editor/public/examples/canvas/slides-01.grida +++ b/editor/public/examples/canvas/slides-01.grida @@ -1,4 +1,5 @@ { + "version": "0.0.1-beta.1+20251010", "document": { "nodes": { "1": { @@ -608,7 +609,7 @@ "paths": [ { "d": "M16.6667 8.33333C16.6667 3.73333 12.9333 0 8.33333 0C3.73333 0 0 3.73333 0 8.33333C0 12.9333 3.73333 16.6667 8.33333 16.6667C12.9333 16.6667 16.6667 12.9333 16.6667 8.33333ZM8.33333 9.16667L5 9.16667L5 7.5L8.33333 7.5L8.33333 5L11.6667 8.33333L8.33333 11.6667L8.33333 9.16667Z", - "fillRile": "nonzero" + "fillRule": "nonzero" } ] }, diff --git a/editor/scaffolds/sidecontrol/controls/flex-align.tsx b/editor/scaffolds/sidecontrol/controls/flex-align.tsx new file mode 100644 index 0000000000..1d1cc7b020 --- /dev/null +++ b/editor/scaffolds/sidecontrol/controls/flex-align.tsx @@ -0,0 +1,251 @@ +/** + * @fileoverview + * @module flex-align + * + * Flex Alignment Control Component + * + * This component provides a unified, interactive UI for selecting flex layout alignment properties. + * It combines both main-axis (justify-content) and cross-axis (align-items) alignment controls + * into a single, intuitive 3x3 grid interface. + * + * ## Features + * - **Unified Interface**: Single UI component for selecting both main and cross axis alignment + * - **3x3 Grid Layout**: Visual representation of 9 possible alignment combinations + * - **Smart Spacing Handling**: When spacing properties (space-between, space-around, space-evenly) + * are set externally, the UI adapts to show only 3 relevant alignment options + * - **Interactive Selection**: Users can easily select alignment combinations with visual feedback + * + * ## Alignment Combinations + * The component supports all 9 combinations of flex alignment: + * - **Main Axis**: start, center, end, space-between, space-around, space-evenly + * - **Cross Axis**: start, center, end, stretch + * + * ## Usage Context + * This component is used in the side control panel for flex containers, providing a more + * intuitive alternative to separate main-axis and cross-axis alignment controls. + * + * ## Related Components + * - `MainAxisAlignmentControl`: Handles main axis alignment (justify-content) + * - `CrossAxisAlignmentControl`: Handles cross axis alignment (align-items) + * + * ## Visual Design + * The component displays a 3x3 grid where: + * - Each cell represents a combination of main and cross axis alignment + * - Selected combinations are highlighted + * - The grid adapts based on external spacing properties + * - Icons and visual indicators help users understand the alignment behavior + * + * @see {@link ./main-axis-alignment.tsx} - Main axis alignment control + * @see {@link ./cross-axis-alignment.tsx} - Cross axis alignment control + */ + +import React from "react"; +import { cn } from "@/components/lib/utils"; +import type cg from "@grida/cg"; +import grida from "@grida/schema"; + +type MainAxisAlignment = cg.MainAxisAlignment; +type CrossAxisAlignment = cg.CrossAxisAlignment; +type Axis = cg.Axis; + +interface AlignmentCellIconProps { + direction: Axis; + mainAxisAlignment: MainAxisAlignment; + crossAxisAlignment: CrossAxisAlignment; + className?: string; +} + +/** + * AlignmentCellIcon - Flexbox icon representing flex alignment + * + * Renders 3 bars with varying sizes to represent flex alignment: + * - Main axis alignment controls bar distribution (justifyContent: start/center/end) + * - Cross axis alignment controls bar positioning (alignItems: start/center/end) + * - Direction determines if bars are horizontal or vertical + * - For horizontal: bars are vertical (1px wide), distributed left-right, aligned top/center/bottom + * - For vertical: bars are horizontal (1px tall), distributed top-bottom, aligned left/center/right + */ +export function AlignmentCellIcon({ + direction, + mainAxisAlignment, + crossAxisAlignment, + className, +}: AlignmentCellIconProps) { + const isHorizontal = direction === "horizontal"; + + const barWidth = 2; + const barLengths = [10, 14, 7]; // Varying sizes for visual distinction + const flexDirection = isHorizontal ? "row" : "column"; + + return ( +
+ {barLengths.map((length, index) => ( +
+ ))} +
+ ); +} + +type TMixed = typeof grida.mixed | T; + +interface FlexAlignValue { + mainAxisAlignment: MainAxisAlignment; + crossAxisAlignment: CrossAxisAlignment; +} + +interface FlexAlignControlProps { + direction?: Axis; + value?: TMixed; + onValueChange?: (value: FlexAlignValue) => void; + className?: string; +} + +// Pre-defined grid orders for each direction +// Key format: "mainAxis-crossAxis" +const GRID_ORDERS = { + horizontal: { + // Horizontal: rows=cross-axis, columns=main-axis + "start-start": 0, + "center-start": 1, + "end-start": 2, + "start-center": 3, + "center-center": 4, + "end-center": 5, + "start-end": 6, + "center-end": 7, + "end-end": 8, + }, + vertical: { + // Vertical: rows=main-axis, columns=cross-axis + "start-start": 0, + "start-center": 1, + "start-end": 2, + "center-start": 3, + "center-center": 4, + "center-end": 5, + "end-start": 6, + "end-center": 7, + "end-end": 8, + }, +} as const; + +/** + * FlexAlignControl - 3x3 grid interface for selecting flex alignment + * + * Provides a unified interface for selecting both main-axis and cross-axis alignment + * through a visual 3x3 grid where each cell represents a combination of alignments. + */ +export function FlexAlignControl({ + direction = "horizontal", + value, + onValueChange, + className, +}: FlexAlignControlProps) { + const isMixed = value === grida.mixed; + const hasValue = value && value !== grida.mixed; + + const mainAxisAlignment = hasValue ? value.mainAxisAlignment : undefined; + const crossAxisAlignment = hasValue ? value.crossAxisAlignment : undefined; + const isHorizontal = direction === "horizontal"; + + // Define the 3x3 grid combinations + const mainAxisOptions: MainAxisAlignment[] = ["start", "center", "end"]; + const crossAxisOptions: CrossAxisAlignment[] = ["start", "center", "end"]; + + const handleCellClick = ( + mainAxis: MainAxisAlignment, + crossAxis: CrossAxisAlignment + ) => { + onValueChange?.({ + mainAxisAlignment: mainAxis, + crossAxisAlignment: crossAxis, + }); + }; + + const isSelected = ( + mainAxis: MainAxisAlignment, + crossAxis: CrossAxisAlignment + ) => { + if (!hasValue) return false; + return mainAxisAlignment === mainAxis && crossAxisAlignment === crossAxis; + }; + + const getGridOrder = ( + mainAxis: MainAxisAlignment, + crossAxis: CrossAxisAlignment + ) => { + const key = + `${mainAxis}-${crossAxis}` as keyof typeof GRID_ORDERS.horizontal; + return GRID_ORDERS[direction][key]; + }; + + return ( +
+ {mainAxisOptions.flatMap((mainAxis) => + crossAxisOptions.map((crossAxis) => { + const selected = isSelected(mainAxis, crossAxis); + + return ( + + ); + }) + )} +
+ ); +} diff --git a/editor/scaffolds/sidecontrol/controls/flex-direction.tsx b/editor/scaffolds/sidecontrol/controls/flex-direction.tsx deleted file mode 100644 index fa3c78b9d3..0000000000 --- a/editor/scaffolds/sidecontrol/controls/flex-direction.tsx +++ /dev/null @@ -1,70 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { ToggleGroup, ToggleGroupItem } from "./utils/toggle-group"; -import { ViewHorizontalIcon, ViewVerticalIcon } from "@radix-ui/react-icons"; - -type FlexDirection = "row" | "column" | "row-reverse" | "column-reverse"; -type Direction = "row" | "column"; - -export function FlexDirectionControl({ - value, - onValueChange, -}: { - value?: FlexDirection; - onValueChange?: (value?: FlexDirection) => void; -}) { - const [direction, setDirection] = useState(); - const [reverse, setReverse] = useState<"reverse">(); - - useEffect(() => { - if (value === "row" || value === "row-reverse") { - setDirection("row"); - } else { - setDirection("column"); - } - setReverse( - value === "row-reverse" || value === "column-reverse" - ? "reverse" - : undefined - ); - }, [value]); - - const onDirectionChange = (value: Direction) => { - setDirection(value); - onValueChange?.((value + (reverse ? "-reverse" : "")) as FlexDirection); - }; - - const onReverseChange = (value: "reverse") => { - setReverse(value); - onValueChange?.((direction + (value ? "-reverse" : "")) as FlexDirection); - }; - - return ( -
- - - - - - - - - - - Reverse - - -
- ); -} diff --git a/editor/scaffolds/sidecontrol/controls/flex-wrap.tsx b/editor/scaffolds/sidecontrol/controls/flex-wrap.tsx index 323a63972d..4ff1b874ef 100644 --- a/editor/scaffolds/sidecontrol/controls/flex-wrap.tsx +++ b/editor/scaffolds/sidecontrol/controls/flex-wrap.tsx @@ -1,7 +1,7 @@ import { ToggleGroup, ToggleGroupItem } from "./utils/toggle-group"; export function FlexWrapControl({ - value, + value = "nowrap", onValueChange, }: { value?: "wrap" | "nowrap"; @@ -11,8 +11,11 @@ export function FlexWrapControl({ v !== "" && onValueChange?.(v as "wrap" | "nowrap")} > Yes diff --git a/editor/scaffolds/sidecontrol/controls/gap.tsx b/editor/scaffolds/sidecontrol/controls/gap.tsx index f3e9bc91bc..918d666f42 100644 --- a/editor/scaffolds/sidecontrol/controls/gap.tsx +++ b/editor/scaffolds/sidecontrol/controls/gap.tsx @@ -2,19 +2,57 @@ import InputPropertyNumber from "../ui/number"; export function GapControl({ value, + mode = "single", onValueCommit, }: { - value: { mainAxisGap: number; crossAxisGap: number }; + value: { mainAxisGap: number; crossAxisGap?: number }; + mode?: "single" | "multiple"; onValueCommit?: ( value: number | { mainAxisGap: number; crossAxisGap: number } ) => void; }) { + const mainAxisGap = value.mainAxisGap ?? 0; + + if (mode === "multiple") { + return ( +
+ + onValueCommit?.({ + mainAxisGap: v ?? 0, + crossAxisGap: value.crossAxisGap ?? v ?? 0, + }) + } + /> + + onValueCommit?.({ + mainAxisGap, + crossAxisGap: v ?? 0, + }) + } + /> +
+ ); + } + return ( ; - onValueChange?: (value: Layout) => void; + value?: PartialLayoutProperties; + onValueChange?: ( + value: PartialLayoutProperties & { + /** + * when you need this, we expose internal option value to you. + */ + key: Option; + } + ) => void; +}) { + const op: Option = + value?.layoutMode === "flex" + ? value.direction === "horizontal" + ? "flex-row" + : "flex-column" + : "normal"; + + const _onValueChange = (value: Option) => { + if (value === "normal") { + onValueChange?.({ layoutMode: "flow", key: value }); + } else if (value === "flex-row") { + onValueChange?.({ + layoutMode: "flex", + direction: "horizontal", + key: value, + }); + } else if (value === "flex-column") { + onValueChange?.({ + layoutMode: "flex", + direction: "vertical", + key: value, + }); + } + }; + + return ( + + enum={[ + { value: "normal", icon: }, + { value: "flex-row", icon: }, + { value: "flex-column", icon: }, + // WILL BE 'GRID' + // @ts-expect-error grid is not yet supported + { value: "grid", icon: , disabled: true }, + ]} + value={op} + onValueChange={_onValueChange} + /> + ); +} + +export function LayoutModeControl({ + value, + onValueChange, +}: { + value?: TMixed; + onValueChange?: (value: LayoutMode) => void; }) { return ( - + placeholder="Display" enum={[ { value: "flow", label: "Normal Flow" }, diff --git a/editor/scaffolds/sidecontrol/controls/main-axis-alignment.tsx b/editor/scaffolds/sidecontrol/controls/main-axis-alignment.tsx index 41acbba543..1844ac9382 100644 --- a/editor/scaffolds/sidecontrol/controls/main-axis-alignment.tsx +++ b/editor/scaffolds/sidecontrol/controls/main-axis-alignment.tsx @@ -22,6 +22,10 @@ export function MainAxisAlignmentControl({ label: "Center", value: "center", }, + { + label: "End", + value: "end", + }, { label: "Space Between", value: "space-between", diff --git a/editor/scaffolds/sidecontrol/controls/padding.tsx b/editor/scaffolds/sidecontrol/controls/padding.tsx index 9a88ba9877..6d68f9c244 100644 --- a/editor/scaffolds/sidecontrol/controls/padding.tsx +++ b/editor/scaffolds/sidecontrol/controls/padding.tsx @@ -1,5 +1,9 @@ +import React, { useState, useMemo } from "react"; import InputPropertyNumber from "../ui/number"; import { WorkbenchUI } from "@/components/workbench"; +import { AllSidesIcon } from "@radix-ui/react-icons"; +import { Toggle } from "@/components/ui/toggle"; +import { cn } from "@/components/lib/utils"; import grida from "@grida/schema"; type Padding = grida.program.nodes.i.IPadding["padding"]; @@ -11,21 +15,181 @@ export function PaddingControl({ value: Padding; onValueCommit?: (value: Padding) => void; }) { + const [showIndividual, setShowIndividual] = useState(false); + + // Determine if current value is uniform or individual + const isUniform = typeof value === "number"; + + // Get individual padding values + const paddingValues = useMemo(() => { + if (typeof value === "number") { + return { + top: value, + right: value, + bottom: value, + left: value, + }; + } + return { + top: value.paddingTop ?? 0, + right: value.paddingRight ?? 0, + bottom: value.paddingBottom ?? 0, + left: value.paddingLeft ?? 0, + }; + }, [value]); + + // Get uniform value (if all sides are equal) + const uniformValue = useMemo(() => { + if (isUniform) return value as number; + const { top, right, bottom, left } = paddingValues; + if (top === right && right === bottom && bottom === left) { + return top; + } + return undefined; + }, [isUniform, value, paddingValues]); + + const placeholder = useMemo(() => { + if (isUniform) return String(uniformValue ?? 0); + return [ + paddingValues.left ?? 0, + paddingValues.top ?? 0, + paddingValues.right ?? 0, + paddingValues.bottom ?? 0, + ].join(", "); + }, [isUniform, uniformValue, paddingValues]); + + const handleUniformChange = (newValue: number | undefined) => { + if (newValue === undefined) return; + onValueCommit?.(newValue); + }; + + const handleIndividualChange = ( + side: "top" | "right" | "bottom" | "left", + newValue: number | undefined + ) => { + if (newValue === undefined) return; + onValueCommit?.({ + paddingTop: side === "top" ? newValue : paddingValues.top, + paddingRight: side === "right" ? newValue : paddingValues.right, + paddingBottom: side === "bottom" ? newValue : paddingValues.bottom, + paddingLeft: side === "left" ? newValue : paddingValues.left, + }); + }; + return ( - +
+ {/* First Row: Uniform Input + Toggle Buttons */} +
+ +
+ + +
+
+ + {/* Second Row: Individual Padding Controls */} + {showIndividual && ( +
+ {/* Left */} +
+ handleIndividualChange("left", v)} + aria-label="Padding left" + /> + +
+ {/* Separator */} +
+ {/* Top */} +
+ handleIndividualChange("top", v)} + aria-label="Padding top" + /> + +
+ {/* Separator */} +
+ {/* Right */} +
+ handleIndividualChange("right", v)} + aria-label="Padding right" + /> + +
+ {/* Separator */} +
+ {/* Bottom */} +
+ handleIndividualChange("bottom", v)} + aria-label="Padding bottom" + /> + +
+
+ )} +
); } + +const Label = ({ children }: React.PropsWithChildren) => { + return ( + + ); +}; diff --git a/editor/scaffolds/sidecontrol/controls/positioning.tsx b/editor/scaffolds/sidecontrol/controls/positioning.tsx index 23de7feb1e..5405fdbce8 100644 --- a/editor/scaffolds/sidecontrol/controls/positioning.tsx +++ b/editor/scaffolds/sidecontrol/controls/positioning.tsx @@ -36,9 +36,16 @@ export function PositioningModeControl({ export function PositioningConstraintsControl({ value, onValueCommit, + disabled, }: { value: grida.program.nodes.i.IPositioning; onValueCommit?: (value: grida.program.nodes.i.IPositioning) => void; + disabled?: { + top?: boolean; + right?: boolean; + bottom?: boolean; + left?: boolean; + }; }) { return (
@@ -49,6 +56,7 @@ export function PositioningConstraintsControl({ aria-label="Top" type="number" value={value.top ?? ""} + disabled={disabled?.top} onValueCommit={(v) => { onValueCommit?.({ ...value, @@ -65,6 +73,7 @@ export function PositioningConstraintsControl({ type="number" aria-label="Left" value={value.left ?? ""} + disabled={disabled?.left} onValueCommit={(v) => { onValueCommit?.({ ...value, @@ -80,6 +89,7 @@ export function PositioningConstraintsControl({ top: value.top !== undefined, bottom: value.bottom !== undefined, }} + disabled={disabled} onConstraintChange={(side, checked) => { onValueCommit?.({ ...value, @@ -93,6 +103,7 @@ export function PositioningConstraintsControl({ type="number" aria-label="Right" value={value.right ?? ""} + disabled={disabled?.right} onValueCommit={(v) => { onValueCommit?.({ ...value, @@ -109,6 +120,7 @@ export function PositioningConstraintsControl({ type="number" aria-label="Bottom" value={value.bottom ?? ""} + disabled={disabled?.bottom} onValueCommit={(v) => { onValueCommit?.({ ...value, @@ -125,6 +137,7 @@ export function PositioningConstraintsControl({ function ConstraintsBox({ constraint, onConstraintChange, + disabled, }: { constraint: { left: boolean; @@ -136,12 +149,19 @@ function ConstraintsBox({ side: "left" | "right" | "top" | "bottom", checked: boolean ) => void; + disabled?: { + top?: boolean; + right?: boolean; + bottom?: boolean; + left?: boolean; + }; }) { return (
onConstraintChange?.("top", !constraint.top)} tabIndex={-1} /> @@ -149,6 +169,7 @@ function ConstraintsBox({
onConstraintChange?.("left", !constraint.left)} tabIndex={-1} /> @@ -156,6 +177,7 @@ function ConstraintsBox({
onConstraintChange?.("bottom", !constraint.bottom)} tabIndex={-1} /> @@ -163,6 +185,7 @@ function ConstraintsBox({
onConstraintChange?.("right", !constraint.right)} tabIndex={-1} /> @@ -176,16 +199,19 @@ function AnchorLineButton({ checked, onClick, tabIndex, + disabled, }: { checked?: boolean; onClick?: () => void; tabIndex?: number; + disabled?: boolean; }) { return (