diff --git a/.agents/skills/cg-perf/SKILL.md b/.agents/skills/cg-perf/SKILL.md index 936b6d8c1d..fafa832ada 100644 --- a/.agents/skills/cg-perf/SKILL.md +++ b/.agents/skills/cg-perf/SKILL.md @@ -53,6 +53,11 @@ relying on hardcoded paths. File locations shift as the engine evolves. | Benchmark fixture scenes | `--list-scenes` flag on `grida-dev bench`, or run `bench-report` on a directory | | Config toggles (compositing, atlas, etc.) | `grep "set_layer_compositing\|set_compositor_atlas\|set_interaction_render_scale" --include="*.rs"` | | Existing `.plan.md` proposals | `glob "docs/wg/feat-2d/*.plan.md"` | +| Scene loading pipeline | `grep "fn load_scene" --include="*.rs"` in `src/runtime/scene.rs` | +| Layout engine entry point | `grep "fn compute\b" --include="*.rs"` in `src/layout/engine.rs` | +| Text measurement stats | `grep "ParagraphMeasureStats" --include="*.rs"` | +| Skip-layout config | `grep "skip_layout" --include="*.rs"` in `src/runtime/` | +| Load-bench CLI tool | Read `crates/grida-dev/src/bench/load_bench.rs` | --- @@ -126,6 +131,7 @@ reports `min/p50/p95/p99/MAX` plus per-stage breakdown and settle cost. | `zoom` | slow/fast × around-fit/high | Zoom oscillation at different levels | | `pan_with_settle` | slow/fast × fit/zoomed | Pan with settle frames interleaved every 12 frames | | `realtime` | fast/slow × fit/zoomed | **Real-time event loop simulation** with sleep, 240Hz tick thread, and settle countdown matching the native viewer | +| `resize` | alternating viewport sizes | `--resize` flag. Measures `resize()` + `redraw()` cost per cycle (layout rebuild + cache invalidation + repaint) | The `realtime` scenarios use actual `thread::sleep()` between frames and simulate the native viewer's 240Hz tick thread + settle countdown. @@ -168,6 +174,7 @@ of scenes, configs, and operations. The naming convention is | Does a config toggle actually help? | Both GPU benchmarks + Criterion | | Does it match what users see in the app? | `realtime` scenarios (sleep + settle simulation) | | Are there frame drops during gestures? | Check `p99` and `MAX` in scenario stats | +| Is resize janky? | Single-scene GPU bench with `--resize` | --- @@ -338,13 +345,62 @@ The document is organized by category: - Pan-Only Optimization (items 15-20) - Zoom Asymmetry (items 21-23) - Zoom & Interaction Optimization (items 24-30) -- Image, Text, Engine-Level (items 31-39) +- Image, Text (items 31-33) +- Scene Loading & Layout (items 40-44) +- Engine-Level (items 34-39) When working on a new optimization, check whether an item already exists for it, and update or add to the document as part of the work. --- +## Scene Loading & Layout Performance + +Scene loading (`Renderer::load_scene`) is the cold-start bottleneck. +For large documents (100K–150K+ nodes), the layout phase dominates +load time. This is separate from frame rendering — it runs once per +scene switch, not per frame. + +### Key files + +| File | Role | +| --------------------------------------------------------- | ------------------------------------------ | +| `crates/grida-canvas/src/runtime/scene.rs` (`load_scene`) | Orchestrates the load pipeline | +| `crates/grida-canvas/src/layout/engine.rs` | Layout engine (Taffy tree build + compute) | +| `crates/grida-canvas/src/runtime/config.rs` | `skip_layout` flag | +| `crates/grida-dev/src/bench/load_bench.rs` | `load-bench` CLI for per-stage timing | +| `crates/grida-canvas/benches/bench_load_scene.rs` | Criterion benchmarks for layout at scale | + +### The load-bench tool + +Primary diagnostic for scene loading. Reports per-stage timings. + +```sh +cargo run -p grida-dev --release -- load-bench file.grida --iterations 5 +cargo run -p grida-dev --release -- load-bench file.grida --list-scenes +cargo run -p grida-dev --release -- load-bench file.grida --skip-text # isolate tree + flexbox cost +cargo run -p grida-dev --release -- load-bench file.grida --skip-layout # schema-only fast path +``` + +### Cost breakdown + +For 100K–150K node scenes, layout is ~95%+ of `load_scene`. The main +cost centers: + +1. **Taffy tree construction** — node insertion + ID mappings +2. **Text measurement** — Skia paragraph layout calls per Taffy measure callback +3. **Flexbox computation** — `compute_layout_with_measure()` per subtree +4. **Layout extraction** — DFS walk to read computed results + +### Key optimization: skip_layout + +`skip_layout` bypasses Taffy entirely. `compute_schema_only()` copies +schema positions/sizes in a single walk — correct for absolute-positioned +documents. Set via `runtime_renderer_set_skip_layout(true)` before +loading a scene. + +--- + ## Pitfalls These are failure modes learned from experience. Each one has caused @@ -404,6 +460,13 @@ frame gets a cache hit. Without recapture, every frame after settle is also a full draw, producing 7fps instead of 100+fps. The capture guard should be `if self.backend.is_gpu()` — NOT `if !plan.stable`. +### Layout is the cold-start bottleneck, not rendering + +For large documents (100K+ nodes), `load_scene` dominates cold start +— frame rendering optimizations do not help here. Use `load-bench` +(not `bench`) to measure this path. Use `skip_layout` for +absolute-positioned documents. + ### Timing overhead in budgeted loops `Instant::now()` costs ~30ns per call. In a tight loop processing diff --git a/.agents/skills/research/SKILL.md b/.agents/skills/research/SKILL.md index 31bdf7affd..4b6bc9806b 100644 --- a/.agents/skills/research/SKILL.md +++ b/.agents/skills/research/SKILL.md @@ -3,7 +3,7 @@ name: research description: > Research upstream and peer projects to inform Grida's design and implementation. Use when investigating how Chromium, Skia, Servo, - or peer canvas editors solve a problem before writing code. Covers + Taffy, or peer canvas editors solve a problem before writing code. Covers source-code exploration, research document authoring, and the study-adapt-differ pattern used in .plan.md files. Relevant dirs: docs/wg/research/, docs/wg/feat-2d/, crates/grida-canvas/. @@ -92,10 +92,11 @@ Known citations: ### Web Standards & CSS -| Repo | Lang | When to reference | Key paths | -| --------------------------------------- | ---- | ------------------------------------------------------------------------- | ---------------------------------------- | -| [servo](https://github.com/servo/servo) | Rust | CSS layout, DOM, Rust browser-engine patterns. We vendor its style system | `components/style/` `components/layout/` | -| [stylo](https://github.com/servo/stylo) | Rust | CSS parsing and style resolution | `style/` | +| Repo | Lang | When to reference | Key paths | +| -------------------------------------------- | ---- | ------------------------------------------------------------------------- | ---------------------------------------- | +| [servo](https://github.com/servo/servo) | Rust | CSS layout, DOM, Rust browser-engine patterns. We vendor its style system | `components/style/` `components/layout/` | +| [stylo](https://github.com/servo/stylo) | Rust | CSS parsing and style resolution | `style/` | +| [taffy](https://github.com/DioxusLabs/taffy) | Rust | Flexbox/Grid layout algorithms and Rust-native layout engine internals | `src/tree/` `src/compute/` `src/style/` | ### Canvas Editor Peers diff --git a/.claude/skills b/.claude/skills new file mode 120000 index 0000000000..2b7a412b8f --- /dev/null +++ b/.claude/skills @@ -0,0 +1 @@ +../.agents/skills \ No newline at end of file diff --git a/.gitignore b/.gitignore index bbb20e2523..65bdcdf8dc 100644 --- a/.gitignore +++ b/.gitignore @@ -217,4 +217,12 @@ __pycache__/ # AI & Tooling *.plan.md -*.todo.md \ No newline at end of file +*.todo.md + + +# Claude Code (local settings and session files) +.claude/settings.local.json +.claude/plans/ +.claude/todos.json +.claude/worktrees +CLAUDE.local.md diff --git a/.nvmrc b/.nvmrc index d2c5c8a013..ffa3aecbfb 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v22.15.0 \ No newline at end of file +v22.22.1 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 33b00e7ebd..61edb15e4e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1505,9 +1505,9 @@ dependencies = [ [[package]] name = "grid" -version = "0.18.0" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12101ecc8225ea6d675bc70263074eab6169079621c2186fe0c66590b2df9681" +checksum = "f9e2d4c0a8296178d8802098410ca05d86b17a10bb5ab559b3fb404c1f948220" [[package]] name = "grida-canvas-wasm" @@ -4275,9 +4275,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b25026fb8cc9ab51ab9fdabe5d11706796966f6d1c78e19871ef63be2b8f0644" +checksum = "41ba83ebaf2954d31d05d67340fd46cebe99da2b7133b0dd68d70c65473a437b" dependencies = [ "arrayvec", "grid", diff --git a/crates/grida-canvas-wasm/lib/__test__/environment-node-api-spec-validation.test.ts b/crates/grida-canvas-wasm/lib/__test__/environment-node-api-spec-validation.test.ts index ed0e1e82bc..4e482092a5 100644 --- a/crates/grida-canvas-wasm/lib/__test__/environment-node-api-spec-validation.test.ts +++ b/crates/grida-canvas-wasm/lib/__test__/environment-node-api-spec-validation.test.ts @@ -35,8 +35,8 @@ const EXPECTED_FUNCTIONS = [ { name: "_deallocate", paramCount: 2 }, // initialization / app lifecycle - { name: "_init", paramCount: 3 }, - { name: "_init_with_backend", paramCount: 4 }, + { name: "_init", paramCount: 4 }, + { name: "_init_with_backend", paramCount: 5 }, { name: "_tick", paramCount: 2 }, { name: "_destroy", paramCount: 1 }, { name: "_resize_surface", paramCount: 3 }, @@ -100,6 +100,7 @@ const EXPECTED_FUNCTIONS = [ { name: "_runtime_renderer_set_layer_compositing", paramCount: 2 }, { name: "_runtime_renderer_set_pixel_preview_scale", paramCount: 2 }, { name: "_runtime_renderer_set_pixel_preview_stable", paramCount: 2 }, + { name: "_runtime_renderer_set_skip_layout", paramCount: 2 }, ] as const; // Expected Emscripten runtime methods @@ -130,7 +131,10 @@ describe("WASM API Validation", () => { useEmbeddedFonts: true, }); - const doc = readFileSync(resolve(process.cwd(), "example/rectangle.grida1"), "utf8"); + const doc = readFileSync( + resolve(process.cwd(), "example/rectangle.grida1"), + "utf8" + ); canvas.loadScene(doc); canvas.dispose(); 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 a0d824a7d4..4094ee5487 100644 --- a/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js +++ b/crates/grida-canvas-wasm/lib/bin/grida-canvas-wasm.js @@ -1,2 +1,2 @@ -var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Ng"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Og"];_add_font=Module["_add_font"]=wasmExports["Qg"];_add_image=Module["_add_image"]=wasmExports["Rg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Sg"];_allocate=Module["_allocate"]=wasmExports["Tg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Ug"];_command=Module["_command"]=wasmExports["Vg"];_deallocate=Module["_deallocate"]=wasmExports["Wg"];_destroy=Module["_destroy"]=wasmExports["Xg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Yg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Zg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["_g"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["$g"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["ah"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["bh"];_export_node_as=Module["_export_node_as"]=wasmExports["ch"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["dh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["eh"];_get_image_size=Module["_get_image_size"]=wasmExports["fh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["gh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["hh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["ih"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["jh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["kh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["lh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["mh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["nh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["oh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["ph"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["qh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["rh"];_init=Module["_init"]=wasmExports["sh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["th"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["uh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["vh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["wh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["xh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["yh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["zh"];_pointer_move=Module["_pointer_move"]=wasmExports["Ah"];_redraw=Module["_redraw"]=wasmExports["Bh"];_resize_surface=Module["_resize_surface"]=wasmExports["Ch"];_resolve_image=Module["_resolve_image"]=wasmExports["Dh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Eh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Fh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Gh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Hh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Ih"];_set_debug=Module["_set_debug"]=wasmExports["Jh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Kh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Lh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Mh"];_set_verbose=Module["_set_verbose"]=wasmExports["Nh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Oh"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Ph"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Qh"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Rh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["Sh"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["Th"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["Uh"];_switch_scene=Module["_switch_scene"]=wasmExports["Vh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Wh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Xh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Yh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["Zh"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["_h"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["$h"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["ai"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["bi"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["ci"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["di"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["ei"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["fi"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["gi"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["hi"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["ii"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["ji"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["ki"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["li"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["mi"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["ni"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["oi"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["pi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["qi"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["ri"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["si"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["ti"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["ui"];_tick=Module["_tick"]=wasmExports["vi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["wi"];_toggle_debug=Module["_toggle_debug"]=wasmExports["xi"];_main=Module["_main"]=wasmExports["yi"];_emscripten_builtin_memalign=wasmExports["zi"];_setThrew=wasmExports["Ai"];__emscripten_tempret_set=wasmExports["Bi"];__emscripten_stack_restore=wasmExports["Ci"];__emscripten_stack_alloc=wasmExports["Di"];_emscripten_stack_get_current=wasmExports["Ei"];___cxa_decrement_exception_refcount=wasmExports["Fi"];___cxa_increment_exception_refcount=wasmExports["Gi"];___cxa_can_catch=wasmExports["Hi"];___cxa_get_exception_ptr=wasmExports["Ii"];memory=wasmMemory=wasmExports["Mg"];__indirect_function_table=wasmTable=wasmExports["Pg"]}var wasmImports={G:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,ga:___cxa_find_matching_catch_4,Ca:___cxa_rethrow,H:___cxa_throw,eb:___cxa_uncaught_exceptions,e:___resumeException,Fa:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Ga:___syscall_openat,ub:___syscall_stat64,zb:__abort_js,gb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,ib:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Fc:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Cc:_emscripten_glBindBufferBase,Dc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Rb:_emscripten_glBindTransformFeedback,Xe:_emscripten_glBindVertexArray,_e:_emscripten_glBindVertexArrayOES,Ff:_emscripten_glBlendColor,Gf:_emscripten_glBlendEquation,Jd:_emscripten_glBlendEquationSeparate,Hf:_emscripten_glBlendFunc,Id:_emscripten_glBlendFuncSeparate,ve:_emscripten_glBlitFramebuffer,If:_emscripten_glBufferData,Jf:_emscripten_glBufferSubData,De:_emscripten_glCheckFramebufferStatus,Kf:_emscripten_glClear,fc:_emscripten_glClearBufferfi,gc:_emscripten_glClearBufferfv,ic:_emscripten_glClearBufferiv,hc:_emscripten_glClearBufferuiv,Lf:_emscripten_glClearColor,Hd:_emscripten_glClearDepthf,Mf:_emscripten_glClearStencil,se:_emscripten_glClientWaitSync,_c:_emscripten_glClipControlEXT,Nf:_emscripten_glColorMask,Of:_emscripten_glCompileShader,Qf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Rf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Sf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Tf:_emscripten_glCreateProgram,Uf:_emscripten_glCreateShader,Vf:_emscripten_glCullFace,Wf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Xf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Yf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Qb:_emscripten_glDeleteTransformFeedbacks,Ye:_emscripten_glDeleteVertexArrays,$e:_emscripten_glDeleteVertexArraysOES,Fd:_emscripten_glDepthFunc,_f:_emscripten_glDepthMask,Ed:_emscripten_glDepthRangef,Dd:_emscripten_glDetachShader,$f:_emscripten_glDisable,ag:_emscripten_glDisableVertexAttribArray,bg:_emscripten_glDrawArrays,Ve:_emscripten_glDrawArraysInstanced,Md:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Se:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Xc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Qe:_emscripten_glDrawBuffers,Vc:_emscripten_glDrawBuffersEXT,Nd:_emscripten_glDrawBuffersWEBGL,cg:_emscripten_glDrawElements,We:_emscripten_glDrawElementsInstanced,Ld:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Te:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Wc:_emscripten_glDrawElementsInstancedNV,Ke:_emscripten_glDrawRangeElements,dg:_emscripten_glEnable,eg:_emscripten_glEnableVertexAttribArray,fe:_emscripten_glEndQuery,$d:_emscripten_glEndQueryEXT,Ec:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Ic:_emscripten_glFramebufferTextureLayer,hg:_emscripten_glFrontFace,ig:_emscripten_glGenBuffers,Ie:_emscripten_glGenFramebuffers,ge:_emscripten_glGenQueries,ae:_emscripten_glGenQueriesEXT,Je:_emscripten_glGenRenderbuffers,le:_emscripten_glGenSamplers,jg:_emscripten_glGenTextures,Pb:_emscripten_glGenTransformFeedbacks,Ue:_emscripten_glGenVertexArrays,af:_emscripten_glGenVertexArraysOES,xe:_emscripten_glGenerateMipmap,Cd:_emscripten_glGetActiveAttrib,Bd:_emscripten_glGetActiveUniform,ac:_emscripten_glGetActiveUniformBlockName,bc:_emscripten_glGetActiveUniformBlockiv,dc:_emscripten_glGetActiveUniformsiv,Ad:_emscripten_glGetAttachedShaders,zd:_emscripten_glGetAttribLocation,yd:_emscripten_glGetBooleanv,Xb:_emscripten_glGetBufferParameteri64v,kg:_emscripten_glGetBufferParameteriv,lg:_emscripten_glGetError,mg:_emscripten_glGetFloatv,rc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Gc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Hb:_emscripten_glGetInternalformativ,Lb:_emscripten_glGetProgramBinary,og:_emscripten_glGetProgramInfoLog,pg:_emscripten_glGetProgramiv,Wd:_emscripten_glGetQueryObjecti64vEXT,Pd:_emscripten_glGetQueryObjectivEXT,Xd:_emscripten_glGetQueryObjectui64vEXT,he:_emscripten_glGetQueryObjectuiv,be:_emscripten_glGetQueryObjectuivEXT,ie:_emscripten_glGetQueryiv,ce:_emscripten_glGetQueryivEXT,ze:_emscripten_glGetRenderbufferParameteriv,Tb:_emscripten_glGetSamplerParameterfv,Ub:_emscripten_glGetSamplerParameteriv,qg:_emscripten_glGetShaderInfoLog,Td:_emscripten_glGetShaderPrecisionFormat,xd:_emscripten_glGetShaderSource,rg:_emscripten_glGetShaderiv,sg:_emscripten_glGetString,Ze:_emscripten_glGetStringi,Zb:_emscripten_glGetSynciv,wd:_emscripten_glGetTexParameterfv,vd:_emscripten_glGetTexParameteriv,Ac:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,sc:_emscripten_glGetUniformuiv,zc:_emscripten_glGetVertexAttribIiv,yc:_emscripten_glGetVertexAttribIuiv,qd:_emscripten_glGetVertexAttribPointerv,sd:_emscripten_glGetVertexAttribfv,rd:_emscripten_glGetVertexAttribiv,pd:_emscripten_glHint,Ud:_emscripten_glInvalidateFramebuffer,Vd:_emscripten_glInvalidateSubFramebuffer,od:_emscripten_glIsBuffer,nd:_emscripten_glIsEnabled,md:_emscripten_glIsFramebuffer,ld:_emscripten_glIsProgram,Pc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Ob:_emscripten_glIsTransformFeedback,Hc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Nb:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Kb:_emscripten_glProgramBinary,Jb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Mb:_emscripten_glResumeTransformFeedback,gd:_emscripten_glSampleCoverage,me:_emscripten_glSamplerParameterf,Vb:_emscripten_glSamplerParameterfv,ne:_emscripten_glSamplerParameteri,oe:_emscripten_glSamplerParameteriv,zg:_emscripten_glScissor,fd:_emscripten_glShaderBinary,Ag:_emscripten_glShaderSource,Bg:_emscripten_glStencilFunc,Cg:_emscripten_glStencilFuncSeparate,Dg:_emscripten_glStencilMask,Eg:_emscripten_glStencilMaskSeparate,Fg:_emscripten_glStencilOp,Gg:_emscripten_glStencilOpSeparate,Hg:_emscripten_glTexImage2D,Uc:_emscripten_glTexImage3D,Ig:_emscripten_glTexParameterf,Jg:_emscripten_glTexParameterfv,Kg:_emscripten_glTexParameteri,Lg:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Ib:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Bc:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,qc:_emscripten_glUniform1ui,mc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,pc:_emscripten_glUniform2ui,lc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,oc:_emscripten_glUniform3ui,kc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,nc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Oc:_emscripten_glUniformMatrix2x3fv,Mc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Nc:_emscripten_glUniformMatrix3x2fv,Kc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Lc:_emscripten_glUniformMatrix4x2fv,Jc:_emscripten_glUniformMatrix4x3fv,gf:_emscripten_glUseProgram,ed:_emscripten_glValidateProgram,hf:_emscripten_glVertexAttrib1f,dd:_emscripten_glVertexAttrib1fv,cd:_emscripten_glVertexAttrib2f,jf:_emscripten_glVertexAttrib2fv,bd:_emscripten_glVertexAttrib3f,kf:_emscripten_glVertexAttrib3fv,ad:_emscripten_glVertexAttrib4f,lf:_emscripten_glVertexAttrib4fv,Me:_emscripten_glVertexAttribDivisor,Kd:_emscripten_glVertexAttribDivisorANGLE,Fb:_emscripten_glVertexAttribDivisorARB,Yc:_emscripten_glVertexAttribDivisorEXT,Gb:_emscripten_glVertexAttribDivisorNV,xc:_emscripten_glVertexAttribI4i,uc:_emscripten_glVertexAttribI4iv,wc:_emscripten_glVertexAttribI4ui,tc:_emscripten_glVertexAttribI4uiv,Ne:_emscripten_glVertexAttribIPointer,mf:_emscripten_glVertexAttribPointer,nf:_emscripten_glViewport,re:_emscripten_glWaitSync,Za:_emscripten_request_animation_frame_loop,hb:_emscripten_resize_heap,ob:_environ_get,pb:_environ_sizes_get,Ra:_exit,la:_fd_close,jb:_fd_pread,Ea:_fd_read,nb:_fd_seek,ka:_fd_write,Pa:_glGetIntegerv,pa:_glGetString,Qa:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,Aa:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,Ba:invoke_fiii,s:invoke_fiiidi,U:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,E:invoke_iif,$a:invoke_iiffi,ra:invoke_iiffiii,f:invoke_iii,Ja:invoke_iiiffii,ta:invoke_iiifi,g:invoke_iiii,T:invoke_iiiiff,l:invoke_iiiii,db:invoke_iiiiid,A:invoke_iiiiii,y:invoke_iiiiiii,F:invoke_iiiiiiii,q:invoke_iiiiiiiii,qa:invoke_iiiiiiiiii,ca:invoke_iiiiiiiiiiii,oa:invoke_iiiiiiiiiiiifiii,R:invoke_iij,fb:invoke_j,ha:invoke_ji,_:invoke_jiii,da:invoke_jiiii,J:invoke_jjji,k:invoke_v,Pf:invoke_vff,b:invoke_vi,P:invoke_vid,S:invoke_vif,u:invoke_viff,D:invoke_viffff,Z:invoke_vifffff,Va:invoke_viffffff,C:invoke_viffi,ia:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,O:invoke_viif,v:invoke_viiff,$:invoke_viifi,va:invoke_viififii,x:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,xa:invoke_viiiff,B:invoke_viiiffi,K:invoke_viiiffiffii,L:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ja:invoke_viiiif,ya:invoke_viiiiff,Da:invoke_viiiiffi,wa:invoke_viiiifi,h:invoke_viiiii,ab:invoke_viiiiif,Ia:invoke_viiiiiff,Wa:invoke_viiiiiffiii,cb:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,V:invoke_viiiiiiii,Y:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,sa:invoke_viiiiiiiiiii,ba:invoke_viiiiiiiiiiiiiii,La:invoke_viiiiiji,Sb:invoke_viiiijjiiiiff,Q:invoke_viiij,z:invoke_viiijii,X:invoke_viij,n:invoke_viiji,ma:invoke_viijiffi,fa:invoke_viijii,bb:invoke_viijiii,aa:invoke_viijiiiif,Ka:invoke_viijiiiii,ua:invoke_viijj,W:invoke_viji,w:invoke_vijii,Ha:invoke_vijiifi,_a:invoke_vijiififi,za:invoke_vijiii,ea:invoke_vijjjj,vc:invoke_vjii,na:_llvm_eh_typeid_for,qb:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_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_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_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}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_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_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_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_viijiiiii(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_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_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_iiiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(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_viijii(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_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_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_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_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_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_viiiff(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_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_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_vijiififi(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_iij(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_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_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(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_viifi(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_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +var createGridaCanvas=(()=>{var _scriptName=globalThis.document?.currentScript?.src;return async function(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};if(typeof __filename!="undefined"){_scriptName=__filename}else{}var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");scriptDirectory=__dirname+"/";readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");var readyPromiseResolve,readyPromiseReject;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;var HEAP64,HEAPU64;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;if(!Module["noFSInit"]&&!FS.initialized)FS.init();TTY.init();wasmExports["Ng"]();FS.ignorePermissions=false}function preMain(){}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){return locateFile("grida_canvas_wasm.wasm")}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_find_matching_catch_4=(arg0,arg1)=>findMatchingCatch([arg0,arg1]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;throw exceptionLast};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);___cxa_increment_exception_refcount(ptr);exceptionLast=ptr;uncaughtExceptionCount++;throw exceptionLast};var ___cxa_uncaught_exceptions=()=>uncaughtExceptionCount;var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}throw exceptionLast};var syscallGetVarargI=()=>{var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret};var syscallGetVarargP=syscallGetVarargI;var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.slice(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.slice(0,-1)}return root+dir},basename:path=>path&&path.match(/([^\/]+|\/)\/*$/)[1],join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(ENVIRONMENT_IS_NODE){var nodeCrypto=require("node:crypto");return view=>nodeCrypto.randomFillSync(view)}return view=>crypto.getRandomValues(view)};var randomFill=view=>{(randomFill=initRandomFill())(view)};var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).slice(1);to=PATH_FS.resolve(to).slice(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var FS_stdin_getChar_buffer=[];var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var intArrayFromString=(stringy,dontAddNull,length)=>{var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array};var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(globalThis.window?.prompt){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output?.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(ptr,size)=>HEAPU8.fill(0,ptr,ptr+size);var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16895,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.atime=node.mtime=node.ctime=Date.now();if(parent){parent.contents[name]=node;parent.atime=parent.mtime=parent.ctime=node.atime}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.atime);attr.mtime=new Date(node.mtime);attr.ctime=new Date(node.ctime);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){for(const key of["mode","atime","mtime","ctime"]){if(attr[key]!=null){node[key]=attr[key]}}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){if(!MEMFS.doesNotExistError){MEMFS.doesNotExistError=new FS.ErrnoError(44);MEMFS.doesNotExistError.stack=""}throw MEMFS.doesNotExistError},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){if(FS.isDir(old_node.mode)){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}FS.hashRemoveNode(new_node)}delete old_node.parent.contents[old_node.name];new_dir.contents[new_name]=old_node;old_node.name=new_name;new_dir.ctime=new_dir.mtime=old_node.parent.ctime=old_node.parent.mtime=Date.now()},unlink(parent,name){delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.ctime=parent.mtime=Date.now()},readdir(node){return[".","..",...Object.keys(node.contents)]},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var asyncLoad=async url=>{var arrayBuffer=await readAsync(url);return new Uint8Array(arrayBuffer)};var FS_createDataFile=(...args)=>FS.createDataFile(...args);var getUniqueRunDependency=id=>id;var runDependencies=0;var dependenciesFulfilled=null;var removeRunDependency=id=>{runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(runDependencies==0){if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}};var addRunDependency=id=>{runDependencies++;Module["monitorRunDependencies"]?.(runDependencies)};var preloadPlugins=[];var FS_handledByPreloadPlugin=async(byteArray,fullname)=>{if(typeof Browser!="undefined")Browser.init();for(var plugin of preloadPlugins){if(plugin["canHandle"](fullname)){return plugin["handle"](byteArray,fullname)}}return byteArray};var FS_preloadFile=async(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);addRunDependency(dep);try{var byteArray=url;if(typeof url=="string"){byteArray=await asyncLoad(url)}byteArray=await FS_handledByPreloadPlugin(byteArray,fullname);preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}}finally{removeRunDependency(dep)}};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{FS_preloadFile(parent,name,url,canRead,canWrite,dontCreateFile,canOwn,preFinish).then(onload).catch(onerror)};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,filesystems:null,syncFSRequests:0,readFiles:{},ErrnoError:class{name="ErrnoError";constructor(errno){this.errno=errno}},FSStream:class{shared={};get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{node_ops={};stream_ops={};readMode=292|73;writeMode=146;mounted=null;constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.rdev=rdev;this.atime=this.mtime=this.ctime=Date.now()}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){if(!path){throw new FS.ErrnoError(44)}opts.follow_mount??=true;if(!PATH.isAbs(path)){path=FS.cwd()+"/"+path}linkloop:for(var nlinks=0;nlinks<40;nlinks++){var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){if(!FS.isDir(dir.mode)){return 54}try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&(512|64)){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},checkOpExists(op,err){if(!op){throw new FS.ErrnoError(err)}return op},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},doSetAttr(stream,node,attr){var setattr=stream?.stream_ops.setattr;var arg=setattr?stream:node;setattr??=node.node_ops.setattr;FS.checkOpExists(setattr,63);setattr(arg,attr)},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}for(var mount of mounts){if(mount.type.syncfs){mount.type.syncfs(mount,populate,done)}else{done(null)}}},mount(type,opts,mountpoint){var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);for(var[hash,current]of Object.entries(FS.nameTable)){while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}}node.mounted=null;var idx=node.mount.mounts.indexOf(mount);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name){throw new FS.ErrnoError(28)}if(name==="."||name===".."){throw new FS.ErrnoError(20)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},statfs(path){return FS.statfsNode(FS.lookupPath(path,{follow:true}).node)},statfsStream(stream){return FS.statfsNode(stream.node)},statfsNode(node){var rtn={bsize:4096,frsize:4096,blocks:1e6,bfree:5e5,bavail:5e5,files:FS.nextInode,ffree:FS.nextInode-1,fsid:42,flags:2,namelen:255};if(node.node_ops.statfs){Object.assign(rtn,node.node_ops.statfs(node.mount.opts.root))}return rtn},create(path,mode=438){mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode=511){mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var dir of dirs){if(!dir)continue;if(d||PATH.isAbs(path))d+="/";d+=dir;try{FS.mkdir(d,mode)}catch(e){if(e.errno!=20)throw e}}},mkdev(path,mode,dev){if(typeof dev=="undefined"){dev=mode;mode=438}mode|=8192;return FS.mknod(path,mode,dev)},symlink(oldpath,newpath){if(!PATH_FS.resolve(oldpath)){throw new FS.ErrnoError(44)}var lookup=FS.lookupPath(newpath,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var newname=PATH.basename(newpath);var errCode=FS.mayCreate(parent,newname);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.symlink){throw new FS.ErrnoError(63)}return parent.node_ops.symlink(parent,newname,oldpath)},rename(old_path,new_path){var old_dirname=PATH.dirname(old_path);var new_dirname=PATH.dirname(new_path);var old_name=PATH.basename(old_path);var new_name=PATH.basename(new_path);var lookup,old_dir,new_dir;lookup=FS.lookupPath(old_path,{parent:true});old_dir=lookup.node;lookup=FS.lookupPath(new_path,{parent:true});new_dir=lookup.node;if(!old_dir||!new_dir)throw new FS.ErrnoError(44);if(old_dir.mount!==new_dir.mount){throw new FS.ErrnoError(75)}var old_node=FS.lookupNode(old_dir,old_name);var relative=PATH_FS.relative(old_path,new_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(28)}relative=PATH_FS.relative(new_path,old_dirname);if(relative.charAt(0)!=="."){throw new FS.ErrnoError(55)}var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(old_node===new_node){return}var isdir=FS.isDir(old_node.mode);var errCode=FS.mayDelete(old_dir,old_name,isdir);if(errCode){throw new FS.ErrnoError(errCode)}errCode=new_node?FS.mayDelete(new_dir,new_name,isdir):FS.mayCreate(new_dir,new_name);if(errCode){throw new FS.ErrnoError(errCode)}if(!old_dir.node_ops.rename){throw new FS.ErrnoError(63)}if(FS.isMountpoint(old_node)||new_node&&FS.isMountpoint(new_node)){throw new FS.ErrnoError(10)}if(new_dir!==old_dir){errCode=FS.nodePermissions(old_dir,"w");if(errCode){throw new FS.ErrnoError(errCode)}}FS.hashRemoveNode(old_node);try{old_dir.node_ops.rename(old_node,new_dir,new_name);old_node.parent=new_dir}catch(e){throw e}finally{FS.hashAddNode(old_node)}},rmdir(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,true);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.rmdir){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.rmdir(parent,name);FS.destroyNode(node)},readdir(path){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var readdir=FS.checkOpExists(node.node_ops.readdir,54);return readdir(node)},unlink(path){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;if(!parent){throw new FS.ErrnoError(44)}var name=PATH.basename(path);var node=FS.lookupNode(parent,name);var errCode=FS.mayDelete(parent,name,false);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.unlink){throw new FS.ErrnoError(63)}if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}parent.node_ops.unlink(parent,name);FS.destroyNode(node)},readlink(path){var lookup=FS.lookupPath(path);var link=lookup.node;if(!link){throw new FS.ErrnoError(44)}if(!link.node_ops.readlink){throw new FS.ErrnoError(28)}return link.node_ops.readlink(link)},stat(path,dontFollow){var lookup=FS.lookupPath(path,{follow:!dontFollow});var node=lookup.node;var getattr=FS.checkOpExists(node.node_ops.getattr,63);return getattr(node)},fstat(fd){var stream=FS.getStreamChecked(fd);var node=stream.node;var getattr=stream.stream_ops.getattr;var arg=getattr?stream:node;getattr??=node.node_ops.getattr;FS.checkOpExists(getattr,63);return getattr(arg)},lstat(path){return FS.stat(path,true)},doChmod(stream,node,mode,dontFollow){FS.doSetAttr(stream,node,{mode:mode&4095|node.mode&~4095,ctime:Date.now(),dontFollow})},chmod(path,mode,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChmod(null,node,mode,dontFollow)},lchmod(path,mode){FS.chmod(path,mode,true)},fchmod(fd,mode){var stream=FS.getStreamChecked(fd);FS.doChmod(stream,stream.node,mode,false)},doChown(stream,node,dontFollow){FS.doSetAttr(stream,node,{timestamp:Date.now(),dontFollow})},chown(path,uid,gid,dontFollow){var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:!dontFollow});node=lookup.node}else{node=path}FS.doChown(null,node,dontFollow)},lchown(path,uid,gid){FS.chown(path,uid,gid,true)},fchown(fd,uid,gid){var stream=FS.getStreamChecked(fd);FS.doChown(stream,stream.node,false)},doTruncate(stream,node,len){if(FS.isDir(node.mode)){throw new FS.ErrnoError(31)}if(!FS.isFile(node.mode)){throw new FS.ErrnoError(28)}var errCode=FS.nodePermissions(node,"w");if(errCode){throw new FS.ErrnoError(errCode)}FS.doSetAttr(stream,node,{size:len,timestamp:Date.now()})},truncate(path,len){if(len<0){throw new FS.ErrnoError(28)}var node;if(typeof path=="string"){var lookup=FS.lookupPath(path,{follow:true});node=lookup.node}else{node=path}FS.doTruncate(null,node,len)},ftruncate(fd,len){var stream=FS.getStreamChecked(fd);if(len<0||(stream.flags&2097155)===0){throw new FS.ErrnoError(28)}FS.doTruncate(stream,stream.node,len)},utime(path,atime,mtime){var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;var setattr=FS.checkOpExists(node.node_ops.setattr,63);setattr(node,{atime,mtime})},open(path,flags,mode=438){if(path===""){throw new FS.ErrnoError(44)}flags=typeof flags=="string"?FS_modeStringToFlags(flags):flags;if(flags&64){mode=mode&4095|32768}else{mode=0}var node;var isDirPath;if(typeof path=="object"){node=path}else{isDirPath=path.endsWith("/");var lookup=FS.lookupPath(path,{follow:!(flags&131072),noent_okay:true});node=lookup.node;path=lookup.path}var created=false;if(flags&64){if(node){if(flags&128){throw new FS.ErrnoError(20)}}else if(isDirPath){throw new FS.ErrnoError(31)}else{node=FS.mknod(path,mode|511,0);created=true}}if(!node){throw new FS.ErrnoError(44)}if(FS.isChrdev(node.mode)){flags&=~512}if(flags&65536&&!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}if(!created){var errCode=FS.mayOpen(node,flags);if(errCode){throw new FS.ErrnoError(errCode)}}if(flags&512&&!created){FS.truncate(node,0)}flags&=~(128|512|131072);var stream=FS.createStream({node,path:FS.getPath(node),flags,seekable:true,position:0,stream_ops:node.stream_ops,ungotten:[],error:false});if(stream.stream_ops.open){stream.stream_ops.open(stream)}if(created){FS.chmod(node,mode&511)}if(Module["logReadFiles"]&&!(flags&1)){if(!(path in FS.readFiles)){FS.readFiles[path]=1}}return stream},close(stream){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(stream.getdents)stream.getdents=null;try{if(stream.stream_ops.close){stream.stream_ops.close(stream)}}catch(e){throw e}finally{FS.closeStream(stream.fd)}stream.fd=null},isClosed(stream){return stream.fd===null},llseek(stream,offset,whence){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(!stream.seekable||!stream.stream_ops.llseek){throw new FS.ErrnoError(70)}if(whence!=0&&whence!=1&&whence!=2){throw new FS.ErrnoError(28)}stream.position=stream.stream_ops.llseek(stream,offset,whence);stream.ungotten=[];return stream.position},read(stream,buffer,offset,length,position){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){abort(`Invalid encoding type "${opts.encoding}"`)}var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){buf=UTF8ArrayToString(buf)}FS.close(stream);return buf},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){data=new Uint8Array(intArrayFromString(data,true))}if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{abort("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length,llseek:()=>0});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomFill(randomBuffer);randomLeft=randomBuffer.byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16895,73);node.stream_ops={llseek:MEMFS.stream_ops.llseek};node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path},id:fd+1};ret.parent=ret;return ret},readdir(){return Array.from(FS.streams.entries()).filter(([k,v])=>v).map(([k,v])=>k.toString())}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1)},staticInit(){FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;for(var stream of FS.streams){if(stream){FS.close(stream)}}},findObject(path,dontResolveLastLink){var ret=FS.analyzePath(path,dontResolveLastLink);if(!ret.exists){return null}return ret.object},analyzePath(path,dontResolveLastLink){try{var lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});path=lookup.path}catch(e){}var ret={isRoot:false,exists:false,error:0,name:null,path:null,object:null,parentExists:false,parentPath:null,parentObject:null};try{var lookup=FS.lookupPath(path,{parent:true});ret.parentExists=true;ret.parentPath=lookup.path;ret.parentObject=lookup.node;ret.name=PATH.basename(path);lookup=FS.lookupPath(path,{follow:!dontResolveLastLink});ret.exists=true;ret.path=lookup.path;ret.object=lookup.node;ret.name=lookup.node.name;ret.isRoot=lookup.path==="/"}catch(e){ret.error=e.errno}return ret},createPath(parent,path,canRead,canWrite){parent=typeof parent=="string"?parent:FS.getPath(parent);var parts=path.split("/").reverse();while(parts.length){var part=parts.pop();if(!part)continue;var current=PATH.join2(parent,part);try{FS.mkdir(current)}catch(e){if(e.errno!=20)throw e}parent=current}return current},createFile(parent,name,properties,canRead,canWrite){var path=PATH.join2(typeof parent=="string"?parent:FS.getPath(parent),name);var mode=FS_getMode(canRead,canWrite);return FS.create(path,mode)},createDataFile(parent,name,data,canRead,canWrite,canOwn){var path=name;if(parent){parent=typeof parent=="string"?parent:FS.getPath(parent);path=name?PATH.join2(parent,name):parent}var mode=FS_getMode(canRead,canWrite);var node=FS.create(path,mode);if(data){if(typeof data=="string"){var arr=new Array(data.length);for(var i=0,len=data.length;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)abort("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)abort("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))abort("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")abort("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(globalThis.XMLHttpRequest){if(!ENVIRONMENT_IS_WORKER)abort("Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc");var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};for(const[key,fn]of Object.entries(node.stream_ops)){stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}}function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var SYSCALLS={calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return dir+"/"+path},writeStat(buf,stat){HEAPU32[buf>>2]=stat.dev;HEAPU32[buf+4>>2]=stat.mode;HEAPU32[buf+8>>2]=stat.nlink;HEAPU32[buf+12>>2]=stat.uid;HEAPU32[buf+16>>2]=stat.gid;HEAPU32[buf+20>>2]=stat.rdev;HEAP64[buf+24>>3]=BigInt(stat.size);HEAP32[buf+32>>2]=4096;HEAP32[buf+36>>2]=stat.blocks;var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();HEAP64[buf+40>>3]=BigInt(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;HEAP64[buf+56>>3]=BigInt(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;HEAP64[buf+72>>3]=BigInt(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;HEAP64[buf+88>>3]=BigInt(stat.ino);return 0},writeStatFs(buf,stats){HEAPU32[buf+4>>2]=stats.bsize;HEAPU32[buf+60>>2]=stats.bsize;HEAP64[buf+8>>3]=BigInt(stats.blocks);HEAP64[buf+16>>3]=BigInt(stats.bfree);HEAP64[buf+24>>3]=BigInt(stats.bavail);HEAP64[buf+32>>3]=BigInt(stats.files);HEAP64[buf+40>>3]=BigInt(stats.ffree);HEAPU32[buf+48>>2]=stats.fsid;HEAPU32[buf+64>>2]=stats.flags;HEAPU32[buf+56>>2]=stats.namelen},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{return SYSCALLS.writeStat(buf,FS.fstat(fd))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);function ___syscall_getcwd(buf,size){try{if(size===0)return-28;var cwd=FS.cwd();var cwdLengthInBytes=lengthBytesUTF8(cwd)+1;if(size>2]=termios.c_iflag||0;HEAP32[argp+4>>2]=termios.c_oflag||0;HEAP32[argp+8>>2]=termios.c_cflag||0;HEAP32[argp+12>>2]=termios.c_lflag||0;for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21537:case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];HEAP16[argp+2>>1]=winsize[1]}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.lstat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.writeStat(buf,nofollow?FS.lstat(path):FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.writeStat(buf,FS.stat(path))}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>abort("");var __emscripten_throw_longjmp=()=>{throw Infinity};var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function __gmtime_js(time,tmPtr){time=bigintToI53Checked(time);var date=new Date(time*1e3);HEAP32[tmPtr>>2]=date.getUTCSeconds();HEAP32[tmPtr+4>>2]=date.getUTCMinutes();HEAP32[tmPtr+8>>2]=date.getUTCHours();HEAP32[tmPtr+12>>2]=date.getUTCDate();HEAP32[tmPtr+16>>2]=date.getUTCMonth();HEAP32[tmPtr+20>>2]=date.getUTCFullYear()-1900;HEAP32[tmPtr+24>>2]=date.getUTCDay();var start=Date.UTC(date.getUTCFullYear(),0,1,0,0,0,0);var yday=(date.getTime()-start)/(1e3*60*60*24)|0;HEAP32[tmPtr+28>>2]=yday}function __mmap_js(len,prot,flags,fd,offset,allocated,addr){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset){offset=bigintToI53Checked(offset);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);if(summerOffsetperformance.now();var _emscripten_date_now=()=>Date.now();var nowIsMonotonic=1;var checkWasiClock=clock_id=>clock_id>=0&&clock_id<=3;function _clock_time_get(clk_id,ignored_precision,ptime){ignored_precision=bigintToI53Checked(ignored_precision);if(!checkWasiClock(clk_id)){return 28}var now;if(clk_id===0){now=_emscripten_date_now()}else if(nowIsMonotonic){now=_emscripten_get_now()}else{return 52}var nsec=Math.round(now*1e3*1e3);HEAP64[ptime>>3]=BigInt(nsec);return 0}var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var GLctx;var webgl_enable_ANGLE_instanced_arrays=ctx=>{var ext=ctx.getExtension("ANGLE_instanced_arrays");if(ext){ctx["vertexAttribDivisor"]=(index,divisor)=>ext["vertexAttribDivisorANGLE"](index,divisor);ctx["drawArraysInstanced"]=(mode,first,count,primcount)=>ext["drawArraysInstancedANGLE"](mode,first,count,primcount);ctx["drawElementsInstanced"]=(mode,count,type,indices,primcount)=>ext["drawElementsInstancedANGLE"](mode,count,type,indices,primcount);return 1}};var webgl_enable_OES_vertex_array_object=ctx=>{var ext=ctx.getExtension("OES_vertex_array_object");if(ext){ctx["createVertexArray"]=()=>ext["createVertexArrayOES"]();ctx["deleteVertexArray"]=vao=>ext["deleteVertexArrayOES"](vao);ctx["bindVertexArray"]=vao=>ext["bindVertexArrayOES"](vao);ctx["isVertexArray"]=vao=>ext["isVertexArrayOES"](vao);return 1}};var webgl_enable_WEBGL_draw_buffers=ctx=>{var ext=ctx.getExtension("WEBGL_draw_buffers");if(ext){ctx["drawBuffers"]=(n,bufs)=>ext["drawBuffersWEBGL"](n,bufs);return 1}};var webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.dibvbi=ctx.getExtension("WEBGL_draw_instanced_base_vertex_base_instance"));var webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance=ctx=>!!(ctx.mdibvbi=ctx.getExtension("WEBGL_multi_draw_instanced_base_vertex_base_instance"));var webgl_enable_EXT_polygon_offset_clamp=ctx=>!!(ctx.extPolygonOffsetClamp=ctx.getExtension("EXT_polygon_offset_clamp"));var webgl_enable_EXT_clip_control=ctx=>!!(ctx.extClipControl=ctx.getExtension("EXT_clip_control"));var webgl_enable_WEBGL_polygon_mode=ctx=>!!(ctx.webglPolygonMode=ctx.getExtension("WEBGL_polygon_mode"));var webgl_enable_WEBGL_multi_draw=ctx=>!!(ctx.multiDrawWebgl=ctx.getExtension("WEBGL_multi_draw"));var getEmscriptenSupportedExtensions=ctx=>{var supportedExtensions=["ANGLE_instanced_arrays","EXT_blend_minmax","EXT_disjoint_timer_query","EXT_frag_depth","EXT_shader_texture_lod","EXT_sRGB","OES_element_index_uint","OES_fbo_render_mipmap","OES_standard_derivatives","OES_texture_float","OES_texture_half_float","OES_texture_half_float_linear","OES_vertex_array_object","WEBGL_color_buffer_float","WEBGL_depth_texture","WEBGL_draw_buffers","EXT_color_buffer_float","EXT_conservative_depth","EXT_disjoint_timer_query_webgl2","EXT_texture_norm16","NV_shader_noperspective_interpolation","WEBGL_clip_cull_distance","EXT_clip_control","EXT_color_buffer_half_float","EXT_depth_clamp","EXT_float_blend","EXT_polygon_offset_clamp","EXT_texture_compression_bptc","EXT_texture_compression_rgtc","EXT_texture_filter_anisotropic","KHR_parallel_shader_compile","OES_texture_float_linear","WEBGL_blend_func_extended","WEBGL_compressed_texture_astc","WEBGL_compressed_texture_etc","WEBGL_compressed_texture_etc1","WEBGL_compressed_texture_s3tc","WEBGL_compressed_texture_s3tc_srgb","WEBGL_debug_renderer_info","WEBGL_debug_shaders","WEBGL_lose_context","WEBGL_multi_draw","WEBGL_polygon_mode"];return(ctx.getSupportedExtensions()||[]).filter(ext=>supportedExtensions.includes(ext))};var GL={counter:1,buffers:[],programs:[],framebuffers:[],renderbuffers:[],textures:[],shaders:[],vaos:[],contexts:[],offscreenCanvases:{},queries:[],samplers:[],transformFeedbacks:[],syncs:[],stringCache:{},stringiCache:{},unpackAlignment:4,unpackRowLength:0,recordError:errorCode=>{if(!GL.lastError){GL.lastError=errorCode}},getNewId:table=>{var ret=GL.counter++;for(var i=table.length;i{for(var i=0;i>2]=id}},getSource:(shader,count,string,length)=>{var source="";for(var i=0;i>2]:undefined;source+=UTF8ToString(HEAPU32[string+i*4>>2],len)}return source},createContext:(canvas,webGLContextAttributes)=>{if(!canvas.getContextSafariWebGL2Fixed){canvas.getContextSafariWebGL2Fixed=canvas.getContext;function fixedGetContext(ver,attrs){var gl=canvas.getContextSafariWebGL2Fixed(ver,attrs);return ver=="webgl"==gl instanceof WebGLRenderingContext?gl:null}canvas.getContext=fixedGetContext}var ctx=webGLContextAttributes.majorVersion>1?canvas.getContext("webgl2",webGLContextAttributes):canvas.getContext("webgl",webGLContextAttributes);if(!ctx)return 0;var handle=GL.registerContext(ctx,webGLContextAttributes);return handle},registerContext:(ctx,webGLContextAttributes)=>{var handle=GL.getNewId(GL.contexts);var context={handle,attributes:webGLContextAttributes,version:webGLContextAttributes.majorVersion,GLctx:ctx};if(ctx.canvas)ctx.canvas.GLctxObject=context;GL.contexts[handle]=context;if(typeof webGLContextAttributes.enableExtensionsByDefault=="undefined"||webGLContextAttributes.enableExtensionsByDefault){GL.initExtensions(context)}return handle},makeContextCurrent:contextHandle=>{GL.currentContext=GL.contexts[contextHandle];Module["ctx"]=GLctx=GL.currentContext?.GLctx;return!(contextHandle&&!GLctx)},getContext:contextHandle=>GL.contexts[contextHandle],deleteContext:contextHandle=>{if(GL.currentContext===GL.contexts[contextHandle]){GL.currentContext=null}if(typeof JSEvents=="object"){JSEvents.removeAllHandlersOnTarget(GL.contexts[contextHandle].GLctx.canvas)}if(GL.contexts[contextHandle]?.GLctx.canvas){GL.contexts[contextHandle].GLctx.canvas.GLctxObject=undefined}GL.contexts[contextHandle]=null},initExtensions:context=>{context||=GL.currentContext;if(context.initExtensionsDone)return;context.initExtensionsDone=true;var GLctx=context.GLctx;webgl_enable_WEBGL_multi_draw(GLctx);webgl_enable_EXT_polygon_offset_clamp(GLctx);webgl_enable_EXT_clip_control(GLctx);webgl_enable_WEBGL_polygon_mode(GLctx);webgl_enable_ANGLE_instanced_arrays(GLctx);webgl_enable_OES_vertex_array_object(GLctx);webgl_enable_WEBGL_draw_buffers(GLctx);webgl_enable_WEBGL_draw_instanced_base_vertex_base_instance(GLctx);webgl_enable_WEBGL_multi_draw_instanced_base_vertex_base_instance(GLctx);if(context.version>=2){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query_webgl2")}if(context.version<2||!GLctx.disjointTimerQueryExt){GLctx.disjointTimerQueryExt=GLctx.getExtension("EXT_disjoint_timer_query")}for(var ext of getEmscriptenSupportedExtensions(GLctx)){if(!ext.includes("lose_context")&&!ext.includes("debug")){GLctx.getExtension(ext)}}}};var _emscripten_glActiveTexture=x0=>GLctx.activeTexture(x0);var _emscripten_glAttachShader=(program,shader)=>{GLctx.attachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glBeginQuery=(target,id)=>{GLctx.beginQuery(target,GL.queries[id])};var _emscripten_glBeginQueryEXT=(target,id)=>{GLctx.disjointTimerQueryExt["beginQueryEXT"](target,GL.queries[id])};var _emscripten_glBeginTransformFeedback=x0=>GLctx.beginTransformFeedback(x0);var _emscripten_glBindAttribLocation=(program,index,name)=>{GLctx.bindAttribLocation(GL.programs[program],index,UTF8ToString(name))};var _emscripten_glBindBuffer=(target,buffer)=>{if(target==35051){GLctx.currentPixelPackBufferBinding=buffer}else if(target==35052){GLctx.currentPixelUnpackBufferBinding=buffer}GLctx.bindBuffer(target,GL.buffers[buffer])};var _emscripten_glBindBufferBase=(target,index,buffer)=>{GLctx.bindBufferBase(target,index,GL.buffers[buffer])};var _emscripten_glBindBufferRange=(target,index,buffer,offset,ptrsize)=>{GLctx.bindBufferRange(target,index,GL.buffers[buffer],offset,ptrsize)};var _emscripten_glBindFramebuffer=(target,framebuffer)=>{GLctx.bindFramebuffer(target,GL.framebuffers[framebuffer])};var _emscripten_glBindRenderbuffer=(target,renderbuffer)=>{GLctx.bindRenderbuffer(target,GL.renderbuffers[renderbuffer])};var _emscripten_glBindSampler=(unit,sampler)=>{GLctx.bindSampler(unit,GL.samplers[sampler])};var _emscripten_glBindTexture=(target,texture)=>{GLctx.bindTexture(target,GL.textures[texture])};var _emscripten_glBindTransformFeedback=(target,id)=>{GLctx.bindTransformFeedback(target,GL.transformFeedbacks[id])};var _emscripten_glBindVertexArray=vao=>{GLctx.bindVertexArray(GL.vaos[vao])};var _glBindVertexArray=_emscripten_glBindVertexArray;var _emscripten_glBindVertexArrayOES=_glBindVertexArray;var _emscripten_glBlendColor=(x0,x1,x2,x3)=>GLctx.blendColor(x0,x1,x2,x3);var _emscripten_glBlendEquation=x0=>GLctx.blendEquation(x0);var _emscripten_glBlendEquationSeparate=(x0,x1)=>GLctx.blendEquationSeparate(x0,x1);var _emscripten_glBlendFunc=(x0,x1)=>GLctx.blendFunc(x0,x1);var _emscripten_glBlendFuncSeparate=(x0,x1,x2,x3)=>GLctx.blendFuncSeparate(x0,x1,x2,x3);var _emscripten_glBlitFramebuffer=(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9)=>GLctx.blitFramebuffer(x0,x1,x2,x3,x4,x5,x6,x7,x8,x9);var _emscripten_glBufferData=(target,size,data,usage)=>{if(GL.currentContext.version>=2){if(data&&size){GLctx.bufferData(target,HEAPU8,usage,data,size)}else{GLctx.bufferData(target,size,usage)}return}GLctx.bufferData(target,data?HEAPU8.subarray(data,data+size):size,usage)};var _emscripten_glBufferSubData=(target,offset,size,data)=>{if(GL.currentContext.version>=2){size&&GLctx.bufferSubData(target,offset,HEAPU8,data,size);return}GLctx.bufferSubData(target,offset,HEAPU8.subarray(data,data+size))};var _emscripten_glCheckFramebufferStatus=x0=>GLctx.checkFramebufferStatus(x0);var _emscripten_glClear=x0=>GLctx.clear(x0);var _emscripten_glClearBufferfi=(x0,x1,x2,x3)=>GLctx.clearBufferfi(x0,x1,x2,x3);var _emscripten_glClearBufferfv=(buffer,drawbuffer,value)=>{GLctx.clearBufferfv(buffer,drawbuffer,HEAPF32,value>>2)};var _emscripten_glClearBufferiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferiv(buffer,drawbuffer,HEAP32,value>>2)};var _emscripten_glClearBufferuiv=(buffer,drawbuffer,value)=>{GLctx.clearBufferuiv(buffer,drawbuffer,HEAPU32,value>>2)};var _emscripten_glClearColor=(x0,x1,x2,x3)=>GLctx.clearColor(x0,x1,x2,x3);var _emscripten_glClearDepthf=x0=>GLctx.clearDepth(x0);var _emscripten_glClearStencil=x0=>GLctx.clearStencil(x0);var _emscripten_glClientWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);return GLctx.clientWaitSync(GL.syncs[sync],flags,timeout)};var _emscripten_glClipControlEXT=(origin,depth)=>{GLctx.extClipControl["clipControlEXT"](origin,depth)};var _emscripten_glColorMask=(red,green,blue,alpha)=>{GLctx.colorMask(!!red,!!green,!!blue,!!alpha)};var _emscripten_glCompileShader=shader=>{GLctx.compileShader(GL.shaders[shader])};var _emscripten_glCompressedTexImage2D=(target,level,internalFormat,width,height,border,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,imageSize,data);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8,data,imageSize);return}GLctx.compressedTexImage2D(target,level,internalFormat,width,height,border,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexImage3D=(target,level,internalFormat,width,height,depth,border,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,imageSize,data)}else{GLctx.compressedTexImage3D(target,level,internalFormat,width,height,depth,border,HEAPU8,data,imageSize)}};var _emscripten_glCompressedTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,imageSize,data)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding||!imageSize){GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,imageSize,data);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8,data,imageSize);return}GLctx.compressedTexSubImage2D(target,level,xoffset,yoffset,width,height,format,HEAPU8.subarray(data,data+imageSize))};var _emscripten_glCompressedTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,imageSize,data)}else{GLctx.compressedTexSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,HEAPU8,data,imageSize)}};var _emscripten_glCopyBufferSubData=(x0,x1,x2,x3,x4)=>GLctx.copyBufferSubData(x0,x1,x2,x3,x4);var _emscripten_glCopyTexImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage2D=(x0,x1,x2,x3,x4,x5,x6,x7)=>GLctx.copyTexSubImage2D(x0,x1,x2,x3,x4,x5,x6,x7);var _emscripten_glCopyTexSubImage3D=(x0,x1,x2,x3,x4,x5,x6,x7,x8)=>GLctx.copyTexSubImage3D(x0,x1,x2,x3,x4,x5,x6,x7,x8);var _emscripten_glCreateProgram=()=>{var id=GL.getNewId(GL.programs);var program=GLctx.createProgram();program.name=id;program.maxUniformLength=program.maxAttributeLength=program.maxUniformBlockNameLength=0;program.uniformIdCounter=1;GL.programs[id]=program;return id};var _emscripten_glCreateShader=shaderType=>{var id=GL.getNewId(GL.shaders);GL.shaders[id]=GLctx.createShader(shaderType);return id};var _emscripten_glCullFace=x0=>GLctx.cullFace(x0);var _emscripten_glDeleteBuffers=(n,buffers)=>{for(var i=0;i>2];var buffer=GL.buffers[id];if(!buffer)continue;GLctx.deleteBuffer(buffer);buffer.name=0;GL.buffers[id]=null;if(id==GLctx.currentPixelPackBufferBinding)GLctx.currentPixelPackBufferBinding=0;if(id==GLctx.currentPixelUnpackBufferBinding)GLctx.currentPixelUnpackBufferBinding=0}};var _emscripten_glDeleteFramebuffers=(n,framebuffers)=>{for(var i=0;i>2];var framebuffer=GL.framebuffers[id];if(!framebuffer)continue;GLctx.deleteFramebuffer(framebuffer);framebuffer.name=0;GL.framebuffers[id]=null}};var _emscripten_glDeleteProgram=id=>{if(!id)return;var program=GL.programs[id];if(!program){GL.recordError(1281);return}GLctx.deleteProgram(program);program.name=0;GL.programs[id]=null};var _emscripten_glDeleteQueries=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.deleteQuery(query);GL.queries[id]=null}};var _emscripten_glDeleteQueriesEXT=(n,ids)=>{for(var i=0;i>2];var query=GL.queries[id];if(!query)continue;GLctx.disjointTimerQueryExt["deleteQueryEXT"](query);GL.queries[id]=null}};var _emscripten_glDeleteRenderbuffers=(n,renderbuffers)=>{for(var i=0;i>2];var renderbuffer=GL.renderbuffers[id];if(!renderbuffer)continue;GLctx.deleteRenderbuffer(renderbuffer);renderbuffer.name=0;GL.renderbuffers[id]=null}};var _emscripten_glDeleteSamplers=(n,samplers)=>{for(var i=0;i>2];var sampler=GL.samplers[id];if(!sampler)continue;GLctx.deleteSampler(sampler);sampler.name=0;GL.samplers[id]=null}};var _emscripten_glDeleteShader=id=>{if(!id)return;var shader=GL.shaders[id];if(!shader){GL.recordError(1281);return}GLctx.deleteShader(shader);GL.shaders[id]=null};var _emscripten_glDeleteSync=id=>{if(!id)return;var sync=GL.syncs[id];if(!sync){GL.recordError(1281);return}GLctx.deleteSync(sync);sync.name=0;GL.syncs[id]=null};var _emscripten_glDeleteTextures=(n,textures)=>{for(var i=0;i>2];var texture=GL.textures[id];if(!texture)continue;GLctx.deleteTexture(texture);texture.name=0;GL.textures[id]=null}};var _emscripten_glDeleteTransformFeedbacks=(n,ids)=>{for(var i=0;i>2];var transformFeedback=GL.transformFeedbacks[id];if(!transformFeedback)continue;GLctx.deleteTransformFeedback(transformFeedback);transformFeedback.name=0;GL.transformFeedbacks[id]=null}};var _emscripten_glDeleteVertexArrays=(n,vaos)=>{for(var i=0;i>2];GLctx.deleteVertexArray(GL.vaos[id]);GL.vaos[id]=null}};var _glDeleteVertexArrays=_emscripten_glDeleteVertexArrays;var _emscripten_glDeleteVertexArraysOES=_glDeleteVertexArrays;var _emscripten_glDepthFunc=x0=>GLctx.depthFunc(x0);var _emscripten_glDepthMask=flag=>{GLctx.depthMask(!!flag)};var _emscripten_glDepthRangef=(x0,x1)=>GLctx.depthRange(x0,x1);var _emscripten_glDetachShader=(program,shader)=>{GLctx.detachShader(GL.programs[program],GL.shaders[shader])};var _emscripten_glDisable=x0=>GLctx.disable(x0);var _emscripten_glDisableVertexAttribArray=index=>{GLctx.disableVertexAttribArray(index)};var _emscripten_glDrawArrays=(mode,first,count)=>{GLctx.drawArrays(mode,first,count)};var _emscripten_glDrawArraysInstanced=(mode,first,count,primcount)=>{GLctx.drawArraysInstanced(mode,first,count,primcount)};var _glDrawArraysInstanced=_emscripten_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedANGLE=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedARB=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedBaseInstanceWEBGL=(mode,first,count,instanceCount,baseInstance)=>{GLctx.dibvbi["drawArraysInstancedBaseInstanceWEBGL"](mode,first,count,instanceCount,baseInstance)};var _emscripten_glDrawArraysInstancedEXT=_glDrawArraysInstanced;var _emscripten_glDrawArraysInstancedNV=_glDrawArraysInstanced;var tempFixedLengthArray=[];var _emscripten_glDrawBuffers=(n,bufs)=>{var bufArray=tempFixedLengthArray[n];for(var i=0;i>2]}GLctx.drawBuffers(bufArray)};var _glDrawBuffers=_emscripten_glDrawBuffers;var _emscripten_glDrawBuffersEXT=_glDrawBuffers;var _emscripten_glDrawBuffersWEBGL=_glDrawBuffers;var _emscripten_glDrawElements=(mode,count,type,indices)=>{GLctx.drawElements(mode,count,type,indices)};var _emscripten_glDrawElementsInstanced=(mode,count,type,indices,primcount)=>{GLctx.drawElementsInstanced(mode,count,type,indices,primcount)};var _glDrawElementsInstanced=_emscripten_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedANGLE=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedARB=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,count,type,offset,instanceCount,baseVertex,baseinstance)=>{GLctx.dibvbi["drawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,count,type,offset,instanceCount,baseVertex,baseinstance)};var _emscripten_glDrawElementsInstancedEXT=_glDrawElementsInstanced;var _emscripten_glDrawElementsInstancedNV=_glDrawElementsInstanced;var _glDrawElements=_emscripten_glDrawElements;var _emscripten_glDrawRangeElements=(mode,start,end,count,type,indices)=>{_glDrawElements(mode,count,type,indices)};var _emscripten_glEnable=x0=>GLctx.enable(x0);var _emscripten_glEnableVertexAttribArray=index=>{GLctx.enableVertexAttribArray(index)};var _emscripten_glEndQuery=x0=>GLctx.endQuery(x0);var _emscripten_glEndQueryEXT=target=>{GLctx.disjointTimerQueryExt["endQueryEXT"](target)};var _emscripten_glEndTransformFeedback=()=>GLctx.endTransformFeedback();var _emscripten_glFenceSync=(condition,flags)=>{var sync=GLctx.fenceSync(condition,flags);if(sync){var id=GL.getNewId(GL.syncs);sync.name=id;GL.syncs[id]=sync;return id}return 0};var _emscripten_glFinish=()=>GLctx.finish();var _emscripten_glFlush=()=>GLctx.flush();var _emscripten_glFramebufferRenderbuffer=(target,attachment,renderbuffertarget,renderbuffer)=>{GLctx.framebufferRenderbuffer(target,attachment,renderbuffertarget,GL.renderbuffers[renderbuffer])};var _emscripten_glFramebufferTexture2D=(target,attachment,textarget,texture,level)=>{GLctx.framebufferTexture2D(target,attachment,textarget,GL.textures[texture],level)};var _emscripten_glFramebufferTextureLayer=(target,attachment,texture,level,layer)=>{GLctx.framebufferTextureLayer(target,attachment,GL.textures[texture],level,layer)};var _emscripten_glFrontFace=x0=>GLctx.frontFace(x0);var _emscripten_glGenBuffers=(n,buffers)=>{GL.genObject(n,buffers,"createBuffer",GL.buffers)};var _emscripten_glGenFramebuffers=(n,ids)=>{GL.genObject(n,ids,"createFramebuffer",GL.framebuffers)};var _emscripten_glGenQueries=(n,ids)=>{GL.genObject(n,ids,"createQuery",GL.queries)};var _emscripten_glGenQueriesEXT=(n,ids)=>{for(var i=0;i>2]=0;return}var id=GL.getNewId(GL.queries);query.name=id;GL.queries[id]=query;HEAP32[ids+i*4>>2]=id}};var _emscripten_glGenRenderbuffers=(n,renderbuffers)=>{GL.genObject(n,renderbuffers,"createRenderbuffer",GL.renderbuffers)};var _emscripten_glGenSamplers=(n,samplers)=>{GL.genObject(n,samplers,"createSampler",GL.samplers)};var _emscripten_glGenTextures=(n,textures)=>{GL.genObject(n,textures,"createTexture",GL.textures)};var _emscripten_glGenTransformFeedbacks=(n,ids)=>{GL.genObject(n,ids,"createTransformFeedback",GL.transformFeedbacks)};var _emscripten_glGenVertexArrays=(n,arrays)=>{GL.genObject(n,arrays,"createVertexArray",GL.vaos)};var _glGenVertexArrays=_emscripten_glGenVertexArrays;var _emscripten_glGenVertexArraysOES=_glGenVertexArrays;var _emscripten_glGenerateMipmap=x0=>GLctx.generateMipmap(x0);var __glGetActiveAttribOrUniform=(funcName,program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx[funcName](program,index);if(info){var numBytesWrittenExclNull=name&&stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull;if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type}};var _emscripten_glGetActiveAttrib=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveAttrib",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniform=(program,index,bufSize,length,size,type,name)=>__glGetActiveAttribOrUniform("getActiveUniform",program,index,bufSize,length,size,type,name);var _emscripten_glGetActiveUniformBlockName=(program,uniformBlockIndex,bufSize,length,uniformBlockName)=>{program=GL.programs[program];var result=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);if(!result)return;if(uniformBlockName&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(result,uniformBlockName,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}};var _emscripten_glGetActiveUniformBlockiv=(program,uniformBlockIndex,pname,params)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];if(pname==35393){var name=GLctx.getActiveUniformBlockName(program,uniformBlockIndex);HEAP32[params>>2]=name.length+1;return}var result=GLctx.getActiveUniformBlockParameter(program,uniformBlockIndex,pname);if(result===null)return;if(pname==35395){for(var i=0;i>2]=result[i]}}else{HEAP32[params>>2]=result}};var _emscripten_glGetActiveUniformsiv=(program,uniformCount,uniformIndices,pname,params)=>{if(!params){GL.recordError(1281);return}if(uniformCount>0&&uniformIndices==0){GL.recordError(1281);return}program=GL.programs[program];var ids=[];for(var i=0;i>2])}var result=GLctx.getActiveUniforms(program,ids,pname);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var _emscripten_glGetAttachedShaders=(program,maxCount,count,shaders)=>{var result=GLctx.getAttachedShaders(GL.programs[program]);var len=result.length;if(len>maxCount){len=maxCount}HEAP32[count>>2]=len;for(var i=0;i>2]=id}};var _emscripten_glGetAttribLocation=(program,name)=>GLctx.getAttribLocation(GL.programs[program],UTF8ToString(name));var writeI53ToI64=(ptr,num)=>{HEAPU32[ptr>>2]=num;var lower=HEAPU32[ptr>>2];HEAPU32[ptr+4>>2]=(num-lower)/4294967296};var webglGetExtensions=()=>{var exts=getEmscriptenSupportedExtensions(GLctx);exts=exts.concat(exts.map(e=>"GL_"+e));return exts};var emscriptenWebGLGet=(name_,p,type)=>{if(!p){GL.recordError(1281);return}var ret=undefined;switch(name_){case 36346:ret=1;break;case 36344:if(type!=0&&type!=1){GL.recordError(1280)}return;case 34814:case 36345:ret=0;break;case 34466:var formats=GLctx.getParameter(34467);ret=formats?formats.length:0;break;case 33309:if(GL.currentContext.version<2){GL.recordError(1282);return}ret=webglGetExtensions().length;break;case 33307:case 33308:if(GL.currentContext.version<2){GL.recordError(1280);return}ret=name_==33307?3:0;break}if(ret===undefined){var result=GLctx.getParameter(name_);switch(typeof result){case"number":ret=result;break;case"boolean":ret=result?1:0;break;case"string":GL.recordError(1280);return;case"object":if(result===null){switch(name_){case 34964:case 35725:case 34965:case 36006:case 36007:case 32873:case 34229:case 36662:case 36663:case 35053:case 35055:case 36010:case 35097:case 35869:case 32874:case 36389:case 35983:case 35368:case 34068:{ret=0;break}default:{GL.recordError(1280);return}}}else if(result instanceof Float32Array||result instanceof Uint32Array||result instanceof Int32Array||result instanceof Array){for(var i=0;i>2]=result[i];break;case 2:HEAPF32[p+i*4>>2]=result[i];break;case 4:HEAP8[p+i]=result[i]?1:0;break}}return}else{try{ret=result.name|0}catch(e){GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Unknown object returned from WebGL getParameter(${name_})! (error: ${e})`);return}}break;default:GL.recordError(1280);err(`GL_INVALID_ENUM in glGet${type}v: Native code calling glGet${type}v(${name_}) and it returns ${result} of type ${typeof result}!`);return}}switch(type){case 1:writeI53ToI64(p,ret);break;case 0:HEAP32[p>>2]=ret;break;case 2:HEAPF32[p>>2]=ret;break;case 4:HEAP8[p]=ret?1:0;break}};var _emscripten_glGetBooleanv=(name_,p)=>emscriptenWebGLGet(name_,p,4);var _emscripten_glGetBufferParameteri64v=(target,value,data)=>{if(!data){GL.recordError(1281);return}writeI53ToI64(data,GLctx.getBufferParameter(target,value))};var _emscripten_glGetBufferParameteriv=(target,value,data)=>{if(!data){GL.recordError(1281);return}HEAP32[data>>2]=GLctx.getBufferParameter(target,value)};var _emscripten_glGetError=()=>{var error=GLctx.getError()||GL.lastError;GL.lastError=0;return error};var _emscripten_glGetFloatv=(name_,p)=>emscriptenWebGLGet(name_,p,2);var _emscripten_glGetFragDataLocation=(program,name)=>GLctx.getFragDataLocation(GL.programs[program],UTF8ToString(name));var _emscripten_glGetFramebufferAttachmentParameteriv=(target,attachment,pname,params)=>{var result=GLctx.getFramebufferAttachmentParameter(target,attachment,pname);if(result instanceof WebGLRenderbuffer||result instanceof WebGLTexture){result=result.name|0}HEAP32[params>>2]=result};var emscriptenWebGLGetIndexed=(target,index,data,type)=>{if(!data){GL.recordError(1281);return}var result=GLctx.getIndexedParameter(target,index);var ret;switch(typeof result){case"boolean":ret=result?1:0;break;case"number":ret=result;break;case"object":if(result===null){switch(target){case 35983:case 35368:ret=0;break;default:{GL.recordError(1280);return}}}else if(result instanceof WebGLBuffer){ret=result.name|0}else{GL.recordError(1280);return}break;default:GL.recordError(1280);return}switch(type){case 1:writeI53ToI64(data,ret);break;case 0:HEAP32[data>>2]=ret;break;case 2:HEAPF32[data>>2]=ret;break;case 4:HEAP8[data]=ret?1:0;break;default:abort("internal emscriptenWebGLGetIndexed() error, bad type: "+type)}};var _emscripten_glGetInteger64i_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,1);var _emscripten_glGetInteger64v=(name_,p)=>{emscriptenWebGLGet(name_,p,1)};var _emscripten_glGetIntegeri_v=(target,index,data)=>emscriptenWebGLGetIndexed(target,index,data,0);var _emscripten_glGetIntegerv=(name_,p)=>emscriptenWebGLGet(name_,p,0);var _emscripten_glGetInternalformativ=(target,internalformat,pname,bufSize,params)=>{if(bufSize<0){GL.recordError(1281);return}if(!params){GL.recordError(1281);return}var ret=GLctx.getInternalformatParameter(target,internalformat,pname);if(ret===null)return;for(var i=0;i>2]=ret[i]}};var _emscripten_glGetProgramBinary=(program,bufSize,length,binaryFormat,binary)=>{GL.recordError(1282)};var _emscripten_glGetProgramInfoLog=(program,maxLength,length,infoLog)=>{var log=GLctx.getProgramInfoLog(GL.programs[program]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetProgramiv=(program,pname,p)=>{if(!p){GL.recordError(1281);return}if(program>=GL.counter){GL.recordError(1281);return}program=GL.programs[program];if(pname==35716){var log=GLctx.getProgramInfoLog(program);if(log===null)log="(unknown error)";HEAP32[p>>2]=log.length+1}else if(pname==35719){if(!program.maxUniformLength){var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(var i=0;i>2]=program.maxUniformLength}else if(pname==35722){if(!program.maxAttributeLength){var numActiveAttributes=GLctx.getProgramParameter(program,35721);for(var i=0;i>2]=program.maxAttributeLength}else if(pname==35381){if(!program.maxUniformBlockNameLength){var numActiveUniformBlocks=GLctx.getProgramParameter(program,35382);for(var i=0;i>2]=program.maxUniformBlockNameLength}else{HEAP32[p>>2]=GLctx.getProgramParameter(program,pname)}};var _emscripten_glGetQueryObjecti64vEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param;if(GL.currentContext.version<2){param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname)}else{param=GLctx.getQueryParameter(query,pname)}var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}writeI53ToI64(params,ret)};var _emscripten_glGetQueryObjectivEXT=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.disjointTimerQueryExt["getQueryObjectEXT"](query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjecti64vEXT=_emscripten_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectui64vEXT=_glGetQueryObjecti64vEXT;var _emscripten_glGetQueryObjectuiv=(id,pname,params)=>{if(!params){GL.recordError(1281);return}var query=GL.queries[id];var param=GLctx.getQueryParameter(query,pname);var ret;if(typeof param=="boolean"){ret=param?1:0}else{ret=param}HEAP32[params>>2]=ret};var _glGetQueryObjectivEXT=_emscripten_glGetQueryObjectivEXT;var _emscripten_glGetQueryObjectuivEXT=_glGetQueryObjectivEXT;var _emscripten_glGetQueryiv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getQuery(target,pname)};var _emscripten_glGetQueryivEXT=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.disjointTimerQueryExt["getQueryEXT"](target,pname)};var _emscripten_glGetRenderbufferParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getRenderbufferParameter(target,pname)};var _emscripten_glGetSamplerParameterfv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetSamplerParameteriv=(sampler,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getSamplerParameter(GL.samplers[sampler],pname)};var _emscripten_glGetShaderInfoLog=(shader,maxLength,length,infoLog)=>{var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var numBytesWrittenExclNull=maxLength>0&&infoLog?stringToUTF8(log,infoLog,maxLength):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderPrecisionFormat=(shaderType,precisionType,range,precision)=>{var result=GLctx.getShaderPrecisionFormat(shaderType,precisionType);HEAP32[range>>2]=result.rangeMin;HEAP32[range+4>>2]=result.rangeMax;HEAP32[precision>>2]=result.precision};var _emscripten_glGetShaderSource=(shader,bufSize,length,source)=>{var result=GLctx.getShaderSource(GL.shaders[shader]);if(!result)return;var numBytesWrittenExclNull=bufSize>0&&source?stringToUTF8(result,source,bufSize):0;if(length)HEAP32[length>>2]=numBytesWrittenExclNull};var _emscripten_glGetShaderiv=(shader,pname,p)=>{if(!p){GL.recordError(1281);return}if(pname==35716){var log=GLctx.getShaderInfoLog(GL.shaders[shader]);if(log===null)log="(unknown error)";var logLength=log?log.length+1:0;HEAP32[p>>2]=logLength}else if(pname==35720){var source=GLctx.getShaderSource(GL.shaders[shader]);var sourceLength=source?source.length+1:0;HEAP32[p>>2]=sourceLength}else{HEAP32[p>>2]=GLctx.getShaderParameter(GL.shaders[shader],pname)}};var stringToNewUTF8=str=>{var size=lengthBytesUTF8(str)+1;var ret=_malloc(size);if(ret)stringToUTF8(str,ret,size);return ret};var _emscripten_glGetString=name_=>{var ret=GL.stringCache[name_];if(!ret){switch(name_){case 7939:ret=stringToNewUTF8(webglGetExtensions().join(" "));break;case 7936:case 7937:case 37445:case 37446:var s=GLctx.getParameter(name_);if(!s){GL.recordError(1280)}ret=s?stringToNewUTF8(s):0;break;case 7938:var webGLVersion=GLctx.getParameter(7938);var glVersion=`OpenGL ES 2.0 (${webGLVersion})`;if(GL.currentContext.version>=2)glVersion=`OpenGL ES 3.0 (${webGLVersion})`;ret=stringToNewUTF8(glVersion);break;case 35724:var glslVersion=GLctx.getParameter(35724);var ver_re=/^WebGL GLSL ES ([0-9]\.[0-9][0-9]?)(?:$| .*)/;var ver_num=glslVersion.match(ver_re);if(ver_num!==null){if(ver_num[1].length==3)ver_num[1]=ver_num[1]+"0";glslVersion=`OpenGL ES GLSL ES ${ver_num[1]} (${glslVersion})`}ret=stringToNewUTF8(glslVersion);break;default:GL.recordError(1280)}GL.stringCache[name_]=ret}return ret};var _emscripten_glGetStringi=(name,index)=>{if(GL.currentContext.version<2){GL.recordError(1282);return 0}var stringiCache=GL.stringiCache[name];if(stringiCache){if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index]}switch(name){case 7939:var exts=webglGetExtensions().map(stringToNewUTF8);stringiCache=GL.stringiCache[name]=exts;if(index<0||index>=stringiCache.length){GL.recordError(1281);return 0}return stringiCache[index];default:GL.recordError(1280);return 0}};var _emscripten_glGetSynciv=(sync,pname,bufSize,length,values)=>{if(bufSize<0){GL.recordError(1281);return}if(!values){GL.recordError(1281);return}var ret=GLctx.getSyncParameter(GL.syncs[sync],pname);if(ret!==null){HEAP32[values>>2]=ret;if(length)HEAP32[length>>2]=1}};var _emscripten_glGetTexParameterfv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAPF32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTexParameteriv=(target,pname,params)=>{if(!params){GL.recordError(1281);return}HEAP32[params>>2]=GLctx.getTexParameter(target,pname)};var _emscripten_glGetTransformFeedbackVarying=(program,index,bufSize,length,size,type,name)=>{program=GL.programs[program];var info=GLctx.getTransformFeedbackVarying(program,index);if(!info)return;if(name&&bufSize>0){var numBytesWrittenExclNull=stringToUTF8(info.name,name,bufSize);if(length)HEAP32[length>>2]=numBytesWrittenExclNull}else{if(length)HEAP32[length>>2]=0}if(size)HEAP32[size>>2]=info.size;if(type)HEAP32[type>>2]=info.type};var _emscripten_glGetUniformBlockIndex=(program,uniformBlockName)=>GLctx.getUniformBlockIndex(GL.programs[program],UTF8ToString(uniformBlockName));var _emscripten_glGetUniformIndices=(program,uniformCount,uniformNames,uniformIndices)=>{if(!uniformIndices){GL.recordError(1281);return}if(uniformCount>0&&(uniformNames==0||uniformIndices==0)){GL.recordError(1281);return}program=GL.programs[program];var names=[];for(var i=0;i>2]));var result=GLctx.getUniformIndices(program,names);if(!result)return;var len=result.length;for(var i=0;i>2]=result[i]}};var jstoi_q=str=>parseInt(str);var webglGetLeftBracePos=name=>name.slice(-1)=="]"&&name.lastIndexOf("[");var webglPrepareUniformLocationsBeforeFirstUse=program=>{var uniformLocsById=program.uniformLocsById,uniformSizeAndIdsByName=program.uniformSizeAndIdsByName,i,j;if(!uniformLocsById){program.uniformLocsById=uniformLocsById={};program.uniformArrayNamesById={};var numActiveUniforms=GLctx.getProgramParameter(program,35718);for(i=0;i0?nm.slice(0,lb):nm;var id=program.uniformIdCounter;program.uniformIdCounter+=sz;uniformSizeAndIdsByName[arrayName]=[sz,id];for(j=0;j{name=UTF8ToString(name);if(program=GL.programs[program]){webglPrepareUniformLocationsBeforeFirstUse(program);var uniformLocsById=program.uniformLocsById;var arrayIndex=0;var uniformBaseName=name;var leftBrace=webglGetLeftBracePos(name);if(leftBrace>0){arrayIndex=jstoi_q(name.slice(leftBrace+1))>>>0;uniformBaseName=name.slice(0,leftBrace)}var sizeAndId=program.uniformSizeAndIdsByName[uniformBaseName];if(sizeAndId&&arrayIndex{var p=GLctx.currentProgram;if(p){var webglLoc=p.uniformLocsById[location];if(typeof webglLoc=="number"){p.uniformLocsById[location]=webglLoc=GLctx.getUniformLocation(p,p.uniformArrayNamesById[location]+(webglLoc>0?`[${webglLoc}]`:""))}return webglLoc}else{GL.recordError(1282)}};var emscriptenWebGLGetUniform=(program,location,params,type)=>{if(!params){GL.recordError(1281);return}program=GL.programs[program];webglPrepareUniformLocationsBeforeFirstUse(program);var data=GLctx.getUniform(program,webglGetUniformLocation(location));if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break}}}};var _emscripten_glGetUniformfv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,2)};var _emscripten_glGetUniformiv=(program,location,params)=>{emscriptenWebGLGetUniform(program,location,params,0)};var _emscripten_glGetUniformuiv=(program,location,params)=>emscriptenWebGLGetUniform(program,location,params,0);var emscriptenWebGLGetVertexAttrib=(index,pname,params,type)=>{if(!params){GL.recordError(1281);return}var data=GLctx.getVertexAttrib(index,pname);if(pname==34975){HEAP32[params>>2]=data&&data["name"]}else if(typeof data=="number"||typeof data=="boolean"){switch(type){case 0:HEAP32[params>>2]=data;break;case 2:HEAPF32[params>>2]=data;break;case 5:HEAP32[params>>2]=Math.fround(data);break}}else{for(var i=0;i>2]=data[i];break;case 2:HEAPF32[params+i*4>>2]=data[i];break;case 5:HEAP32[params+i*4>>2]=Math.fround(data[i]);break}}}};var _emscripten_glGetVertexAttribIiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,0)};var _glGetVertexAttribIiv=_emscripten_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribIuiv=_glGetVertexAttribIiv;var _emscripten_glGetVertexAttribPointerv=(index,pname,pointer)=>{if(!pointer){GL.recordError(1281);return}HEAP32[pointer>>2]=GLctx.getVertexAttribOffset(index,pname)};var _emscripten_glGetVertexAttribfv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,2)};var _emscripten_glGetVertexAttribiv=(index,pname,params)=>{emscriptenWebGLGetVertexAttrib(index,pname,params,5)};var _emscripten_glHint=(x0,x1)=>GLctx.hint(x0,x1);var _emscripten_glInvalidateFramebuffer=(target,numAttachments,attachments)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateFramebuffer(target,list)};var _emscripten_glInvalidateSubFramebuffer=(target,numAttachments,attachments,x,y,width,height)=>{var list=tempFixedLengthArray[numAttachments];for(var i=0;i>2]}GLctx.invalidateSubFramebuffer(target,list,x,y,width,height)};var _emscripten_glIsBuffer=buffer=>{var b=GL.buffers[buffer];if(!b)return 0;return GLctx.isBuffer(b)};var _emscripten_glIsEnabled=x0=>GLctx.isEnabled(x0);var _emscripten_glIsFramebuffer=framebuffer=>{var fb=GL.framebuffers[framebuffer];if(!fb)return 0;return GLctx.isFramebuffer(fb)};var _emscripten_glIsProgram=program=>{program=GL.programs[program];if(!program)return 0;return GLctx.isProgram(program)};var _emscripten_glIsQuery=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.isQuery(query)};var _emscripten_glIsQueryEXT=id=>{var query=GL.queries[id];if(!query)return 0;return GLctx.disjointTimerQueryExt["isQueryEXT"](query)};var _emscripten_glIsRenderbuffer=renderbuffer=>{var rb=GL.renderbuffers[renderbuffer];if(!rb)return 0;return GLctx.isRenderbuffer(rb)};var _emscripten_glIsSampler=id=>{var sampler=GL.samplers[id];if(!sampler)return 0;return GLctx.isSampler(sampler)};var _emscripten_glIsShader=shader=>{var s=GL.shaders[shader];if(!s)return 0;return GLctx.isShader(s)};var _emscripten_glIsSync=sync=>GLctx.isSync(GL.syncs[sync]);var _emscripten_glIsTexture=id=>{var texture=GL.textures[id];if(!texture)return 0;return GLctx.isTexture(texture)};var _emscripten_glIsTransformFeedback=id=>GLctx.isTransformFeedback(GL.transformFeedbacks[id]);var _emscripten_glIsVertexArray=array=>{var vao=GL.vaos[array];if(!vao)return 0;return GLctx.isVertexArray(vao)};var _glIsVertexArray=_emscripten_glIsVertexArray;var _emscripten_glIsVertexArrayOES=_glIsVertexArray;var _emscripten_glLineWidth=x0=>GLctx.lineWidth(x0);var _emscripten_glLinkProgram=program=>{program=GL.programs[program];GLctx.linkProgram(program);program.uniformLocsById=0;program.uniformSizeAndIdsByName={}};var _emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL=(mode,firsts,counts,instanceCounts,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawArraysInstancedBaseInstanceWEBGL"](mode,HEAP32,firsts>>2,HEAP32,counts>>2,HEAP32,instanceCounts>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL=(mode,counts,type,offsets,instanceCounts,baseVertices,baseInstances,drawCount)=>{GLctx.mdibvbi["multiDrawElementsInstancedBaseVertexBaseInstanceWEBGL"](mode,HEAP32,counts>>2,type,HEAP32,offsets>>2,HEAP32,instanceCounts>>2,HEAP32,baseVertices>>2,HEAPU32,baseInstances>>2,drawCount)};var _emscripten_glPauseTransformFeedback=()=>GLctx.pauseTransformFeedback();var _emscripten_glPixelStorei=(pname,param)=>{if(pname==3317){GL.unpackAlignment=param}else if(pname==3314){GL.unpackRowLength=param}GLctx.pixelStorei(pname,param)};var _emscripten_glPolygonModeWEBGL=(face,mode)=>{GLctx.webglPolygonMode["polygonModeWEBGL"](face,mode)};var _emscripten_glPolygonOffset=(x0,x1)=>GLctx.polygonOffset(x0,x1);var _emscripten_glPolygonOffsetClampEXT=(factor,units,clamp)=>{GLctx.extPolygonOffsetClamp["polygonOffsetClampEXT"](factor,units,clamp)};var _emscripten_glProgramBinary=(program,binaryFormat,binary,length)=>{GL.recordError(1280)};var _emscripten_glProgramParameteri=(program,pname,value)=>{GL.recordError(1280)};var _emscripten_glQueryCounterEXT=(id,target)=>{GLctx.disjointTimerQueryExt["queryCounterEXT"](GL.queries[id],target)};var _emscripten_glReadBuffer=x0=>GLctx.readBuffer(x0);var computeUnpackAlignedImageSize=(width,height,sizePerPixel)=>{function roundedToNextMultipleOf(x,y){return x+y-1&-y}var plainRowSize=(GL.unpackRowLength||width)*sizePerPixel;var alignedRowSize=roundedToNextMultipleOf(plainRowSize,GL.unpackAlignment);return height*alignedRowSize};var colorChannelsInGlTextureFormat=format=>{var colorChannels={5:3,6:4,8:2,29502:3,29504:4,26917:2,26918:2,29846:3,29847:4};return colorChannels[format-6402]||1};var heapObjectForWebGLType=type=>{type-=5120;if(type==0)return HEAP8;if(type==1)return HEAPU8;if(type==2)return HEAP16;if(type==4)return HEAP32;if(type==6)return HEAPF32;if(type==5||type==28922||type==28520||type==30779||type==30782)return HEAPU32;return HEAPU16};var toTypedArrayIndex=(pointer,heap)=>pointer>>>31-Math.clz32(heap.BYTES_PER_ELEMENT);var emscriptenWebGLGetTexPixelData=(type,format,width,height,pixels,internalFormat)=>{var heap=heapObjectForWebGLType(type);var sizePerPixel=colorChannelsInGlTextureFormat(format)*heap.BYTES_PER_ELEMENT;var bytes=computeUnpackAlignedImageSize(width,height,sizePerPixel);return heap.subarray(toTypedArrayIndex(pixels,heap),toTypedArrayIndex(pixels+bytes,heap))};var _emscripten_glReadPixels=(x,y,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelPackBufferBinding){GLctx.readPixels(x,y,width,height,format,type,pixels);return}var heap=heapObjectForWebGLType(type);var target=toTypedArrayIndex(pixels,heap);GLctx.readPixels(x,y,width,height,format,type,heap,target);return}var pixelData=emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,format);if(!pixelData){GL.recordError(1280);return}GLctx.readPixels(x,y,width,height,format,type,pixelData)};var _emscripten_glReleaseShaderCompiler=()=>{};var _emscripten_glRenderbufferStorage=(x0,x1,x2,x3)=>GLctx.renderbufferStorage(x0,x1,x2,x3);var _emscripten_glRenderbufferStorageMultisample=(x0,x1,x2,x3,x4)=>GLctx.renderbufferStorageMultisample(x0,x1,x2,x3,x4);var _emscripten_glResumeTransformFeedback=()=>GLctx.resumeTransformFeedback();var _emscripten_glSampleCoverage=(value,invert)=>{GLctx.sampleCoverage(value,!!invert)};var _emscripten_glSamplerParameterf=(sampler,pname,param)=>{GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameterfv=(sampler,pname,params)=>{var param=HEAPF32[params>>2];GLctx.samplerParameterf(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteri=(sampler,pname,param)=>{GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glSamplerParameteriv=(sampler,pname,params)=>{var param=HEAP32[params>>2];GLctx.samplerParameteri(GL.samplers[sampler],pname,param)};var _emscripten_glScissor=(x0,x1,x2,x3)=>GLctx.scissor(x0,x1,x2,x3);var _emscripten_glShaderBinary=(count,shaders,binaryformat,binary,length)=>{GL.recordError(1280)};var _emscripten_glShaderSource=(shader,count,string,length)=>{var source=GL.getSource(shader,count,string,length);GLctx.shaderSource(GL.shaders[shader],source)};var _emscripten_glStencilFunc=(x0,x1,x2)=>GLctx.stencilFunc(x0,x1,x2);var _emscripten_glStencilFuncSeparate=(x0,x1,x2,x3)=>GLctx.stencilFuncSeparate(x0,x1,x2,x3);var _emscripten_glStencilMask=x0=>GLctx.stencilMask(x0);var _emscripten_glStencilMaskSeparate=(x0,x1)=>GLctx.stencilMaskSeparate(x0,x1);var _emscripten_glStencilOp=(x0,x1,x2)=>GLctx.stencilOp(x0,x1,x2);var _emscripten_glStencilOpSeparate=(x0,x1,x2,x3)=>GLctx.stencilOpSeparate(x0,x1,x2,x3);var _emscripten_glTexImage2D=(target,level,internalFormat,width,height,border,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);var index=toTypedArrayIndex(pixels,heap);GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,heap,index);return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,internalFormat):null;GLctx.texImage2D(target,level,internalFormat,width,height,border,format,type,pixelData)};var _emscripten_glTexImage3D=(target,level,internalFormat,width,height,depth,border,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texImage3D(target,level,internalFormat,width,height,depth,border,format,type,null)}};var _emscripten_glTexParameterf=(x0,x1,x2)=>GLctx.texParameterf(x0,x1,x2);var _emscripten_glTexParameterfv=(target,pname,params)=>{var param=HEAPF32[params>>2];GLctx.texParameterf(target,pname,param)};var _emscripten_glTexParameteri=(x0,x1,x2)=>GLctx.texParameteri(x0,x1,x2);var _emscripten_glTexParameteriv=(target,pname,params)=>{var param=HEAP32[params>>2];GLctx.texParameteri(target,pname,param)};var _emscripten_glTexStorage2D=(x0,x1,x2,x3,x4)=>GLctx.texStorage2D(x0,x1,x2,x3,x4);var _emscripten_glTexStorage3D=(x0,x1,x2,x3,x4,x5)=>GLctx.texStorage3D(x0,x1,x2,x3,x4,x5);var _emscripten_glTexSubImage2D=(target,level,xoffset,yoffset,width,height,format,type,pixels)=>{if(GL.currentContext.version>=2){if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixels);return}if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,heap,toTypedArrayIndex(pixels,heap));return}}var pixelData=pixels?emscriptenWebGLGetTexPixelData(type,format,width,height,pixels,0):null;GLctx.texSubImage2D(target,level,xoffset,yoffset,width,height,format,type,pixelData)};var _emscripten_glTexSubImage3D=(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)=>{if(GLctx.currentPixelUnpackBufferBinding){GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,pixels)}else if(pixels){var heap=heapObjectForWebGLType(type);GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,heap,toTypedArrayIndex(pixels,heap))}else{GLctx.texSubImage3D(target,level,xoffset,yoffset,zoffset,width,height,depth,format,type,null)}};var _emscripten_glTransformFeedbackVaryings=(program,count,varyings,bufferMode)=>{program=GL.programs[program];var vars=[];for(var i=0;i>2]));GLctx.transformFeedbackVaryings(program,vars,bufferMode)};var _emscripten_glUniform1f=(location,v0)=>{GLctx.uniform1f(webglGetUniformLocation(location),v0)};var miniTempWebGLFloatBuffers=[];var _emscripten_glUniform1fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1fv(webglGetUniformLocation(location),HEAPF32,value>>2,count);return}if(count<=288){var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1i=(location,v0)=>{GLctx.uniform1i(webglGetUniformLocation(location),v0)};var miniTempWebGLIntBuffers=[];var _emscripten_glUniform1iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform1iv(webglGetUniformLocation(location),HEAP32,value>>2,count);return}if(count<=288){var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2]}}else{var view=HEAP32.subarray(value>>2,value+count*4>>2)}GLctx.uniform1iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform1ui=(location,v0)=>{GLctx.uniform1ui(webglGetUniformLocation(location),v0)};var _emscripten_glUniform1uiv=(location,count,value)=>{count&&GLctx.uniform1uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count)};var _emscripten_glUniform2f=(location,v0,v1)=>{GLctx.uniform2f(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2i=(location,v0,v1)=>{GLctx.uniform2i(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform2iv(webglGetUniformLocation(location),HEAP32,value>>2,count*2);return}if(count<=144){count*=2;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*8>>2)}GLctx.uniform2iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform2ui=(location,v0,v1)=>{GLctx.uniform2ui(webglGetUniformLocation(location),v0,v1)};var _emscripten_glUniform2uiv=(location,count,value)=>{count&&GLctx.uniform2uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*2)};var _emscripten_glUniform3f=(location,v0,v1,v2)=>{GLctx.uniform3f(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3i=(location,v0,v1,v2)=>{GLctx.uniform3i(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform3iv(webglGetUniformLocation(location),HEAP32,value>>2,count*3);return}if(count<=96){count*=3;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*12>>2)}GLctx.uniform3iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform3ui=(location,v0,v1,v2)=>{GLctx.uniform3ui(webglGetUniformLocation(location),v0,v1,v2)};var _emscripten_glUniform3uiv=(location,count,value)=>{count&&GLctx.uniform3uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*3)};var _emscripten_glUniform4f=(location,v0,v1,v2,v3)=>{GLctx.uniform4f(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4fv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4fv(webglGetUniformLocation(location),HEAPF32,value>>2,count*4);return}if(count<=72){var view=miniTempWebGLFloatBuffers[4*count];var heap=HEAPF32;value=value>>2;count*=4;for(var i=0;i>2,value+count*16>>2)}GLctx.uniform4fv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4i=(location,v0,v1,v2,v3)=>{GLctx.uniform4i(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4iv=(location,count,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniform4iv(webglGetUniformLocation(location),HEAP32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLIntBuffers[count];for(var i=0;i>2];view[i+1]=HEAP32[value+(4*i+4)>>2];view[i+2]=HEAP32[value+(4*i+8)>>2];view[i+3]=HEAP32[value+(4*i+12)>>2]}}else{var view=HEAP32.subarray(value>>2,value+count*16>>2)}GLctx.uniform4iv(webglGetUniformLocation(location),view)};var _emscripten_glUniform4ui=(location,v0,v1,v2,v3)=>{GLctx.uniform4ui(webglGetUniformLocation(location),v0,v1,v2,v3)};var _emscripten_glUniform4uiv=(location,count,value)=>{count&&GLctx.uniform4uiv(webglGetUniformLocation(location),HEAPU32,value>>2,count*4)};var _emscripten_glUniformBlockBinding=(program,uniformBlockIndex,uniformBlockBinding)=>{program=GL.programs[program];GLctx.uniformBlockBinding(program,uniformBlockIndex,uniformBlockBinding)};var _emscripten_glUniformMatrix2fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*4);return}if(count<=72){count*=4;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*16>>2)}GLctx.uniformMatrix2fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix2x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix2x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix2x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix3fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*9);return}if(count<=32){count*=9;var view=miniTempWebGLFloatBuffers[count];for(var i=0;i>2];view[i+1]=HEAPF32[value+(4*i+4)>>2];view[i+2]=HEAPF32[value+(4*i+8)>>2];view[i+3]=HEAPF32[value+(4*i+12)>>2];view[i+4]=HEAPF32[value+(4*i+16)>>2];view[i+5]=HEAPF32[value+(4*i+20)>>2];view[i+6]=HEAPF32[value+(4*i+24)>>2];view[i+7]=HEAPF32[value+(4*i+28)>>2];view[i+8]=HEAPF32[value+(4*i+32)>>2]}}else{var view=HEAPF32.subarray(value>>2,value+count*36>>2)}GLctx.uniformMatrix3fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix3x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*6)};var _emscripten_glUniformMatrix3x4fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix3x4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUniformMatrix4fv=(location,count,transpose,value)=>{if(GL.currentContext.version>=2){count&&GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*16);return}if(count<=18){var view=miniTempWebGLFloatBuffers[16*count];var heap=HEAPF32;value=value>>2;count*=16;for(var i=0;i>2,value+count*64>>2)}GLctx.uniformMatrix4fv(webglGetUniformLocation(location),!!transpose,view)};var _emscripten_glUniformMatrix4x2fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x2fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*8)};var _emscripten_glUniformMatrix4x3fv=(location,count,transpose,value)=>{count&&GLctx.uniformMatrix4x3fv(webglGetUniformLocation(location),!!transpose,HEAPF32,value>>2,count*12)};var _emscripten_glUseProgram=program=>{program=GL.programs[program];GLctx.useProgram(program);GLctx.currentProgram=program};var _emscripten_glValidateProgram=program=>{GLctx.validateProgram(GL.programs[program])};var _emscripten_glVertexAttrib1f=(x0,x1)=>GLctx.vertexAttrib1f(x0,x1);var _emscripten_glVertexAttrib1fv=(index,v)=>{GLctx.vertexAttrib1f(index,HEAPF32[v>>2])};var _emscripten_glVertexAttrib2f=(x0,x1,x2)=>GLctx.vertexAttrib2f(x0,x1,x2);var _emscripten_glVertexAttrib2fv=(index,v)=>{GLctx.vertexAttrib2f(index,HEAPF32[v>>2],HEAPF32[v+4>>2])};var _emscripten_glVertexAttrib3f=(x0,x1,x2,x3)=>GLctx.vertexAttrib3f(x0,x1,x2,x3);var _emscripten_glVertexAttrib3fv=(index,v)=>{GLctx.vertexAttrib3f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2])};var _emscripten_glVertexAttrib4f=(x0,x1,x2,x3,x4)=>GLctx.vertexAttrib4f(x0,x1,x2,x3,x4);var _emscripten_glVertexAttrib4fv=(index,v)=>{GLctx.vertexAttrib4f(index,HEAPF32[v>>2],HEAPF32[v+4>>2],HEAPF32[v+8>>2],HEAPF32[v+12>>2])};var _emscripten_glVertexAttribDivisor=(index,divisor)=>{GLctx.vertexAttribDivisor(index,divisor)};var _glVertexAttribDivisor=_emscripten_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorANGLE=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorARB=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorEXT=_glVertexAttribDivisor;var _emscripten_glVertexAttribDivisorNV=_glVertexAttribDivisor;var _emscripten_glVertexAttribI4i=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4i(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4iv=(index,v)=>{GLctx.vertexAttribI4i(index,HEAP32[v>>2],HEAP32[v+4>>2],HEAP32[v+8>>2],HEAP32[v+12>>2])};var _emscripten_glVertexAttribI4ui=(x0,x1,x2,x3,x4)=>GLctx.vertexAttribI4ui(x0,x1,x2,x3,x4);var _emscripten_glVertexAttribI4uiv=(index,v)=>{GLctx.vertexAttribI4ui(index,HEAPU32[v>>2],HEAPU32[v+4>>2],HEAPU32[v+8>>2],HEAPU32[v+12>>2])};var _emscripten_glVertexAttribIPointer=(index,size,type,stride,ptr)=>{GLctx.vertexAttribIPointer(index,size,type,stride,ptr)};var _emscripten_glVertexAttribPointer=(index,size,type,normalized,stride,ptr)=>{GLctx.vertexAttribPointer(index,size,type,!!normalized,stride,ptr)};var _emscripten_glViewport=(x0,x1,x2,x3)=>GLctx.viewport(x0,x1,x2,x3);var _emscripten_glWaitSync=(sync,flags,timeout)=>{timeout=Number(timeout);GLctx.waitSync(GL.syncs[sync],flags,timeout)};var wasmTableMirror=[];var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}return func};var _emscripten_request_animation_frame_loop=(cb,userData)=>{function tick(timeStamp){if(getWasmTableEntry(cb)(timeStamp,userData)){requestAnimationFrame(tick)}}return requestAnimationFrame(tick)};var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(globalThis.navigator?.language??"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var _environ_get=(__environ,environ_buf)=>{var bufSize=0;var envp=0;for(var string of getEnvStrings()){var ptr=environ_buf+bufSize;HEAPU32[__environ+envp>>2]=ptr;bufSize+=stringToUTF8(string,ptr,Infinity)+1;envp+=4}return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;var bufSize=0;for(var string of strings){bufSize+=lengthBytesUTF8(string)+1}HEAPU32[penviron_buf_size>>2]=bufSize;return 0};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_read(fd,iov,iovcnt,pnum){try{var stream=SYSCALLS.getStreamFromFD(fd);var num=doReadv(stream,iov,iovcnt);HEAPU32[pnum>>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);HEAP64[newOffset>>3]=BigInt(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _glGetIntegerv=_emscripten_glGetIntegerv;var _glGetString=_emscripten_glGetString;var _glGetStringi=_emscripten_glGetStringi;var _llvm_eh_typeid_for=type=>type;function _random_get(buffer,size){try{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};FS.createPreloadedFile=FS_createPreloadedFile;FS.preloadFile=FS_preloadFile;FS.staticInit();for(let i=0;i<32;++i)tempFixedLengthArray.push(new Array(i));var miniTempWebGLFloatBuffersStorage=new Float32Array(288);for(var i=0;i<=288;++i){miniTempWebGLFloatBuffers[i]=miniTempWebGLFloatBuffersStorage.subarray(0,i)}var miniTempWebGLIntBuffersStorage=new Int32Array(288);for(var i=0;i<=288;++i){miniTempWebGLIntBuffers[i]=miniTempWebGLIntBuffersStorage.subarray(0,i)}{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["preloadPlugins"])preloadPlugins=Module["preloadPlugins"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["UTF8ToString"]=UTF8ToString;Module["stringToUTF8"]=stringToUTF8;Module["lengthBytesUTF8"]=lengthBytesUTF8;Module["GL"]=GL;var _malloc,_add_font,_add_image,_add_image_with_rid,_allocate,_apply_scene_transactions,_command,_deallocate,_destroy,_devtools_rendering_set_show_fps_meter,_devtools_rendering_set_show_hit_testing,_devtools_rendering_set_show_ruler,_devtools_rendering_set_show_stats,_devtools_rendering_set_show_tiles,_drain_missing_images,_export_node_as,_get_default_fallback_fonts,_get_image_bytes,_get_image_size,_get_node_absolute_bounding_box,_get_node_id_from_point,_get_node_ids_from_envelope,_get_node_ids_from_point,_grida_fonts_analyze_family,_grida_fonts_free,_grida_fonts_parse_font,_grida_markdown_to_html,_grida_svg_optimize,_grida_svg_to_document,_has_missing_fonts,_highlight_strokes,_init,_init_with_backend,_list_available_fonts,_list_missing_fonts,_load_benchmark_scene,_load_dummy_scene,_load_scene_grida,_load_scene_grida1,_pointer_move,_redraw,_resize_surface,_resolve_image,_runtime_renderer_set_layer_compositing,_runtime_renderer_set_outline_mode,_runtime_renderer_set_pixel_preview_scale,_runtime_renderer_set_pixel_preview_stable,_runtime_renderer_set_render_policy_flags,_runtime_renderer_set_skip_layout,_set_debug,_set_default_fallback_fonts,_set_main_camera_transform,_set_surface_overlay_config,_set_verbose,_surface_get_cursor,_surface_get_hovered_node,_surface_get_selected_nodes,_surface_pointer_down,_surface_pointer_move,_surface_pointer_up,_surface_set_selection,_switch_scene,_text_edit_command,_text_edit_enter,_text_edit_exit,_text_edit_get_caret_rect,_text_edit_get_selected_html,_text_edit_get_selected_text,_text_edit_get_selection_rects,_text_edit_get_text,_text_edit_ime_cancel,_text_edit_ime_commit,_text_edit_ime_set_preedit,_text_edit_is_active,_text_edit_paste_html,_text_edit_paste_text,_text_edit_pointer_down,_text_edit_pointer_move,_text_edit_pointer_up,_text_edit_redo,_text_edit_set_color,_text_edit_set_font_family,_text_edit_set_font_size,_text_edit_tick,_text_edit_toggle_bold,_text_edit_toggle_italic,_text_edit_toggle_strikethrough,_text_edit_toggle_underline,_text_edit_undo,_tick,_to_vector_network,_toggle_debug,_main,_emscripten_builtin_memalign,_setThrew,__emscripten_tempret_set,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,___cxa_decrement_exception_refcount,___cxa_increment_exception_refcount,___cxa_can_catch,___cxa_get_exception_ptr,memory,__indirect_function_table,wasmMemory,wasmTable;function assignWasmExports(wasmExports){_malloc=wasmExports["Og"];_add_font=Module["_add_font"]=wasmExports["Qg"];_add_image=Module["_add_image"]=wasmExports["Rg"];_add_image_with_rid=Module["_add_image_with_rid"]=wasmExports["Sg"];_allocate=Module["_allocate"]=wasmExports["Tg"];_apply_scene_transactions=Module["_apply_scene_transactions"]=wasmExports["Ug"];_command=Module["_command"]=wasmExports["Vg"];_deallocate=Module["_deallocate"]=wasmExports["Wg"];_destroy=Module["_destroy"]=wasmExports["Xg"];_devtools_rendering_set_show_fps_meter=Module["_devtools_rendering_set_show_fps_meter"]=wasmExports["Yg"];_devtools_rendering_set_show_hit_testing=Module["_devtools_rendering_set_show_hit_testing"]=wasmExports["Zg"];_devtools_rendering_set_show_ruler=Module["_devtools_rendering_set_show_ruler"]=wasmExports["_g"];_devtools_rendering_set_show_stats=Module["_devtools_rendering_set_show_stats"]=wasmExports["$g"];_devtools_rendering_set_show_tiles=Module["_devtools_rendering_set_show_tiles"]=wasmExports["ah"];_drain_missing_images=Module["_drain_missing_images"]=wasmExports["bh"];_export_node_as=Module["_export_node_as"]=wasmExports["ch"];_get_default_fallback_fonts=Module["_get_default_fallback_fonts"]=wasmExports["dh"];_get_image_bytes=Module["_get_image_bytes"]=wasmExports["eh"];_get_image_size=Module["_get_image_size"]=wasmExports["fh"];_get_node_absolute_bounding_box=Module["_get_node_absolute_bounding_box"]=wasmExports["gh"];_get_node_id_from_point=Module["_get_node_id_from_point"]=wasmExports["hh"];_get_node_ids_from_envelope=Module["_get_node_ids_from_envelope"]=wasmExports["ih"];_get_node_ids_from_point=Module["_get_node_ids_from_point"]=wasmExports["jh"];_grida_fonts_analyze_family=Module["_grida_fonts_analyze_family"]=wasmExports["kh"];_grida_fonts_free=Module["_grida_fonts_free"]=wasmExports["lh"];_grida_fonts_parse_font=Module["_grida_fonts_parse_font"]=wasmExports["mh"];_grida_markdown_to_html=Module["_grida_markdown_to_html"]=wasmExports["nh"];_grida_svg_optimize=Module["_grida_svg_optimize"]=wasmExports["oh"];_grida_svg_to_document=Module["_grida_svg_to_document"]=wasmExports["ph"];_has_missing_fonts=Module["_has_missing_fonts"]=wasmExports["qh"];_highlight_strokes=Module["_highlight_strokes"]=wasmExports["rh"];_init=Module["_init"]=wasmExports["sh"];_init_with_backend=Module["_init_with_backend"]=wasmExports["th"];_list_available_fonts=Module["_list_available_fonts"]=wasmExports["uh"];_list_missing_fonts=Module["_list_missing_fonts"]=wasmExports["vh"];_load_benchmark_scene=Module["_load_benchmark_scene"]=wasmExports["wh"];_load_dummy_scene=Module["_load_dummy_scene"]=wasmExports["xh"];_load_scene_grida=Module["_load_scene_grida"]=wasmExports["yh"];_load_scene_grida1=Module["_load_scene_grida1"]=wasmExports["zh"];_pointer_move=Module["_pointer_move"]=wasmExports["Ah"];_redraw=Module["_redraw"]=wasmExports["Bh"];_resize_surface=Module["_resize_surface"]=wasmExports["Ch"];_resolve_image=Module["_resolve_image"]=wasmExports["Dh"];_runtime_renderer_set_layer_compositing=Module["_runtime_renderer_set_layer_compositing"]=wasmExports["Eh"];_runtime_renderer_set_outline_mode=Module["_runtime_renderer_set_outline_mode"]=wasmExports["Fh"];_runtime_renderer_set_pixel_preview_scale=Module["_runtime_renderer_set_pixel_preview_scale"]=wasmExports["Gh"];_runtime_renderer_set_pixel_preview_stable=Module["_runtime_renderer_set_pixel_preview_stable"]=wasmExports["Hh"];_runtime_renderer_set_render_policy_flags=Module["_runtime_renderer_set_render_policy_flags"]=wasmExports["Ih"];_runtime_renderer_set_skip_layout=Module["_runtime_renderer_set_skip_layout"]=wasmExports["Jh"];_set_debug=Module["_set_debug"]=wasmExports["Kh"];_set_default_fallback_fonts=Module["_set_default_fallback_fonts"]=wasmExports["Lh"];_set_main_camera_transform=Module["_set_main_camera_transform"]=wasmExports["Mh"];_set_surface_overlay_config=Module["_set_surface_overlay_config"]=wasmExports["Nh"];_set_verbose=Module["_set_verbose"]=wasmExports["Oh"];_surface_get_cursor=Module["_surface_get_cursor"]=wasmExports["Ph"];_surface_get_hovered_node=Module["_surface_get_hovered_node"]=wasmExports["Qh"];_surface_get_selected_nodes=Module["_surface_get_selected_nodes"]=wasmExports["Rh"];_surface_pointer_down=Module["_surface_pointer_down"]=wasmExports["Sh"];_surface_pointer_move=Module["_surface_pointer_move"]=wasmExports["Th"];_surface_pointer_up=Module["_surface_pointer_up"]=wasmExports["Uh"];_surface_set_selection=Module["_surface_set_selection"]=wasmExports["Vh"];_switch_scene=Module["_switch_scene"]=wasmExports["Wh"];_text_edit_command=Module["_text_edit_command"]=wasmExports["Xh"];_text_edit_enter=Module["_text_edit_enter"]=wasmExports["Yh"];_text_edit_exit=Module["_text_edit_exit"]=wasmExports["Zh"];_text_edit_get_caret_rect=Module["_text_edit_get_caret_rect"]=wasmExports["_h"];_text_edit_get_selected_html=Module["_text_edit_get_selected_html"]=wasmExports["$h"];_text_edit_get_selected_text=Module["_text_edit_get_selected_text"]=wasmExports["ai"];_text_edit_get_selection_rects=Module["_text_edit_get_selection_rects"]=wasmExports["bi"];_text_edit_get_text=Module["_text_edit_get_text"]=wasmExports["ci"];_text_edit_ime_cancel=Module["_text_edit_ime_cancel"]=wasmExports["di"];_text_edit_ime_commit=Module["_text_edit_ime_commit"]=wasmExports["ei"];_text_edit_ime_set_preedit=Module["_text_edit_ime_set_preedit"]=wasmExports["fi"];_text_edit_is_active=Module["_text_edit_is_active"]=wasmExports["gi"];_text_edit_paste_html=Module["_text_edit_paste_html"]=wasmExports["hi"];_text_edit_paste_text=Module["_text_edit_paste_text"]=wasmExports["ii"];_text_edit_pointer_down=Module["_text_edit_pointer_down"]=wasmExports["ji"];_text_edit_pointer_move=Module["_text_edit_pointer_move"]=wasmExports["ki"];_text_edit_pointer_up=Module["_text_edit_pointer_up"]=wasmExports["li"];_text_edit_redo=Module["_text_edit_redo"]=wasmExports["mi"];_text_edit_set_color=Module["_text_edit_set_color"]=wasmExports["ni"];_text_edit_set_font_family=Module["_text_edit_set_font_family"]=wasmExports["oi"];_text_edit_set_font_size=Module["_text_edit_set_font_size"]=wasmExports["pi"];_text_edit_tick=Module["_text_edit_tick"]=wasmExports["qi"];_text_edit_toggle_bold=Module["_text_edit_toggle_bold"]=wasmExports["ri"];_text_edit_toggle_italic=Module["_text_edit_toggle_italic"]=wasmExports["si"];_text_edit_toggle_strikethrough=Module["_text_edit_toggle_strikethrough"]=wasmExports["ti"];_text_edit_toggle_underline=Module["_text_edit_toggle_underline"]=wasmExports["ui"];_text_edit_undo=Module["_text_edit_undo"]=wasmExports["vi"];_tick=Module["_tick"]=wasmExports["wi"];_to_vector_network=Module["_to_vector_network"]=wasmExports["xi"];_toggle_debug=Module["_toggle_debug"]=wasmExports["yi"];_main=Module["_main"]=wasmExports["zi"];_emscripten_builtin_memalign=wasmExports["Ai"];_setThrew=wasmExports["Bi"];__emscripten_tempret_set=wasmExports["Ci"];__emscripten_stack_restore=wasmExports["Di"];__emscripten_stack_alloc=wasmExports["Ei"];_emscripten_stack_get_current=wasmExports["Fi"];___cxa_decrement_exception_refcount=wasmExports["Gi"];___cxa_increment_exception_refcount=wasmExports["Hi"];___cxa_can_catch=wasmExports["Ii"];___cxa_get_exception_ptr=wasmExports["Ji"];memory=wasmMemory=wasmExports["Mg"];__indirect_function_table=wasmTable=wasmExports["Pg"]}var wasmImports={F:___cxa_begin_catch,N:___cxa_end_catch,a:___cxa_find_matching_catch_2,o:___cxa_find_matching_catch_3,ga:___cxa_find_matching_catch_4,Ca:___cxa_rethrow,H:___cxa_throw,eb:___cxa_uncaught_exceptions,e:___resumeException,Fa:___syscall_fcntl64,vb:___syscall_fstat64,rb:___syscall_getcwd,wb:___syscall_ioctl,sb:___syscall_lstat64,tb:___syscall_newfstatat,Ga:___syscall_openat,ub:___syscall_stat64,zb:__abort_js,gb:__emscripten_throw_longjmp,mb:__gmtime_js,kb:__mmap_js,lb:__munmap_js,Ab:__tzset_js,yb:_clock_time_get,xb:_emscripten_date_now,ib:_emscripten_get_heap_max,Af:_emscripten_glActiveTexture,Bf:_emscripten_glAttachShader,de:_emscripten_glBeginQuery,Zd:_emscripten_glBeginQueryEXT,Ec:_emscripten_glBeginTransformFeedback,Cf:_emscripten_glBindAttribLocation,Df:_emscripten_glBindBuffer,Bc:_emscripten_glBindBufferBase,Cc:_emscripten_glBindBufferRange,Be:_emscripten_glBindFramebuffer,Ce:_emscripten_glBindRenderbuffer,je:_emscripten_glBindSampler,Ef:_emscripten_glBindTexture,Rb:_emscripten_glBindTransformFeedback,Xe:_emscripten_glBindVertexArray,_e:_emscripten_glBindVertexArrayOES,Ff:_emscripten_glBlendColor,Gf:_emscripten_glBlendEquation,Jd:_emscripten_glBlendEquationSeparate,Hf:_emscripten_glBlendFunc,Id:_emscripten_glBlendFuncSeparate,ve:_emscripten_glBlitFramebuffer,If:_emscripten_glBufferData,Jf:_emscripten_glBufferSubData,De:_emscripten_glCheckFramebufferStatus,Kf:_emscripten_glClear,fc:_emscripten_glClearBufferfi,gc:_emscripten_glClearBufferfv,ic:_emscripten_glClearBufferiv,hc:_emscripten_glClearBufferuiv,Lf:_emscripten_glClearColor,Hd:_emscripten_glClearDepthf,Mf:_emscripten_glClearStencil,se:_emscripten_glClientWaitSync,_c:_emscripten_glClipControlEXT,Nf:_emscripten_glColorMask,Of:_emscripten_glCompileShader,Qf:_emscripten_glCompressedTexImage2D,Rc:_emscripten_glCompressedTexImage3D,Rf:_emscripten_glCompressedTexSubImage2D,Qc:_emscripten_glCompressedTexSubImage3D,ue:_emscripten_glCopyBufferSubData,Gd:_emscripten_glCopyTexImage2D,Sf:_emscripten_glCopyTexSubImage2D,Sc:_emscripten_glCopyTexSubImage3D,Tf:_emscripten_glCreateProgram,Uf:_emscripten_glCreateShader,Vf:_emscripten_glCullFace,Wf:_emscripten_glDeleteBuffers,Ee:_emscripten_glDeleteFramebuffers,Xf:_emscripten_glDeleteProgram,ee:_emscripten_glDeleteQueries,_d:_emscripten_glDeleteQueriesEXT,Fe:_emscripten_glDeleteRenderbuffers,ke:_emscripten_glDeleteSamplers,Yf:_emscripten_glDeleteShader,te:_emscripten_glDeleteSync,Zf:_emscripten_glDeleteTextures,Qb:_emscripten_glDeleteTransformFeedbacks,Ye:_emscripten_glDeleteVertexArrays,$e:_emscripten_glDeleteVertexArraysOES,Fd:_emscripten_glDepthFunc,_f:_emscripten_glDepthMask,Ed:_emscripten_glDepthRangef,Dd:_emscripten_glDetachShader,$f:_emscripten_glDisable,ag:_emscripten_glDisableVertexAttribArray,bg:_emscripten_glDrawArrays,Ve:_emscripten_glDrawArraysInstanced,Md:_emscripten_glDrawArraysInstancedANGLE,Db:_emscripten_glDrawArraysInstancedARB,Se:_emscripten_glDrawArraysInstancedBaseInstanceWEBGL,Xc:_emscripten_glDrawArraysInstancedEXT,Eb:_emscripten_glDrawArraysInstancedNV,Qe:_emscripten_glDrawBuffers,Vc:_emscripten_glDrawBuffersEXT,Nd:_emscripten_glDrawBuffersWEBGL,cg:_emscripten_glDrawElements,We:_emscripten_glDrawElementsInstanced,Ld:_emscripten_glDrawElementsInstancedANGLE,Bb:_emscripten_glDrawElementsInstancedARB,Te:_emscripten_glDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Cb:_emscripten_glDrawElementsInstancedEXT,Wc:_emscripten_glDrawElementsInstancedNV,Ke:_emscripten_glDrawRangeElements,dg:_emscripten_glEnable,eg:_emscripten_glEnableVertexAttribArray,fe:_emscripten_glEndQuery,$d:_emscripten_glEndQueryEXT,Dc:_emscripten_glEndTransformFeedback,pe:_emscripten_glFenceSync,fg:_emscripten_glFinish,gg:_emscripten_glFlush,Ge:_emscripten_glFramebufferRenderbuffer,He:_emscripten_glFramebufferTexture2D,Hc:_emscripten_glFramebufferTextureLayer,hg:_emscripten_glFrontFace,ig:_emscripten_glGenBuffers,Ie:_emscripten_glGenFramebuffers,ge:_emscripten_glGenQueries,ae:_emscripten_glGenQueriesEXT,Je:_emscripten_glGenRenderbuffers,le:_emscripten_glGenSamplers,jg:_emscripten_glGenTextures,Pb:_emscripten_glGenTransformFeedbacks,Ue:_emscripten_glGenVertexArrays,af:_emscripten_glGenVertexArraysOES,xe:_emscripten_glGenerateMipmap,Cd:_emscripten_glGetActiveAttrib,Bd:_emscripten_glGetActiveUniform,ac:_emscripten_glGetActiveUniformBlockName,bc:_emscripten_glGetActiveUniformBlockiv,dc:_emscripten_glGetActiveUniformsiv,Ad:_emscripten_glGetAttachedShaders,zd:_emscripten_glGetAttribLocation,yd:_emscripten_glGetBooleanv,Xb:_emscripten_glGetBufferParameteri64v,kg:_emscripten_glGetBufferParameteriv,lg:_emscripten_glGetError,mg:_emscripten_glGetFloatv,rc:_emscripten_glGetFragDataLocation,ye:_emscripten_glGetFramebufferAttachmentParameteriv,Yb:_emscripten_glGetInteger64i_v,_b:_emscripten_glGetInteger64v,Fc:_emscripten_glGetIntegeri_v,ng:_emscripten_glGetIntegerv,Hb:_emscripten_glGetInternalformativ,Lb:_emscripten_glGetProgramBinary,og:_emscripten_glGetProgramInfoLog,pg:_emscripten_glGetProgramiv,Wd:_emscripten_glGetQueryObjecti64vEXT,Pd:_emscripten_glGetQueryObjectivEXT,Xd:_emscripten_glGetQueryObjectui64vEXT,he:_emscripten_glGetQueryObjectuiv,be:_emscripten_glGetQueryObjectuivEXT,ie:_emscripten_glGetQueryiv,ce:_emscripten_glGetQueryivEXT,ze:_emscripten_glGetRenderbufferParameteriv,Tb:_emscripten_glGetSamplerParameterfv,Ub:_emscripten_glGetSamplerParameteriv,qg:_emscripten_glGetShaderInfoLog,Td:_emscripten_glGetShaderPrecisionFormat,xd:_emscripten_glGetShaderSource,rg:_emscripten_glGetShaderiv,sg:_emscripten_glGetString,Ze:_emscripten_glGetStringi,Zb:_emscripten_glGetSynciv,wd:_emscripten_glGetTexParameterfv,vd:_emscripten_glGetTexParameteriv,zc:_emscripten_glGetTransformFeedbackVarying,cc:_emscripten_glGetUniformBlockIndex,ec:_emscripten_glGetUniformIndices,tg:_emscripten_glGetUniformLocation,ud:_emscripten_glGetUniformfv,td:_emscripten_glGetUniformiv,sc:_emscripten_glGetUniformuiv,yc:_emscripten_glGetVertexAttribIiv,xc:_emscripten_glGetVertexAttribIuiv,qd:_emscripten_glGetVertexAttribPointerv,sd:_emscripten_glGetVertexAttribfv,rd:_emscripten_glGetVertexAttribiv,pd:_emscripten_glHint,Ud:_emscripten_glInvalidateFramebuffer,Vd:_emscripten_glInvalidateSubFramebuffer,od:_emscripten_glIsBuffer,nd:_emscripten_glIsEnabled,md:_emscripten_glIsFramebuffer,ld:_emscripten_glIsProgram,Oc:_emscripten_glIsQuery,Qd:_emscripten_glIsQueryEXT,kd:_emscripten_glIsRenderbuffer,Wb:_emscripten_glIsSampler,jd:_emscripten_glIsShader,qe:_emscripten_glIsSync,ug:_emscripten_glIsTexture,Ob:_emscripten_glIsTransformFeedback,Gc:_emscripten_glIsVertexArray,Od:_emscripten_glIsVertexArrayOES,vg:_emscripten_glLineWidth,wg:_emscripten_glLinkProgram,Oe:_emscripten_glMultiDrawArraysInstancedBaseInstanceWEBGL,Pe:_emscripten_glMultiDrawElementsInstancedBaseVertexBaseInstanceWEBGL,Nb:_emscripten_glPauseTransformFeedback,xg:_emscripten_glPixelStorei,Zc:_emscripten_glPolygonModeWEBGL,id:_emscripten_glPolygonOffset,$c:_emscripten_glPolygonOffsetClampEXT,Kb:_emscripten_glProgramBinary,Jb:_emscripten_glProgramParameteri,Yd:_emscripten_glQueryCounterEXT,Re:_emscripten_glReadBuffer,yg:_emscripten_glReadPixels,hd:_emscripten_glReleaseShaderCompiler,Ae:_emscripten_glRenderbufferStorage,we:_emscripten_glRenderbufferStorageMultisample,Mb:_emscripten_glResumeTransformFeedback,gd:_emscripten_glSampleCoverage,me:_emscripten_glSamplerParameterf,Vb:_emscripten_glSamplerParameterfv,ne:_emscripten_glSamplerParameteri,oe:_emscripten_glSamplerParameteriv,zg:_emscripten_glScissor,fd:_emscripten_glShaderBinary,Ag:_emscripten_glShaderSource,Bg:_emscripten_glStencilFunc,Cg:_emscripten_glStencilFuncSeparate,Dg:_emscripten_glStencilMask,Eg:_emscripten_glStencilMaskSeparate,Fg:_emscripten_glStencilOp,Gg:_emscripten_glStencilOpSeparate,Hg:_emscripten_glTexImage2D,Uc:_emscripten_glTexImage3D,Ig:_emscripten_glTexParameterf,Jg:_emscripten_glTexParameterfv,Kg:_emscripten_glTexParameteri,Lg:_emscripten_glTexParameteriv,Le:_emscripten_glTexStorage2D,Ib:_emscripten_glTexStorage3D,Ma:_emscripten_glTexSubImage2D,Tc:_emscripten_glTexSubImage3D,Ac:_emscripten_glTransformFeedbackVaryings,Na:_emscripten_glUniform1f,Oa:_emscripten_glUniform1fv,wf:_emscripten_glUniform1i,xf:_emscripten_glUniform1iv,qc:_emscripten_glUniform1ui,mc:_emscripten_glUniform1uiv,yf:_emscripten_glUniform2f,zf:_emscripten_glUniform2fv,vf:_emscripten_glUniform2i,uf:_emscripten_glUniform2iv,pc:_emscripten_glUniform2ui,lc:_emscripten_glUniform2uiv,tf:_emscripten_glUniform3f,sf:_emscripten_glUniform3fv,rf:_emscripten_glUniform3i,qf:_emscripten_glUniform3iv,oc:_emscripten_glUniform3ui,kc:_emscripten_glUniform3uiv,pf:_emscripten_glUniform4f,of:_emscripten_glUniform4fv,bf:_emscripten_glUniform4i,cf:_emscripten_glUniform4iv,nc:_emscripten_glUniform4ui,jc:_emscripten_glUniform4uiv,$b:_emscripten_glUniformBlockBinding,df:_emscripten_glUniformMatrix2fv,Nc:_emscripten_glUniformMatrix2x3fv,Lc:_emscripten_glUniformMatrix2x4fv,ef:_emscripten_glUniformMatrix3fv,Mc:_emscripten_glUniformMatrix3x2fv,Jc:_emscripten_glUniformMatrix3x4fv,ff:_emscripten_glUniformMatrix4fv,Kc:_emscripten_glUniformMatrix4x2fv,Ic:_emscripten_glUniformMatrix4x3fv,gf:_emscripten_glUseProgram,ed:_emscripten_glValidateProgram,hf:_emscripten_glVertexAttrib1f,dd:_emscripten_glVertexAttrib1fv,cd:_emscripten_glVertexAttrib2f,jf:_emscripten_glVertexAttrib2fv,bd:_emscripten_glVertexAttrib3f,kf:_emscripten_glVertexAttrib3fv,ad:_emscripten_glVertexAttrib4f,lf:_emscripten_glVertexAttrib4fv,Me:_emscripten_glVertexAttribDivisor,Kd:_emscripten_glVertexAttribDivisorANGLE,Fb:_emscripten_glVertexAttribDivisorARB,Yc:_emscripten_glVertexAttribDivisorEXT,Gb:_emscripten_glVertexAttribDivisorNV,wc:_emscripten_glVertexAttribI4i,uc:_emscripten_glVertexAttribI4iv,vc:_emscripten_glVertexAttribI4ui,tc:_emscripten_glVertexAttribI4uiv,Ne:_emscripten_glVertexAttribIPointer,mf:_emscripten_glVertexAttribPointer,nf:_emscripten_glViewport,re:_emscripten_glWaitSync,Za:_emscripten_request_animation_frame_loop,hb:_emscripten_resize_heap,ob:_environ_get,pb:_environ_sizes_get,Ra:_exit,la:_fd_close,jb:_fd_pread,Ea:_fd_read,nb:_fd_seek,ka:_fd_write,Pa:_glGetIntegerv,pa:_glGetString,Qa:_glGetStringi,Rd:invoke_dd,Sd:invoke_dddd,Aa:invoke_diii,Ua:invoke_fdiiii,Ta:invoke_fdiiiii,Sa:invoke_fii,Ba:invoke_fiii,s:invoke_fiiidi,V:invoke_fiiif,t:invoke_fiiiidi,r:invoke_i,j:invoke_ii,G:invoke_iif,$a:invoke_iiffi,ra:invoke_iiffiii,f:invoke_iii,Ja:invoke_iiiffii,ta:invoke_iiifi,g:invoke_iiii,T:invoke_iiiiff,l:invoke_iiiii,db:invoke_iiiiid,A:invoke_iiiiii,y:invoke_iiiiiii,E:invoke_iiiiiiii,q:invoke_iiiiiiiii,qa:invoke_iiiiiiiiii,ca:invoke_iiiiiiiiiiii,oa:invoke_iiiiiiiiiiiifiii,R:invoke_iij,fb:invoke_j,ha:invoke_ji,_:invoke_jiii,da:invoke_jiiii,J:invoke_jjji,k:invoke_v,Pf:invoke_vff,b:invoke_vi,P:invoke_vid,S:invoke_vif,u:invoke_viff,D:invoke_viffff,Z:invoke_vifffff,Va:invoke_viffffff,C:invoke_viffi,ia:invoke_viffiiiiiii,c:invoke_vii,Ya:invoke_viidii,O:invoke_viif,v:invoke_viiff,$:invoke_viifi,va:invoke_viififii,x:invoke_viifiiifi,d:invoke_viii,I:invoke_viiif,xa:invoke_viiiff,B:invoke_viiiffi,K:invoke_viiiffiffii,L:invoke_viiififiiiiiiiiiiii,i:invoke_viiii,Xa:invoke_viiiidididii,ja:invoke_viiiif,ya:invoke_viiiiff,Da:invoke_viiiiffi,wa:invoke_viiiifi,h:invoke_viiiii,ab:invoke_viiiiif,Ia:invoke_viiiiiff,Wa:invoke_viiiiiffiii,cb:invoke_viiiiifi,m:invoke_viiiiii,p:invoke_viiiiiii,W:invoke_viiiiiiii,Y:invoke_viiiiiiiii,M:invoke_viiiiiiiiii,sa:invoke_viiiiiiiiiii,ba:invoke_viiiiiiiiiiiiiii,La:invoke_viiiiiji,Sb:invoke_viiiijjiiiiff,Q:invoke_viiij,z:invoke_viiijii,X:invoke_viij,n:invoke_viiji,ma:invoke_viijiffi,fa:invoke_viijii,bb:invoke_viijiii,aa:invoke_viijiiiif,Ka:invoke_viijiiiii,ua:invoke_viijj,U:invoke_viji,w:invoke_vijii,Ha:invoke_vijiifi,_a:invoke_vijiififi,za:invoke_vijiii,ea:invoke_vijjjj,Pc:invoke_vjii,na:_llvm_eh_typeid_for,qb:_random_get};function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_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_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_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}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_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_vff(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_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_viijiiiii(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_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_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_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_iiiiff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiifiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiffi(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_viijii(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_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_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_vijiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_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_viiififiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiffii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_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_viiiff(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_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_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffi(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viififii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijiififi(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_iij(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_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_jjji(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiifi(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_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_viifi(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_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiffiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viff(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifffff(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiidididii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiidi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiffiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffff(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffffff(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiif(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fdiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viffiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dddd(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_dd(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijjjj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_iiiiid(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0);return 0n}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function callMain(args=[]){var entryFunction=_main;args.unshift(thisProgram);var argc=args.length;var argv=stackAlloc((argc+1)*4);var argv_ptr=argv;for(var arg of args){HEAPU32[argv_ptr>>2]=stringToUTF8OnStack(arg);argv_ptr+=4}HEAPU32[argv_ptr>>2]=0;try{var ret=entryFunction(argc,argv);exitJS(ret,true);return ret}catch(e){return handleException(e)}}function run(args=arguments_){if(runDependencies>0){dependenciesFulfilled=run;return}preRun();if(runDependencies>0){dependenciesFulfilled=run;return}function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();preMain();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();var noInitialRun=Module["noInitialRun"]||false;if(!noInitialRun)callMain(args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}})();if(typeof exports==="object"&&typeof module==="object"){module.exports=createGridaCanvas;module.exports.default=createGridaCanvas}else if(typeof define==="function"&&define["amd"])define([],()=>createGridaCanvas); diff --git a/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm b/crates/grida-canvas-wasm/lib/bin/grida_canvas_wasm.wasm index 28a93dc08e..33ce6f67f6 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:83e981384f1f7dd545ce9839603155a377a9206cbe467c700ad5daba1d86fe79 -size 13195627 +oid sha256:64dc52d48723cf9eff216754c3cb024ea27ef810bd016d21d560e42cd82e951b +size 13197254 diff --git a/crates/grida-canvas-wasm/lib/index.ts b/crates/grida-canvas-wasm/lib/index.ts index c6c56e07e7..d4ea122e19 100644 --- a/crates/grida-canvas-wasm/lib/index.ts +++ b/crates/grida-canvas-wasm/lib/index.ts @@ -154,12 +154,40 @@ export default async function init( ); } +/** + * Renderer configuration flags. + * + * Matches the Rust `RuntimeRendererConfig` fields that are exposed via + * the C ABI `config_flags` bitfield. Pass at init time; individual + * fields remain mutable via `Scene.runtime_renderer_set_*()` setters. + */ +interface RendererConfig { + /** + * Skip the Taffy flexbox layout engine during scene loading. + * Derives layout from schema positions/sizes instead. + * @default false + */ + skip_layout?: boolean; +} + +/** Encode a `RendererConfig` into the C ABI `config_flags` bitfield. */ +function encodeConfigFlags(config?: RendererConfig): number { + let flags = 0; + if (config?.skip_layout) flags |= 1 << 0; + return flags; +} + interface CreateSurfaceOptions { /** * when true, embedded fonts will be registered and used for text rendering. * @default true */ use_embedded_fonts?: boolean; + /** + * Initial renderer configuration applied at construction. + * Fields can still be changed at runtime via `Scene.runtime_renderer_set_*()`. + */ + config?: RendererConfig; } class ApplicationFactory { @@ -191,7 +219,8 @@ class ApplicationFactory { const ptr = this.module._init( canvas.width, canvas.height, - options.use_embedded_fonts + options.use_embedded_fonts, + encodeConfigFlags(options.config) ); const _ = new Scene(this.module, ptr); _.resize(canvas.width, canvas.height); @@ -227,6 +256,8 @@ export type CreateCanvasOptions = canvas: HTMLCanvasElement; locateFile?: GridaCanvasModuleInitOptions["locateFile"]; useEmbeddedFonts?: boolean; + /** Initial renderer configuration. */ + config?: RendererConfig; } | { backend: "raster"; @@ -234,6 +265,8 @@ export type CreateCanvasOptions = height: number; locateFile?: GridaCanvasModuleInitOptions["locateFile"]; useEmbeddedFonts?: boolean; + /** Initial renderer configuration. */ + config?: RendererConfig; }; export class Canvas { @@ -291,10 +324,7 @@ export class Canvas { * Register image bytes with an explicit logical RID (e.g. res://images/logo.png). * Use when you need stable, document-mapped identifiers. */ - addImageWithId( - data: Uint8Array, - rid: string - ): AddImageWithIdResult | false { + addImageWithId(data: Uint8Array, rid: string): AddImageWithIdResult | false { return this._scene.addImageWithId(data, rid); } @@ -344,13 +374,15 @@ export async function createCanvas(opts: CreateCanvasOptions): Promise { const module = bindings as createGridaCanvas.GridaCanvasWasmBindings; const useEmbeddedFonts = opts.useEmbeddedFonts ?? true; + const configFlags = encodeConfigFlags(opts.config); if (opts.backend === "raster") { const appptr = module._init_with_backend( BACKEND_ID.Raster, opts.width, opts.height, - useEmbeddedFonts + useEmbeddedFonts, + configFlags ); return Canvas._fromRaster(new Scene(module, appptr)); } @@ -369,7 +401,8 @@ export async function createCanvas(opts: CreateCanvasOptions): Promise { BACKEND_ID.WebGL, opts.canvas.width, opts.canvas.height, - useEmbeddedFonts + useEmbeddedFonts, + configFlags ); const scene = new Scene(module, appptr); scene.resize(opts.canvas.width, opts.canvas.height); diff --git a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts index 724c2d1142..03cea3272d 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas-bindings.d.ts @@ -13,14 +13,16 @@ declare namespace canvas { _init( width: number, height: number, - use_embedded_fonts: boolean + use_embedded_fonts: boolean, + config_flags: number ): GridaCanvasApplicationPtr; _init_with_backend( backend_id: number, width: number, height: number, - use_embedded_fonts: boolean + use_embedded_fonts: boolean, + config_flags: number ): GridaCanvasApplicationPtr; // ==================================================================================================== @@ -49,9 +51,7 @@ declare namespace canvas { ptr: number, len: number ): void; - _drain_missing_images( - state: GridaCanvasApplicationPtr - ): Ptr; + _drain_missing_images(state: GridaCanvasApplicationPtr): Ptr; _resolve_image( state: GridaCanvasApplicationPtr, rid_ptr: number, @@ -248,6 +248,11 @@ declare namespace canvas { flags: number ): void; + _runtime_renderer_set_skip_layout( + state: GridaCanvasApplicationPtr, + skip: boolean + ): void; + _runtime_renderer_set_outline_mode( state: GridaCanvasApplicationPtr, enable: boolean @@ -261,10 +266,7 @@ declare namespace canvas { node_id_ptr: number, node_id_len: number ): boolean; - _text_edit_exit( - state: GridaCanvasApplicationPtr, - commit: boolean - ): Ptr; + _text_edit_exit(state: GridaCanvasApplicationPtr, commit: boolean): Ptr; _text_edit_is_active(state: GridaCanvasApplicationPtr): boolean; _text_edit_get_text(state: GridaCanvasApplicationPtr): Ptr; _text_edit_undo(state: GridaCanvasApplicationPtr): boolean; diff --git a/crates/grida-canvas-wasm/lib/modules/canvas.ts b/crates/grida-canvas-wasm/lib/modules/canvas.ts index 44a715e1f3..38a625b71a 100644 --- a/crates/grida-canvas-wasm/lib/modules/canvas.ts +++ b/crates/grida-canvas-wasm/lib/modules/canvas.ts @@ -452,7 +452,16 @@ export class Scene { return JSON.parse(str); } - getNodeAbsoluteBoundingBox(id: string): types.Rect | null { + /** + * Get the absolute bounding box of a node or the active scene. + * + * @param target - A node ID, or `""` to get the union bounds of the + * active scene's root children (computed in a single WASM call). + */ + getNodeAbsoluteBoundingBox( + target: (string & {}) | "" + ): types.Rect | null { + const id = target; this._assertAlive(); const [ptr, len] = this._alloc_string(id); const outptr = this.module._get_node_absolute_bounding_box( @@ -553,9 +562,7 @@ export class Scene { */ surfacePointerMove(x: number, y: number): SurfaceResponse { this._assertAlive(); - return unpackResponse( - this.module._surface_pointer_move(this.appptr, x, y) - ); + return unpackResponse(this.module._surface_pointer_move(this.appptr, x, y)); } /** @@ -709,6 +716,21 @@ export class Scene { this.module._runtime_renderer_set_render_policy_flags(this.appptr, flags); } + /** + * Skip layout computation during scene loading. + * + * When enabled, `load_scene` derives layout from schema positions/sizes + * instead of running the Taffy flexbox engine. Set **before** loading a scene. + * + * Use this for documents with only absolute positioning (e.g. imported + * Figma files without auto-layout) to eliminate the layout phase, which + * is the dominant cost in `load_scene` for large documents. + */ + runtime_renderer_set_skip_layout(skip: boolean) { + this._assertAlive(); + this.module._runtime_renderer_set_skip_layout(this.appptr, skip); + } + runtime_renderer_set_outline_mode(enable: boolean) { this._assertAlive(); this.module._runtime_renderer_set_outline_mode(this.appptr, enable); diff --git a/crates/grida-canvas-wasm/package.json b/crates/grida-canvas-wasm/package.json index 72697f5099..db08cf0fff 100644 --- a/crates/grida-canvas-wasm/package.json +++ b/crates/grida-canvas-wasm/package.json @@ -1,6 +1,6 @@ { "name": "@grida/canvas-wasm", - "version": "0.91.0-canary.10", + "version": "0.91.0-canary.12", "private": false, "description": "WASM bindings for Grida Canvas", "keywords": [ diff --git a/crates/grida-canvas-wasm/src/wasm_application.rs b/crates/grida-canvas-wasm/src/wasm_application.rs index f9a15df6b7..3b1a73fd6e 100644 --- a/crates/grida-canvas-wasm/src/wasm_application.rs +++ b/crates/grida-canvas-wasm/src/wasm_application.rs @@ -25,14 +25,39 @@ fn alloc_len_prefixed(bytes: &[u8]) -> *const u8 { const BACKEND_WEBGL: u32 = 0; const BACKEND_RASTER: u32 = 1; +/// Renderer config bitflags passed at init time. +/// +/// Bit layout (LSB-first): +/// bit 0: skip_layout +/// +/// Unset bits use `RuntimeRendererConfig::default()` values. +/// Extend by assigning higher bits — existing callers passing 0 +/// get the default behaviour. +const CONFIG_FLAG_SKIP_LAYOUT: u32 = 1 << 0; + +fn config_from_flags(flags: u32) -> cg::runtime::config::RuntimeRendererConfig { + let mut config = cg::runtime::config::RuntimeRendererConfig::default(); + if flags & CONFIG_FLAG_SKIP_LAYOUT != 0 { + config.skip_layout = true; + } + config +} + #[no_mangle] /// js::_init pub extern "C" fn init( width: i32, height: i32, use_embedded_fonts: bool, + config_flags: u32, ) -> Box { - init_with_backend(BACKEND_WEBGL, width, height, use_embedded_fonts) + init_with_backend( + BACKEND_WEBGL, + width, + height, + use_embedded_fonts, + config_flags, + ) } #[no_mangle] @@ -41,13 +66,20 @@ pub extern "C" fn init( /// backend_id: /// - 0 (`BACKEND_WEBGL`): webgl (emscripten/webgl2) /// - 1 (`BACKEND_RASTER`): raster (cpu) +/// +/// config_flags: bitfield for `RuntimeRendererConfig` (see `CONFIG_FLAG_*`). +/// Pass 0 for defaults. pub extern "C" fn init_with_backend( backend_id: u32, width: i32, height: i32, use_embedded_fonts: bool, + config_flags: u32, ) -> Box { - let options = cg::runtime::scene::RendererOptions { use_embedded_fonts }; + let options = cg::runtime::scene::RendererOptions { + use_embedded_fonts, + config: config_from_flags(config_flags), + }; match backend_id { BACKEND_RASTER => UnknownTargetApplication::new_raster(width, height, options), _ => cg::window::application_emscripten::new_webgl_app(width, height, options), @@ -151,9 +183,7 @@ pub unsafe extern "C" fn switch_scene( #[no_mangle] /// js::_drain_missing_images /// Returns a len-prefixed JSON array of missing image ref strings, or null if empty. -pub unsafe extern "C" fn drain_missing_images( - app: *mut UnknownTargetApplication, -) -> *const u8 { +pub unsafe extern "C" fn drain_missing_images(app: *mut UnknownTargetApplication) -> *const u8 { if let Some(app) = app.as_mut() { let refs = app.drain_missing_images(); if refs.is_empty() { @@ -543,10 +573,7 @@ pub unsafe extern "C" fn add_image_with_rid( rid_ptr: *const u8, rid_len: usize, ) -> *const u8 { - if let (Some(app), Some(rid)) = ( - app.as_mut(), - __str_from_ptr_len(rid_ptr, rid_len), - ) { + if let (Some(app), Some(rid)) = (app.as_mut(), __str_from_ptr_len(rid_ptr, rid_len)) { let data = std::slice::from_raw_parts(data_ptr, data_len); if let Some((width, height, r#type)) = app.add_image_with_rid(data, &rid) { let result = AddImageWithRidResult { @@ -783,7 +810,8 @@ pub unsafe extern "C" fn get_node_absolute_bounding_box( if let Some(app) = app.as_mut() { let id = __str_from_ptr_len(ptr, len); if let Some(id) = id { - if let Some(rect) = app.get_node_absolute_bounding_box(&id) { + let target = cg::window::application::BoundsTarget::from_str(&id); + if let Some(rect) = app.get_node_absolute_bounding_box(target) { let vec4 = rect.to_vec4(); // [f32; 4] let out = allocate(std::mem::size_of::() * 4) as *mut f32; std::ptr::copy_nonoverlapping(vec4.as_ptr(), out, 4); @@ -960,6 +988,17 @@ pub unsafe extern "C" fn runtime_renderer_set_render_policy_flags( } } +#[no_mangle] +/// js::_runtime_renderer_set_skip_layout +pub unsafe extern "C" fn runtime_renderer_set_skip_layout( + app: *mut UnknownTargetApplication, + skip: bool, +) { + if let Some(app) = app.as_mut() { + app.runtime_renderer_set_skip_layout(skip); + } +} + #[no_mangle] /// js::_runtime_renderer_set_outline_mode /// diff --git a/crates/grida-canvas/Cargo.toml b/crates/grida-canvas/Cargo.toml index 7b89c94375..732ccf6d7b 100644 --- a/crates/grida-canvas/Cargo.toml +++ b/crates/grida-canvas/Cargo.toml @@ -34,7 +34,7 @@ rstar = "0.12" # core resource hashing seahash = "4.1.0" # layout engine -taffy = "0.9.1" +taffy = "0.9.2" # svg parsing # (+2mb wasm32-unknown-emscripten@opt-level=3) usvg = { path = "../../third_party/usvg" } @@ -90,6 +90,10 @@ harness = false name = "bench_camera" harness = false +[[bench]] +name = "bench_load_scene" +harness = false + [[example]] name = "headless_gpu" required-features = ["native-gl-context"] diff --git a/crates/grida-canvas/benches/bench_load_scene.rs b/crates/grida-canvas/benches/bench_load_scene.rs new file mode 100644 index 0000000000..8973fa56b2 --- /dev/null +++ b/crates/grida-canvas/benches/bench_load_scene.rs @@ -0,0 +1,424 @@ +//! Benchmark: `load_scene` per-stage breakdown. +//! +//! Measures the four stages of `Renderer::load_scene()` independently: +//! 1. **layout** — Taffy flexbox + Skia paragraph measurement +//! 2. **geometry** — DFS transform/bounds propagation +//! 3. **effects** — effect tree classification +//! 4. **layers** — flatten + clip path + sort + RTree +//! +//! Scenes are generated synthetically at varying sizes to expose scaling +//! behaviour. Three scene archetypes are tested: +//! +//! - **flat_rects** — flat list of rectangles (no layout, no text) +//! - **flex_text** — flex containers with text spans (exercises layout + text measurement) +//! - **nested_groups** — deeply nested group hierarchy (exercises clip path computation) +//! +//! # Running +//! +//! ```sh +//! cargo bench -p cg --bench bench_load_scene +//! +//! # Only flat_rects at all sizes: +//! cargo bench -p cg --bench bench_load_scene -- flat_rects +//! +//! # Only the 10k size: +//! cargo bench -p cg --bench bench_load_scene -- 10k +//! +//! # Only full load_scene (not per-stage): +//! cargo bench -p cg --bench bench_load_scene -- /load_scene +//! ``` + +use cg::cache; +use cg::cg::prelude::*; +use cg::layout::engine::LayoutEngine; +use cg::node::factory::NodeFactory; +use cg::node::scene_graph::{Parent, SceneGraph}; +use cg::node::schema::*; +use cg::resources::ByteStore; +use cg::runtime::camera::Camera2D; +use cg::runtime::font_repository::FontRepository; +use cg::runtime::scene::{Backend, Renderer}; +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use math2::transform::AffineTransform; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +// ─── Scene builders ───────────────────────────────────────────────── + +/// Flat list of rectangles — no containers, no text, no grouping. +/// Exercises geometry + effects + layers without layout. +fn create_flat_rects(count: usize) -> Scene { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + let cols = (count as f32).sqrt().ceil() as usize; + + for i in 0..count { + let x = (i % cols) as f32 * 60.0; + let y = (i / cols) as f32 * 60.0; + let mut rect = nf.create_rectangle_node(); + rect.transform = AffineTransform::new(x, y, 0.0); + rect.size = Size { + width: 50.0, + height: 50.0, + }; + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::from_rgba(((i * 13) % 255) as u8, ((i * 7) % 255) as u8, 180, 255), + blend_mode: BlendMode::default(), + active: true, + })); + graph.append_child(Node::Rectangle(rect), Parent::Root); + } + + Scene { + name: format!("flat_rects_{count}"), + graph, + background_color: Some(CGColor::WHITE), + } +} + +/// Flex containers each holding a text span child. +/// `count` is the total number of nodes (count/2 containers + count/2 text spans). +/// Exercises layout (Taffy) + text measurement (Skia paragraph). +fn create_flex_text(count: usize) -> Scene { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + let pairs = count / 2; + let cols = (pairs as f32).sqrt().ceil() as usize; + + for i in 0..pairs { + let x = (i % cols) as f32 * 220.0; + let y = (i / cols) as f32 * 60.0; + + let mut container = nf.create_container_node(); + container.position = LayoutPositioningBasis::Cartesian(CGPoint { x, y }); + container.layout_container.layout_mode = LayoutMode::Flex; + container.layout_container.layout_direction = Axis::Horizontal; + container.layout_dimensions.layout_target_width = Some(200.0); + container.layout_dimensions.layout_target_height = Some(40.0); + + let container_id = graph.append_child(Node::Container(container), Parent::Root); + + let mut text = nf.create_text_span_node(); + text.text = format!("Item {i}"); + text.width = Some(180.0); + graph.append_child(Node::TextSpan(text), Parent::NodeId(container_id)); + } + + Scene { + name: format!("flex_text_{count}"), + graph, + background_color: Some(CGColor::WHITE), + } +} + +/// Deeply nested group hierarchy — each level has `breadth` children, +/// with leaf nodes at the bottom. Total nodes ≈ count. +/// Container nodes have `clip: true` to exercise `compute_clip_path`. +fn create_nested_groups(count: usize) -> Scene { + let nf = NodeFactory::new(); + let mut graph = SceneGraph::new(); + let depth = 8usize; + let breadth = ((count as f64).powf(1.0 / depth as f64)).ceil() as usize; + + fn build_level( + graph: &mut SceneGraph, + nf: &NodeFactory, + parent: Parent, + depth_remaining: usize, + breadth: usize, + pos_x: f32, + pos_y: f32, + remaining: &mut usize, + ) { + if *remaining == 0 { + return; + } + if depth_remaining == 0 { + // Leaf: rectangle + let mut rect = nf.create_rectangle_node(); + rect.transform = AffineTransform::new(pos_x, pos_y, 0.0); + rect.size = Size { + width: 20.0, + height: 20.0, + }; + rect.set_fill(Paint::Solid(SolidPaint { + color: CGColor::RED, + blend_mode: BlendMode::default(), + active: true, + })); + graph.append_child(Node::Rectangle(rect), parent); + *remaining -= 1; + return; + } + + // Interior: container with clip + let mut container = nf.create_container_node(); + container.position = LayoutPositioningBasis::Cartesian(CGPoint { x: pos_x, y: pos_y }); + container.clip = true; + container.layout_dimensions.layout_target_width = Some(500.0); + container.layout_dimensions.layout_target_height = Some(500.0); + let container_id = graph.append_child(Node::Container(container), parent); + *remaining -= 1; + + for b in 0..breadth { + if *remaining == 0 { + break; + } + build_level( + graph, + nf, + Parent::NodeId(container_id), + depth_remaining - 1, + breadth, + b as f32 * 30.0, + 0.0, + remaining, + ); + } + } + + let mut remaining = count; + let top_count = breadth.min(remaining); + for i in 0..top_count { + if remaining == 0 { + break; + } + build_level( + &mut graph, + &nf, + Parent::Root, + depth - 1, + breadth, + i as f32 * 600.0, + 0.0, + &mut remaining, + ); + } + + Scene { + name: format!("nested_groups_{count}"), + graph, + background_color: Some(CGColor::WHITE), + } +} + +// ─── Helpers ──────────────────────────────────────────────────────── + +const VP_W: i32 = 1000; +const VP_H: i32 = 1000; + +fn make_renderer() -> Renderer { + Renderer::new( + Backend::new_from_raster(VP_W, VP_H), + None, + Camera2D::new(Size { + width: VP_W as f32, + height: VP_H as f32, + }), + ) +} + +fn viewport_size() -> Size { + Size { + width: VP_W as f32, + height: VP_H as f32, + } +} + +// ─── Benchmarks ───────────────────────────────────────────────────── + +fn bench_load_scene(c: &mut Criterion) { + let sizes: &[(usize, &str)] = &[ + (1_000, "1k"), + (10_000, "10k"), + (50_000, "50k"), + (100_000, "100k"), + ]; + + // Scene generators: (name, builder_fn) + let generators: &[(&str, fn(usize) -> Scene)] = &[ + ("flat_rects", create_flat_rects), + ("flex_text", create_flex_text), + ("nested_groups", create_nested_groups), + ]; + + for (gen_name, gen_fn) in generators { + // ── Full load_scene ── + { + let mut group = c.benchmark_group(format!("{gen_name}/load_scene")); + group.sample_size(10); + group.measurement_time(Duration::from_secs(30)); + + for &(count, label) in sizes { + let scene = gen_fn(count); + let node_count = scene.graph.node_count(); + + group.bench_with_input( + BenchmarkId::new("total", format!("{label}_n{node_count}")), + &scene, + |b, scene| { + b.iter_batched( + || (make_renderer(), scene.clone()), + |(mut renderer, s)| { + renderer.load_scene(black_box(s)); + renderer.free(); + }, + criterion::BatchSize::LargeInput, + ); + }, + ); + } + group.finish(); + } + + // ── Per-stage: layout ── + { + let mut group = c.benchmark_group(format!("{gen_name}/stage_layout")); + group.sample_size(10); + group.measurement_time(Duration::from_secs(20)); + + for &(count, label) in sizes { + let scene = gen_fn(count); + let node_count = scene.graph.node_count(); + + group.bench_with_input( + BenchmarkId::new("layout", format!("{label}_n{node_count}")), + &scene, + |b, scene| { + let fonts = FontRepository::new(Arc::new(Mutex::new(ByteStore::new()))); + b.iter(|| { + let mut engine = LayoutEngine::new(); + let mut paragraph_cache = cache::paragraph::ParagraphCache::new(); + engine.compute( + black_box(scene), + viewport_size(), + Some(cg::layout::tree::TextMeasureProvider { + paragraph_cache: &mut paragraph_cache, + fonts: &fonts, + }), + ); + }); + }, + ); + } + group.finish(); + } + + // ── Per-stage: geometry ── + { + let mut group = c.benchmark_group(format!("{gen_name}/stage_geometry")); + group.sample_size(10); + group.measurement_time(Duration::from_secs(20)); + + for &(count, label) in sizes { + let scene = gen_fn(count); + let node_count = scene.graph.node_count(); + + group.bench_with_input( + BenchmarkId::new("geometry", format!("{label}_n{node_count}")), + &scene, + |b, scene| { + // Pre-compute layout so we can isolate geometry + let fonts = FontRepository::new(Arc::new(Mutex::new(ByteStore::new()))); + let mut engine = LayoutEngine::new(); + let mut paragraph_cache = cache::paragraph::ParagraphCache::new(); + engine.compute( + scene, + viewport_size(), + Some(cg::layout::tree::TextMeasureProvider { + paragraph_cache: &mut paragraph_cache, + fonts: &fonts, + }), + ); + let layout_result = engine.result(); + + b.iter(|| { + let mut scene_cache = cache::scene::SceneCache::new(); + scene_cache.update_geometry_with_layout( + black_box(scene), + &fonts, + layout_result, + viewport_size(), + ); + }); + }, + ); + } + group.finish(); + } + + // ── Per-stage: effects ── + { + let mut group = c.benchmark_group(format!("{gen_name}/stage_effects")); + group.sample_size(10); + group.measurement_time(Duration::from_secs(20)); + + for &(count, label) in sizes { + let scene = gen_fn(count); + let node_count = scene.graph.node_count(); + + group.bench_with_input( + BenchmarkId::new("effects", format!("{label}_n{node_count}")), + &scene, + |b, scene| { + b.iter(|| { + let mut scene_cache = cache::scene::SceneCache::new(); + scene_cache.update_effect_tree(black_box(scene)); + }); + }, + ); + } + group.finish(); + } + + // ── Per-stage: layers ── + { + let mut group = c.benchmark_group(format!("{gen_name}/stage_layers")); + group.sample_size(10); + group.measurement_time(Duration::from_secs(20)); + + for &(count, label) in sizes { + let scene = gen_fn(count); + let node_count = scene.graph.node_count(); + + group.bench_with_input( + BenchmarkId::new("layers", format!("{label}_n{node_count}")), + &scene, + |b, scene| { + // Pre-build geometry so we can isolate layers + let fonts = FontRepository::new(Arc::new(Mutex::new(ByteStore::new()))); + let mut engine = LayoutEngine::new(); + let mut paragraph_cache = cache::paragraph::ParagraphCache::new(); + engine.compute( + scene, + viewport_size(), + Some(cg::layout::tree::TextMeasureProvider { + paragraph_cache: &mut paragraph_cache, + fonts: &fonts, + }), + ); + let layout_result = engine.result(); + let mut scene_cache = cache::scene::SceneCache::new(); + scene_cache.update_geometry_with_layout( + scene, + &fonts, + layout_result, + viewport_size(), + ); + scene_cache.update_effect_tree(scene); + + b.iter(|| { + // Clone so each iteration rebuilds layers + let mut sc = scene_cache.clone(); + sc.update_layers(black_box(scene)); + }); + }, + ); + } + group.finish(); + } + } +} + +criterion_group!(benches, bench_load_scene); +criterion_main!(benches); diff --git a/crates/grida-canvas/benches/bench_mipmap.rs b/crates/grida-canvas/benches/bench_mipmap.rs index f498154dd0..021d4a7120 100644 --- a/crates/grida-canvas/benches/bench_mipmap.rs +++ b/crates/grida-canvas/benches/bench_mipmap.rs @@ -1,35 +1,151 @@ -use cg::cache::mipmap::{ImageMipmaps, MipmapConfig, MipmapLevels}; -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use skia_safe::surfaces; +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use skia_safe::{ + surfaces, Color, FilterMode, Image, Matrix, MipmapMode, Paint, Rect, SamplingOptions, TileMode, +}; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Render an image shader into a target surface. +/// `image` is the (optionally mipmapped) image. +/// `logical_size` is the node size (e.g. 10x10). +/// `zoom` is the camera zoom applied to the canvas. +fn render_image_shader( + target: &mut skia_safe::Surface, + image: &Image, + logical_size: (f32, f32), + zoom: f32, + sampling: SamplingOptions, +) { + let canvas = target.canvas(); + canvas.save(); + canvas.clear(Color::WHITE); + + // Simulate camera zoom + canvas.scale((zoom, zoom)); + + // Build shader matrix: maps image pixels -> logical container + let sx = logical_size.0 / image.width() as f32; + let sy = logical_size.1 / image.height() as f32; + let matrix = Matrix::new_all(sx, 0.0, 0.0, 0.0, sy, 0.0, 0.0, 0.0, 1.0); + + if let Some(shader) = image.to_shader( + Some((TileMode::Decal, TileMode::Decal)), + sampling, + Some(&matrix), + ) { + let mut paint = Paint::default(); + paint.set_shader(shader); + canvas.draw_rect(Rect::from_wh(logical_size.0, logical_size.1), &paint); + } + + canvas.restore(); +} + +// --------------------------------------------------------------------------- +// 1. Skia mipmap generation at various source sizes +// --------------------------------------------------------------------------- fn bench_mipmap_generation(c: &mut Criterion) { - let mut surface = surfaces::raster_n32_premul((2048, 2048)).unwrap(); + let mut group = c.benchmark_group("mipmap_gen"); + + for &size in &[512, 1024, 2048, 4096] { + let mut surface = surfaces::raster_n32_premul((size, size)).unwrap(); + let image = surface.image_snapshot(); + + group.bench_with_input(BenchmarkId::new("skia_builtin", size), &size, |b, _| { + b.iter(|| { + let _ = black_box(image.with_default_mipmaps()); + }) + }); + } + + group.finish(); +} + +// --------------------------------------------------------------------------- +// 2. Rendering benchmarks: Skia mipmap modes vs no mipmaps +// --------------------------------------------------------------------------- + +fn bench_mipmap_render(c: &mut Criterion) { + let source_size = 4096; + let logical_size = (10.0_f32, 10.0_f32); + + // Prepare source image + let mut surface = surfaces::raster_n32_premul((source_size, source_size)).unwrap(); let image = surface.image_snapshot(); - let mut group = c.benchmark_group("mipmap_gen"); + // Skia built-in mipmaps + let skia_mipmapped = image.with_default_mipmaps().unwrap(); + + let mut group = c.benchmark_group("mipmap_render"); + + for &zoom in &[1.0_f32, 5.0, 25.0, 100.0] { + let render_w = (logical_size.0 * zoom).ceil() as i32; + let render_h = (logical_size.1 * zoom).ceil() as i32; + let target_size = (render_w.max(1), render_h.max(1)); + + // --- Skia built-in: linear mipmap mode --- + group.bench_with_input( + BenchmarkId::new("skia_mipmap_linear", format!("z{zoom}")), + &zoom, + |b, _| { + let mut target = surfaces::raster_n32_premul(target_size).unwrap(); + let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::Linear); + b.iter(|| { + render_image_shader( + &mut target, + black_box(&skia_mipmapped), + logical_size, + zoom, + sampling, + ); + }) + }, + ); + + // --- Skia built-in: nearest mipmap mode --- + group.bench_with_input( + BenchmarkId::new("skia_mipmap_nearest", format!("z{zoom}")), + &zoom, + |b, _| { + let mut target = surfaces::raster_n32_premul(target_size).unwrap(); + let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::Nearest); + b.iter(|| { + render_image_shader( + &mut target, + black_box(&skia_mipmapped), + logical_size, + zoom, + sampling, + ); + }) + }, + ); - let chained = MipmapConfig { - levels: MipmapLevels::FullChain, - chained: true, - }; - group.bench_function("chained", |b| { - b.iter(|| { - let _ = ImageMipmaps::from_image(black_box(image.clone()), &chained); - }) - }); - - let direct = MipmapConfig { - levels: MipmapLevels::FullChain, - chained: false, - }; - group.bench_function("direct", |b| { - b.iter(|| { - let _ = ImageMipmaps::from_image(black_box(image.clone()), &direct); - }) - }); + // --- Baseline: full-res image, no mipmaps at all --- + group.bench_with_input( + BenchmarkId::new("no_mipmap", format!("z{zoom}")), + &zoom, + |b, _| { + let mut target = surfaces::raster_n32_premul(target_size).unwrap(); + let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::None); + b.iter(|| { + render_image_shader( + &mut target, + black_box(&image), + logical_size, + zoom, + sampling, + ); + }) + }, + ); + } group.finish(); } -criterion_group!(benches, bench_mipmap_generation); +criterion_group!(benches, bench_mipmap_generation, bench_mipmap_render,); criterion_main!(benches); diff --git a/crates/grida-canvas/examples/fixture_helpers/mod.rs b/crates/grida-canvas/examples/fixture_helpers/mod.rs index be04a9c1cc..265438fc10 100644 --- a/crates/grida-canvas/examples/fixture_helpers/mod.rs +++ b/crates/grida-canvas/examples/fixture_helpers/mod.rs @@ -8,10 +8,10 @@ pub use cg::cg::color::CGColor; pub use cg::cg::stroke_width::{SingularStrokeWidth, StrokeWidth}; pub use cg::cg::tilemode::TileMode; pub use cg::cg::types::*; -pub use math2::box_fit::BoxFit; use cg::io::io_grida_fbs; pub use cg::node::scene_graph::SceneGraph; pub use cg::node::schema::*; +pub use math2::box_fit::BoxFit; pub use math2::transform::AffineTransform; // ═════════════════════════════════════════════════════════════════════════════ @@ -49,7 +49,9 @@ pub fn build_maps_prefixed( position_map: &mut HashMap, prefix: &str, ) { - id_map.entry(*nid).or_insert_with(|| format!("{prefix}n{nid}")); + id_map + .entry(*nid) + .or_insert_with(|| format!("{prefix}n{nid}")); if let Some(children) = graph.get_children(nid) { for (i, child) in children.clone().iter().enumerate() { let pos = format!("a{i:04}"); @@ -61,7 +63,14 @@ pub fn build_maps_prefixed( } let mut counter = 0usize; for root in scene.graph.roots() { - walk(&scene.graph, &root, &mut counter, id_map, position_map, prefix); + walk( + &scene.graph, + &root, + &mut counter, + id_map, + position_map, + prefix, + ); } } @@ -72,11 +81,8 @@ fn fixtures_dir() -> std::path::PathBuf { /// Encode scenes into raw FlatBuffers bytes. fn encode_scenes(scenes: &[(&str, Scene)]) -> Vec { - let mut entries_data: Vec<( - String, - HashMap, - HashMap, - )> = Vec::new(); + let mut entries_data: Vec<(String, HashMap, HashMap)> = + Vec::new(); for (i, (key, scene)) in scenes.iter().enumerate() { let scene_id = key.to_string(); @@ -111,8 +117,7 @@ fn wrap_zip(fbs_bytes: &[u8]) -> Vec { let manifest = r#"{"document_file":"document.grida"}"#; let buf = Cursor::new(Vec::new()); let mut zip = ZipWriter::new(buf); - let opts = SimpleFileOptions::default() - .compression_method(zip::CompressionMethod::Deflated); + let opts = SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated); zip.start_file("manifest.json", opts).unwrap(); zip.write_all(manifest.as_bytes()).unwrap(); @@ -128,8 +133,8 @@ pub fn write_multi_fixture(scenes: &[(&str, Scene)], name: &str) { let bytes = encode_scenes(scenes); assert!(!bytes.is_empty(), "{name}: encoded bytes empty"); - let decoded = io_grida_fbs::decode_all(&bytes) - .unwrap_or_else(|e| panic!("{name}: decode failed: {e}")); + let decoded = + io_grida_fbs::decode_all(&bytes).unwrap_or_else(|e| panic!("{name}: decode failed: {e}")); assert_eq!( decoded.len(), scenes.len(), @@ -213,8 +218,24 @@ pub fn linear_gradient() -> Paint { tile_mode: TileMode::default(), transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CGColor { r: 255, g: 0, b: 0, a: 255 } }, - GradientStop { offset: 1.0, color: CGColor { r: 0, g: 0, b: 255, a: 255 } }, + GradientStop { + offset: 0.0, + color: CGColor { + r: 255, + g: 0, + b: 0, + a: 255, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 0, + g: 0, + b: 255, + a: 255, + }, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, @@ -226,8 +247,24 @@ pub fn radial_gradient() -> Paint { active: true, transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CGColor { r: 255, g: 255, b: 0, a: 255 } }, - GradientStop { offset: 1.0, color: CGColor { r: 0, g: 128, b: 0, a: 200 } }, + GradientStop { + offset: 0.0, + color: CGColor { + r: 255, + g: 255, + b: 0, + a: 255, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 0, + g: 128, + b: 0, + a: 200, + }, + }, ], opacity: 0.8, blend_mode: BlendMode::Normal, @@ -240,9 +277,33 @@ pub fn sweep_gradient() -> Paint { active: true, transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CGColor { r: 0, g: 255, b: 255, a: 255 } }, - GradientStop { offset: 0.5, color: CGColor { r: 255, g: 0, b: 255, a: 255 } }, - GradientStop { offset: 1.0, color: CGColor { r: 255, g: 255, b: 0, a: 255 } }, + GradientStop { + offset: 0.0, + color: CGColor { + r: 0, + g: 255, + b: 255, + a: 255, + }, + }, + GradientStop { + offset: 0.5, + color: CGColor { + r: 255, + g: 0, + b: 255, + a: 255, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 255, + g: 255, + b: 0, + a: 255, + }, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, @@ -254,7 +315,10 @@ pub fn sweep_gradient() -> Paint { pub const SYSTEM_IMAGE: &str = "system://images/checker-16-strip-L98L92.png"; pub fn image_paint() -> Paint { - image_paint_with(ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), ImagePaintFit::Fit(BoxFit::Cover)) + image_paint_with( + ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), + ImagePaintFit::Fit(BoxFit::Cover), + ) } pub fn image_paint_with(image: ResourceRef, fit: ImagePaintFit) -> Paint { @@ -282,7 +346,10 @@ pub fn rect(x: f32, y: f32, w: f32, h: f32, fill: Paint) -> Node { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), @@ -302,7 +369,10 @@ pub fn rect_rotated(x: f32, y: f32, w: f32, h: f32, rotation: f32, fill: Paint) blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, rotation), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), @@ -322,7 +392,10 @@ pub fn ellipse(x: f32, y: f32, w: f32, h: f32, fill: Paint) -> Node { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, fills: Paints::new(vec![fill]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -345,7 +418,10 @@ pub fn line(x: f32, y: f32, length: f32, rotation: f32, stroke_width: f32) -> No mask: None, effects: LayerEffects::default(), transform: AffineTransform::new(x, y, rotation), - size: Size { width: length, height: 0.0 }, + size: Size { + width: length, + height: 0.0, + }, strokes: Paints::new(vec![solid(0, 0, 0, 255)]), stroke_width, stroke_cap: StrokeCap::Butt, @@ -388,14 +464,24 @@ pub fn text(x: f32, y: f32, content: &str, font_size: f32, font_weight: u32) -> } /// Rectangle with effects (shadows, blur, etc.). -pub fn rect_with_effects(x: f32, y: f32, w: f32, h: f32, fill: Paint, effects: LayerEffects) -> Node { +pub fn rect_with_effects( + x: f32, + y: f32, + w: f32, + h: f32, + fill: Paint, + effects: LayerEffects, +) -> Node { Node::Rectangle(RectangleNodeRec { active: true, opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), @@ -408,14 +494,24 @@ pub fn rect_with_effects(x: f32, y: f32, w: f32, h: f32, fill: Paint, effects: L } /// Rectangle with effects and absolute positioning (for use inside containers). -pub fn rect_absolute_with_effects(x: f32, y: f32, w: f32, h: f32, fill: Paint, effects: LayerEffects) -> Node { +pub fn rect_absolute_with_effects( + x: f32, + y: f32, + w: f32, + h: f32, + fill: Paint, + effects: LayerEffects, +) -> Node { Node::Rectangle(RectangleNodeRec { active: true, opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), @@ -438,7 +534,10 @@ pub fn rect_absolute(x: f32, y: f32, w: f32, h: f32, fill: Paint) -> Node { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), @@ -461,7 +560,10 @@ pub fn rect_opacity(x: f32, y: f32, w: f32, h: f32, fill: Paint, opacity: f32) - blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), @@ -475,9 +577,14 @@ pub fn rect_opacity(x: f32, y: f32, w: f32, h: f32, fill: Paint, opacity: f32) - /// Rectangle with opacity, fill, and stroke. pub fn rect_opacity_fill_stroke( - x: f32, y: f32, w: f32, h: f32, - fill: Paint, stroke: Paint, - stroke_width: f32, opacity: f32, + x: f32, + y: f32, + w: f32, + h: f32, + fill: Paint, + stroke: Paint, + stroke_width: f32, + opacity: f32, ) -> Node { Node::Rectangle(RectangleNodeRec { active: true, @@ -485,7 +592,10 @@ pub fn rect_opacity_fill_stroke( blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, w, h, 0.0), - size: Size { width: w, height: h }, + size: Size { + width: w, + height: h, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![fill]), diff --git a/crates/grida-canvas/examples/fixtures/cover.rs b/crates/grida-canvas/examples/fixtures/cover.rs index 72c82ac1eb..d36902db5d 100644 --- a/crates/grida-canvas/examples/fixtures/cover.rs +++ b/crates/grida-canvas/examples/fixtures/cover.rs @@ -14,19 +14,74 @@ use cg::cg::stroke_width::SingularStrokeWidth; // Palette // ═══════════════════════════════════════════════════════════════════════════ -const BG: CGColor = CGColor { r: 14, g: 14, b: 18, a: 255 }; +const BG: CGColor = CGColor { + r: 14, + g: 14, + b: 18, + a: 255, +}; // Deep purples / blues / ambers -const PURPLE_DEEP: CGColor = CGColor { r: 88, g: 28, b: 180, a: 255 }; -const PURPLE_LIGHT: CGColor = CGColor { r: 168, g: 85, b: 247, a: 255 }; -const BLUE_ELECTRIC: CGColor = CGColor { r: 56, g: 130, b: 255, a: 255 }; -const BLUE_DEEP: CGColor = CGColor { r: 30, g: 64, b: 175, a: 255 }; -const CYAN_GLOW: CGColor = CGColor { r: 34, g: 211, b: 238, a: 255 }; -const AMBER: CGColor = CGColor { r: 245, g: 158, b: 11, a: 255 }; -const AMBER_WARM: CGColor = CGColor { r: 251, g: 191, b: 36, a: 255 }; -const ROSE: CGColor = CGColor { r: 244, g: 63, b: 94, a: 255 }; -const WHITE_DIM: CGColor = CGColor { r: 255, g: 255, b: 255, a: 18 }; -const WHITE_FAINT: CGColor = CGColor { r: 255, g: 255, b: 255, a: 8 }; +const PURPLE_DEEP: CGColor = CGColor { + r: 88, + g: 28, + b: 180, + a: 255, +}; +const PURPLE_LIGHT: CGColor = CGColor { + r: 168, + g: 85, + b: 247, + a: 255, +}; +const BLUE_ELECTRIC: CGColor = CGColor { + r: 56, + g: 130, + b: 255, + a: 255, +}; +const BLUE_DEEP: CGColor = CGColor { + r: 30, + g: 64, + b: 175, + a: 255, +}; +const CYAN_GLOW: CGColor = CGColor { + r: 34, + g: 211, + b: 238, + a: 255, +}; +const AMBER: CGColor = CGColor { + r: 245, + g: 158, + b: 11, + a: 255, +}; +const AMBER_WARM: CGColor = CGColor { + r: 251, + g: 191, + b: 36, + a: 255, +}; +const ROSE: CGColor = CGColor { + r: 244, + g: 63, + b: 94, + a: 255, +}; +const WHITE_DIM: CGColor = CGColor { + r: 255, + g: 255, + b: 255, + a: 18, +}; +const WHITE_FAINT: CGColor = CGColor { + r: 255, + g: 255, + b: 255, + a: 8, +}; // ═══════════════════════════════════════════════════════════════════════════ // Scene @@ -39,420 +94,662 @@ pub fn build() -> Scene { let mut nodes: Vec<(NodeId, Node)> = Vec::new(); let links: std::collections::HashMap> = std::collections::HashMap::new(); let mut id = 0u64; - let mut next_id = || { id += 1; id }; + let mut next_id = || { + id += 1; + id + }; // ── 1. Background fill ────────────────────────────────────────────── let bg_id = next_id(); - nodes.push((bg_id, rect(0.0, 0.0, WIDTH, HEIGHT, Paint::Solid(SolidPaint { - active: true, - color: BG, - blend_mode: BlendMode::Normal, - })))); + nodes.push(( + bg_id, + rect( + 0.0, + 0.0, + WIDTH, + HEIGHT, + Paint::Solid(SolidPaint { + active: true, + color: BG, + blend_mode: BlendMode::Normal, + }), + ), + )); // ── 2. Large ambient glow — radial gradient, bottom-left ──────────── let glow1_id = next_id(); - nodes.push((glow1_id, Node::Ellipse(EllipseNodeRec { - active: true, - opacity: 0.55, - blend_mode: LayerBlendMode::Blend(BlendMode::Screen), - mask: None, - transform: AffineTransform::from_box_center(-200.0, 300.0, 1100.0, 1100.0, 0.0), - size: Size { width: 1100.0, height: 1100.0 }, - fills: Paints::new(vec![Paint::RadialGradient(RadialGradientPaint { + nodes.push(( + glow1_id, + Node::Ellipse(EllipseNodeRec { active: true, - transform: AffineTransform::default(), - stops: vec![ - GradientStop { offset: 0.0, color: PURPLE_DEEP }, - GradientStop { offset: 0.5, color: CGColor { r: 88, g: 28, b: 180, a: 120 } }, - GradientStop { offset: 1.0, color: CGColor { r: 88, g: 28, b: 180, a: 0 } }, - ], - opacity: 1.0, - blend_mode: BlendMode::Normal, - tile_mode: TileMode::default(), - })]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: SingularStrokeWidth(None), - inner_radius: None, - start_angle: 0.0, - angle: None, - corner_radius: None, - effects: LayerEffects::default(), - layout_child: None, - }))); + opacity: 0.55, + blend_mode: LayerBlendMode::Blend(BlendMode::Screen), + mask: None, + transform: AffineTransform::from_box_center(-200.0, 300.0, 1100.0, 1100.0, 0.0), + size: Size { + width: 1100.0, + height: 1100.0, + }, + fills: Paints::new(vec![Paint::RadialGradient(RadialGradientPaint { + active: true, + transform: AffineTransform::default(), + stops: vec![ + GradientStop { + offset: 0.0, + color: PURPLE_DEEP, + }, + GradientStop { + offset: 0.5, + color: CGColor { + r: 88, + g: 28, + b: 180, + a: 120, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 88, + g: 28, + b: 180, + a: 0, + }, + }, + ], + opacity: 1.0, + blend_mode: BlendMode::Normal, + tile_mode: TileMode::default(), + })]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: SingularStrokeWidth(None), + inner_radius: None, + start_angle: 0.0, + angle: None, + corner_radius: None, + effects: LayerEffects::default(), + layout_child: None, + }), + )); // ── 3. Ambient glow — top-right, blue ─────────────────────────────── let glow2_id = next_id(); - nodes.push((glow2_id, Node::Ellipse(EllipseNodeRec { - active: true, - opacity: 0.40, - blend_mode: LayerBlendMode::Blend(BlendMode::Screen), - mask: None, - transform: AffineTransform::from_box_center(900.0, -300.0, 1000.0, 1000.0, 0.0), - size: Size { width: 1000.0, height: 1000.0 }, - fills: Paints::new(vec![Paint::RadialGradient(RadialGradientPaint { + nodes.push(( + glow2_id, + Node::Ellipse(EllipseNodeRec { active: true, - transform: AffineTransform::default(), - stops: vec![ - GradientStop { offset: 0.0, color: BLUE_ELECTRIC }, - GradientStop { offset: 0.55, color: CGColor { r: 56, g: 130, b: 255, a: 80 } }, - GradientStop { offset: 1.0, color: CGColor { r: 56, g: 130, b: 255, a: 0 } }, - ], - opacity: 1.0, - blend_mode: BlendMode::Normal, - tile_mode: TileMode::default(), - })]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: SingularStrokeWidth(None), - inner_radius: None, - start_angle: 0.0, - angle: None, - corner_radius: None, - effects: LayerEffects::default(), - layout_child: None, - }))); + opacity: 0.40, + blend_mode: LayerBlendMode::Blend(BlendMode::Screen), + mask: None, + transform: AffineTransform::from_box_center(900.0, -300.0, 1000.0, 1000.0, 0.0), + size: Size { + width: 1000.0, + height: 1000.0, + }, + fills: Paints::new(vec![Paint::RadialGradient(RadialGradientPaint { + active: true, + transform: AffineTransform::default(), + stops: vec![ + GradientStop { + offset: 0.0, + color: BLUE_ELECTRIC, + }, + GradientStop { + offset: 0.55, + color: CGColor { + r: 56, + g: 130, + b: 255, + a: 80, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 56, + g: 130, + b: 255, + a: 0, + }, + }, + ], + opacity: 1.0, + blend_mode: BlendMode::Normal, + tile_mode: TileMode::default(), + })]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: SingularStrokeWidth(None), + inner_radius: None, + start_angle: 0.0, + angle: None, + corner_radius: None, + effects: LayerEffects::default(), + layout_child: None, + }), + )); // ── 4. Hero rounded rectangle — purple→blue linear gradient ───────── // with corner smoothing (G2), drop shadow, inner shadow let hero_id = next_id(); - nodes.push((hero_id, Node::Rectangle(RectangleNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: AffineTransform::from_box_center(180.0, 160.0, 520.0, 580.0, 0.0), - size: Size { width: 520.0, height: 580.0 }, - corner_radius: RectangularCornerRadius::circular(48.0), - corner_smoothing: CornerSmoothing(0.6), - fills: Paints::new(vec![ - Paint::LinearGradient(LinearGradientPaint { + nodes.push(( + hero_id, + Node::Rectangle(RectangleNodeRec { + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + transform: AffineTransform::from_box_center(180.0, 160.0, 520.0, 580.0, 0.0), + size: Size { + width: 520.0, + height: 580.0, + }, + corner_radius: RectangularCornerRadius::circular(48.0), + corner_smoothing: CornerSmoothing(0.6), + fills: Paints::new(vec![Paint::LinearGradient(LinearGradientPaint { active: true, - xy1: Alignment(-1.0, -1.0), // top-left - xy2: Alignment(1.0, 1.0), // bottom-right + xy1: Alignment(-1.0, -1.0), // top-left + xy2: Alignment(1.0, 1.0), // bottom-right tile_mode: TileMode::default(), transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: PURPLE_DEEP }, - GradientStop { offset: 0.45, color: BLUE_DEEP }, - GradientStop { offset: 1.0, color: BLUE_ELECTRIC }, + GradientStop { + offset: 0.0, + color: PURPLE_DEEP, + }, + GradientStop { + offset: 0.45, + color: BLUE_DEEP, + }, + GradientStop { + offset: 1.0, + color: BLUE_ELECTRIC, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, - }), - ]), - strokes: Paints::new(vec![Paint::Solid(SolidPaint { - active: true, - color: CGColor { r: 255, g: 255, b: 255, a: 15 }, - blend_mode: BlendMode::Normal, - })]), - stroke_style: StrokeStyle::default(), - stroke_width: StrokeWidth::Uniform(1.0), - effects: LayerEffects { - shadows: vec![ - FilterShadowEffect::DropShadow(FeShadow { - dx: 0.0, dy: 24.0, blur: 80.0, spread: -8.0, - color: CGColor { r: 0, g: 0, b: 0, a: 160 }, - active: true, - }), - FilterShadowEffect::InnerShadow(FeShadow { - dx: 0.0, dy: 2.0, blur: 40.0, spread: 0.0, - color: CGColor { r: 255, g: 255, b: 255, a: 30 }, - active: true, - }), - ], - ..LayerEffects::default() - }, - layout_child: None, - }))); + })]), + strokes: Paints::new(vec![Paint::Solid(SolidPaint { + active: true, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 15, + }, + blend_mode: BlendMode::Normal, + })]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(1.0), + effects: LayerEffects { + shadows: vec![ + FilterShadowEffect::DropShadow(FeShadow { + dx: 0.0, + dy: 24.0, + blur: 80.0, + spread: -8.0, + color: CGColor { + r: 0, + g: 0, + b: 0, + a: 160, + }, + active: true, + }), + FilterShadowEffect::InnerShadow(FeShadow { + dx: 0.0, + dy: 2.0, + blur: 40.0, + spread: 0.0, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 30, + }, + active: true, + }), + ], + ..LayerEffects::default() + }, + layout_child: None, + }), + )); // ── 5. Second card — overlapping, amber→rose gradient, rotated ────── let card2_id = next_id(); - nodes.push((card2_id, Node::Rectangle(RectangleNodeRec { - active: true, - opacity: 0.90, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: AffineTransform::from_box_center(520.0, 240.0, 440.0, 520.0, -6.0), - size: Size { width: 440.0, height: 520.0 }, - corner_radius: RectangularCornerRadius::circular(40.0), - corner_smoothing: CornerSmoothing(0.6), - fills: Paints::new(vec![ - Paint::LinearGradient(LinearGradientPaint { + nodes.push(( + card2_id, + Node::Rectangle(RectangleNodeRec { + active: true, + opacity: 0.90, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + transform: AffineTransform::from_box_center(520.0, 240.0, 440.0, 520.0, -6.0), + size: Size { + width: 440.0, + height: 520.0, + }, + corner_radius: RectangularCornerRadius::circular(40.0), + corner_smoothing: CornerSmoothing(0.6), + fills: Paints::new(vec![Paint::LinearGradient(LinearGradientPaint { active: true, - xy1: Alignment(0.0, -1.0), // top-center - xy2: Alignment(0.0, 1.0), // bottom-center + xy1: Alignment(0.0, -1.0), // top-center + xy2: Alignment(0.0, 1.0), // bottom-center tile_mode: TileMode::default(), transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: AMBER }, - GradientStop { offset: 0.6, color: ROSE }, - GradientStop { offset: 1.0, color: PURPLE_DEEP }, + GradientStop { + offset: 0.0, + color: AMBER, + }, + GradientStop { + offset: 0.6, + color: ROSE, + }, + GradientStop { + offset: 1.0, + color: PURPLE_DEEP, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, - }), - ]), - strokes: Paints::new(vec![Paint::Solid(SolidPaint { - active: true, - color: CGColor { r: 255, g: 255, b: 255, a: 12 }, - blend_mode: BlendMode::Normal, - })]), - stroke_style: StrokeStyle::default(), - stroke_width: StrokeWidth::Uniform(1.0), - effects: LayerEffects { - shadows: vec![ - FilterShadowEffect::DropShadow(FeShadow { - dx: 0.0, dy: 16.0, blur: 64.0, spread: -4.0, - color: CGColor { r: 0, g: 0, b: 0, a: 140 }, - active: true, - }), - FilterShadowEffect::InnerShadow(FeShadow { - dx: 0.0, dy: 1.0, blur: 24.0, spread: 0.0, - color: CGColor { r: 255, g: 255, b: 255, a: 25 }, - active: true, - }), - ], - ..LayerEffects::default() - }, - layout_child: None, - }))); + })]), + strokes: Paints::new(vec![Paint::Solid(SolidPaint { + active: true, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 12, + }, + blend_mode: BlendMode::Normal, + })]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(1.0), + effects: LayerEffects { + shadows: vec![ + FilterShadowEffect::DropShadow(FeShadow { + dx: 0.0, + dy: 16.0, + blur: 64.0, + spread: -4.0, + color: CGColor { + r: 0, + g: 0, + b: 0, + a: 140, + }, + active: true, + }), + FilterShadowEffect::InnerShadow(FeShadow { + dx: 0.0, + dy: 1.0, + blur: 24.0, + spread: 0.0, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 25, + }, + active: true, + }), + ], + ..LayerEffects::default() + }, + layout_child: None, + }), + )); // ── 6. Third card — smaller, cyan→blue, further right ─────────────── let card3_id = next_id(); - nodes.push((card3_id, Node::Rectangle(RectangleNodeRec { - active: true, - opacity: 0.85, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: AffineTransform::from_box_center(880.0, 120.0, 380.0, 460.0, 4.0), - size: Size { width: 380.0, height: 460.0 }, - corner_radius: RectangularCornerRadius::circular(36.0), - corner_smoothing: CornerSmoothing(0.6), - fills: Paints::new(vec![ - Paint::LinearGradient(LinearGradientPaint { + nodes.push(( + card3_id, + Node::Rectangle(RectangleNodeRec { + active: true, + opacity: 0.85, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + transform: AffineTransform::from_box_center(880.0, 120.0, 380.0, 460.0, 4.0), + size: Size { + width: 380.0, + height: 460.0, + }, + corner_radius: RectangularCornerRadius::circular(36.0), + corner_smoothing: CornerSmoothing(0.6), + fills: Paints::new(vec![Paint::LinearGradient(LinearGradientPaint { active: true, xy1: Alignment(-1.0, 0.0), xy2: Alignment(1.0, 0.0), tile_mode: TileMode::default(), transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CYAN_GLOW }, - GradientStop { offset: 0.5, color: BLUE_ELECTRIC }, - GradientStop { offset: 1.0, color: BLUE_DEEP }, + GradientStop { + offset: 0.0, + color: CYAN_GLOW, + }, + GradientStop { + offset: 0.5, + color: BLUE_ELECTRIC, + }, + GradientStop { + offset: 1.0, + color: BLUE_DEEP, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, - }), - ]), - strokes: Paints::new(vec![Paint::Solid(SolidPaint { - active: true, - color: CGColor { r: 255, g: 255, b: 255, a: 10 }, - blend_mode: BlendMode::Normal, - })]), - stroke_style: StrokeStyle::default(), - stroke_width: StrokeWidth::Uniform(1.0), - effects: LayerEffects { - shadows: vec![ - FilterShadowEffect::DropShadow(FeShadow { - dx: 0.0, dy: 20.0, blur: 60.0, spread: -6.0, - color: CGColor { r: 0, g: 0, b: 0, a: 150 }, + })]), + strokes: Paints::new(vec![Paint::Solid(SolidPaint { + active: true, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 10, + }, + blend_mode: BlendMode::Normal, + })]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::Uniform(1.0), + effects: LayerEffects { + shadows: vec![FilterShadowEffect::DropShadow(FeShadow { + dx: 0.0, + dy: 20.0, + blur: 60.0, + spread: -6.0, + color: CGColor { + r: 0, + g: 0, + b: 0, + a: 150, + }, active: true, - }), - ], - ..LayerEffects::default() - }, - layout_child: None, - }))); + })], + ..LayerEffects::default() + }, + layout_child: None, + }), + )); // ── 7. Sweep gradient circle — accent, top-right area ─────────────── let sweep_id = next_id(); - nodes.push((sweep_id, Node::Ellipse(EllipseNodeRec { - active: true, - opacity: 0.75, - blend_mode: LayerBlendMode::Blend(BlendMode::Screen), - mask: None, - transform: AffineTransform::from_box_center(1100.0, 80.0, 340.0, 340.0, 0.0), - size: Size { width: 340.0, height: 340.0 }, - fills: Paints::new(vec![Paint::SweepGradient(SweepGradientPaint { + nodes.push(( + sweep_id, + Node::Ellipse(EllipseNodeRec { active: true, - transform: AffineTransform::default(), - stops: vec![ - GradientStop { offset: 0.0, color: AMBER_WARM }, - GradientStop { offset: 0.25, color: ROSE }, - GradientStop { offset: 0.5, color: PURPLE_LIGHT }, - GradientStop { offset: 0.75, color: BLUE_ELECTRIC }, - GradientStop { offset: 1.0, color: AMBER_WARM }, - ], - opacity: 1.0, - blend_mode: BlendMode::Normal, - })]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: SingularStrokeWidth(None), - inner_radius: None, - start_angle: 0.0, - angle: None, - corner_radius: None, - effects: LayerEffects { - shadows: vec![ - FilterShadowEffect::DropShadow(FeShadow { - dx: 0.0, dy: 8.0, blur: 48.0, spread: 0.0, - color: CGColor { r: 245, g: 158, b: 11, a: 100 }, + opacity: 0.75, + blend_mode: LayerBlendMode::Blend(BlendMode::Screen), + mask: None, + transform: AffineTransform::from_box_center(1100.0, 80.0, 340.0, 340.0, 0.0), + size: Size { + width: 340.0, + height: 340.0, + }, + fills: Paints::new(vec![Paint::SweepGradient(SweepGradientPaint { + active: true, + transform: AffineTransform::default(), + stops: vec![ + GradientStop { + offset: 0.0, + color: AMBER_WARM, + }, + GradientStop { + offset: 0.25, + color: ROSE, + }, + GradientStop { + offset: 0.5, + color: PURPLE_LIGHT, + }, + GradientStop { + offset: 0.75, + color: BLUE_ELECTRIC, + }, + GradientStop { + offset: 1.0, + color: AMBER_WARM, + }, + ], + opacity: 1.0, + blend_mode: BlendMode::Normal, + })]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: SingularStrokeWidth(None), + inner_radius: None, + start_angle: 0.0, + angle: None, + corner_radius: None, + effects: LayerEffects { + shadows: vec![FilterShadowEffect::DropShadow(FeShadow { + dx: 0.0, + dy: 8.0, + blur: 48.0, + spread: 0.0, + color: CGColor { + r: 245, + g: 158, + b: 11, + a: 100, + }, active: true, - }), - ], - ..LayerEffects::default() - }, - layout_child: None, - }))); + })], + ..LayerEffects::default() + }, + layout_child: None, + }), + )); // ── 8. Small floating hexagon — bottom-right ──────────────────────── let hex_id = next_id(); - nodes.push((hex_id, Node::RegularPolygon(RegularPolygonNodeRec { - active: true, - opacity: 0.5, - blend_mode: LayerBlendMode::Blend(BlendMode::Screen), - mask: None, - effects: LayerEffects::default(), - transform: AffineTransform::from_box_center(1300.0, 560.0, 180.0, 180.0, 15.0), - size: Size { width: 180.0, height: 180.0 }, - point_count: 6, - corner_radius: 12.0, - fills: Paints::new(vec![Paint::LinearGradient(LinearGradientPaint { + nodes.push(( + hex_id, + Node::RegularPolygon(RegularPolygonNodeRec { active: true, - xy1: Alignment(-1.0, -1.0), - xy2: Alignment(1.0, 1.0), - tile_mode: TileMode::default(), - transform: AffineTransform::default(), - stops: vec![ - GradientStop { offset: 0.0, color: PURPLE_LIGHT }, - GradientStop { offset: 1.0, color: BLUE_ELECTRIC }, - ], - opacity: 1.0, - blend_mode: BlendMode::Normal, - })]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: SingularStrokeWidth(None), - layout_child: None, - }))); + opacity: 0.5, + blend_mode: LayerBlendMode::Blend(BlendMode::Screen), + mask: None, + effects: LayerEffects::default(), + transform: AffineTransform::from_box_center(1300.0, 560.0, 180.0, 180.0, 15.0), + size: Size { + width: 180.0, + height: 180.0, + }, + point_count: 6, + corner_radius: 12.0, + fills: Paints::new(vec![Paint::LinearGradient(LinearGradientPaint { + active: true, + xy1: Alignment(-1.0, -1.0), + xy2: Alignment(1.0, 1.0), + tile_mode: TileMode::default(), + transform: AffineTransform::default(), + stops: vec![ + GradientStop { + offset: 0.0, + color: PURPLE_LIGHT, + }, + GradientStop { + offset: 1.0, + color: BLUE_ELECTRIC, + }, + ], + opacity: 1.0, + blend_mode: BlendMode::Normal, + })]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: SingularStrokeWidth(None), + layout_child: None, + }), + )); // ── 9. 4-point star — small accent, left side ─────────────────────── let star_id = next_id(); - nodes.push((star_id, Node::RegularStarPolygon(RegularStarPolygonNodeRec { - active: true, - opacity: 0.35, - blend_mode: LayerBlendMode::Blend(BlendMode::Screen), - mask: None, - effects: LayerEffects::default(), - transform: AffineTransform::from_box_center(80.0, 680.0, 120.0, 120.0, 22.0), - size: Size { width: 120.0, height: 120.0 }, - point_count: 4, - inner_radius: 0.35, - corner_radius: 4.0, - fills: Paints::new(vec![Paint::Solid(SolidPaint { + nodes.push(( + star_id, + Node::RegularStarPolygon(RegularStarPolygonNodeRec { active: true, - color: AMBER_WARM, - blend_mode: BlendMode::Normal, - })]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: SingularStrokeWidth(None), - layout_child: None, - }))); + opacity: 0.35, + blend_mode: LayerBlendMode::Blend(BlendMode::Screen), + mask: None, + effects: LayerEffects::default(), + transform: AffineTransform::from_box_center(80.0, 680.0, 120.0, 120.0, 22.0), + size: Size { + width: 120.0, + height: 120.0, + }, + point_count: 4, + inner_radius: 0.35, + corner_radius: 4.0, + fills: Paints::new(vec![Paint::Solid(SolidPaint { + active: true, + color: AMBER_WARM, + blend_mode: BlendMode::Normal, + })]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: SingularStrokeWidth(None), + layout_child: None, + }), + )); // ── 10. Thin decorative lines — horizontal rule accents ───────────── let line1_id = next_id(); - nodes.push((line1_id, rect(120.0, 800.0, 400.0, 1.0, Paint::Solid(SolidPaint { - active: true, - color: WHITE_DIM, - blend_mode: BlendMode::Normal, - })))); + nodes.push(( + line1_id, + rect( + 120.0, + 800.0, + 400.0, + 1.0, + Paint::Solid(SolidPaint { + active: true, + color: WHITE_DIM, + blend_mode: BlendMode::Normal, + }), + ), + )); let line2_id = next_id(); - nodes.push((line2_id, rect(1080.0, 800.0, 400.0, 1.0, Paint::Solid(SolidPaint { - active: true, - color: WHITE_FAINT, - blend_mode: BlendMode::Normal, - })))); + nodes.push(( + line2_id, + rect( + 1080.0, + 800.0, + 400.0, + 1.0, + Paint::Solid(SolidPaint { + active: true, + color: WHITE_FAINT, + blend_mode: BlendMode::Normal, + }), + ), + )); // ── 11. "cg" typography — large, with gradient fill ───────────────── let text_id = next_id(); - nodes.push((text_id, Node::TextSpan(TextSpanNodeRec { - active: true, - transform: AffineTransform::new(120.0, 790.0, 0.0), - width: None, - height: None, - layout_child: None, - text: "cg".to_owned(), - text_style: { - let mut ts = TextStyleRec::from_font("Geist", 72.0); - ts.font_weight = FontWeight(800); - ts.letter_spacing = TextLetterSpacing::Fixed(-2.0); - ts - }, - text_align: TextAlign::Left, - text_align_vertical: TextAlignVertical::Top, - max_lines: None, - ellipsis: None, - fills: Paints::new(vec![ - Paint::LinearGradient(LinearGradientPaint { + nodes.push(( + text_id, + Node::TextSpan(TextSpanNodeRec { + active: true, + transform: AffineTransform::new(120.0, 790.0, 0.0), + width: None, + height: None, + layout_child: None, + text: "cg".to_owned(), + text_style: { + let mut ts = TextStyleRec::from_font("Geist", 72.0); + ts.font_weight = FontWeight(800); + ts.letter_spacing = TextLetterSpacing::Fixed(-2.0); + ts + }, + text_align: TextAlign::Left, + text_align_vertical: TextAlignVertical::Top, + max_lines: None, + ellipsis: None, + fills: Paints::new(vec![Paint::LinearGradient(LinearGradientPaint { active: true, xy1: Alignment(-1.0, 0.0), xy2: Alignment(1.0, 0.0), tile_mode: TileMode::default(), transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CGColor { r: 255, g: 255, b: 255, a: 180 } }, - GradientStop { offset: 1.0, color: CGColor { r: 255, g: 255, b: 255, a: 60 } }, + GradientStop { + offset: 0.0, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 180, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 60, + }, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, - }), - ]), - strokes: Paints::new(vec![]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - effects: LayerEffects::default(), - }))); + })]), + strokes: Paints::new(vec![]), + stroke_width: 0.0, + stroke_align: StrokeAlign::Center, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + effects: LayerEffects::default(), + }), + )); // ── 12. Subtitle text ─────────────────────────────────────────────── let sub_id = next_id(); - nodes.push((sub_id, Node::TextSpan(TextSpanNodeRec { - active: true, - transform: AffineTransform::new(240.0, 815.0, 0.0), - width: None, - height: None, - layout_child: None, - text: "grida canvas".to_owned(), - text_style: { - let mut ts = TextStyleRec::from_font("Geist", 20.0); - ts.font_weight = FontWeight(400); - ts.letter_spacing = TextLetterSpacing::Fixed(6.0); - ts.text_transform = TextTransform::Uppercase; - ts - }, - text_align: TextAlign::Left, - text_align_vertical: TextAlignVertical::Top, - max_lines: None, - ellipsis: None, - fills: Paints::new(vec![Paint::Solid(SolidPaint { + nodes.push(( + sub_id, + Node::TextSpan(TextSpanNodeRec { active: true, - color: CGColor { r: 255, g: 255, b: 255, a: 80 }, - blend_mode: BlendMode::Normal, - })]), - strokes: Paints::new(vec![]), - stroke_width: 0.0, - stroke_align: StrokeAlign::Center, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - effects: LayerEffects::default(), - }))); + transform: AffineTransform::new(240.0, 815.0, 0.0), + width: None, + height: None, + layout_child: None, + text: "grida canvas".to_owned(), + text_style: { + let mut ts = TextStyleRec::from_font("Geist", 20.0); + ts.font_weight = FontWeight(400); + ts.letter_spacing = TextLetterSpacing::Fixed(6.0); + ts.text_transform = TextTransform::Uppercase; + ts + }, + text_align: TextAlign::Left, + text_align_vertical: TextAlignVertical::Top, + max_lines: None, + ellipsis: None, + fills: Paints::new(vec![Paint::Solid(SolidPaint { + active: true, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 80, + }, + blend_mode: BlendMode::Normal, + })]), + strokes: Paints::new(vec![]), + stroke_width: 0.0, + stroke_align: StrokeAlign::Center, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + effects: LayerEffects::default(), + }), + )); // ── 13. Floating small circles (decorative particles) ─────────────── let particle_positions: &[(f32, f32, f32, f32)] = &[ @@ -468,61 +765,83 @@ pub fn build() -> Scene { for &(px, py, ps, po) in particle_positions { let pid = next_id(); - nodes.push((pid, Node::Ellipse(EllipseNodeRec { - active: true, - opacity: po, - blend_mode: LayerBlendMode::Blend(BlendMode::Screen), - mask: None, - transform: AffineTransform::from_box_center(px, py, ps, ps, 0.0), - size: Size { width: ps, height: ps }, - fills: Paints::new(vec![Paint::Solid(SolidPaint { + nodes.push(( + pid, + Node::Ellipse(EllipseNodeRec { active: true, - color: CGColor { r: 255, g: 255, b: 255, a: 255 }, - blend_mode: BlendMode::Normal, - })]), - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: SingularStrokeWidth(None), - inner_radius: None, - start_angle: 0.0, - angle: None, - corner_radius: None, - effects: LayerEffects::default(), - layout_child: None, - }))); + opacity: po, + blend_mode: LayerBlendMode::Blend(BlendMode::Screen), + mask: None, + transform: AffineTransform::from_box_center(px, py, ps, ps, 0.0), + size: Size { + width: ps, + height: ps, + }, + fills: Paints::new(vec![Paint::Solid(SolidPaint { + active: true, + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 255, + }, + blend_mode: BlendMode::Normal, + })]), + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: SingularStrokeWidth(None), + inner_radius: None, + start_angle: 0.0, + angle: None, + corner_radius: None, + effects: LayerEffects::default(), + layout_child: None, + }), + )); } // ── 14. Full-canvas noise grain overlay ────────────────────────────── let noise_id = next_id(); - nodes.push((noise_id, Node::Rectangle(RectangleNodeRec { - active: true, - opacity: 1.0, - blend_mode: LayerBlendMode::PassThrough, - mask: None, - transform: AffineTransform::from_box_center(0.0, 0.0, WIDTH, HEIGHT, 0.0), - size: Size { width: WIDTH, height: HEIGHT }, - corner_radius: RectangularCornerRadius::default(), - corner_smoothing: CornerSmoothing(0.0), - fills: Paints::new(vec![]), // transparent — only noise - strokes: Paints::new(vec![]), - stroke_style: StrokeStyle::default(), - stroke_width: StrokeWidth::None, - effects: LayerEffects { - noises: vec![FeNoiseEffect { - active: true, - noise_size: 1.5, - density: 0.35, - num_octaves: 3, - seed: 7.0, - coloring: NoiseEffectColors::Mono { - color: CGColor { r: 255, g: 255, b: 255, a: 18 }, - }, - blend_mode: BlendMode::Normal, - }], - ..LayerEffects::default() - }, - layout_child: None, - }))); + nodes.push(( + noise_id, + Node::Rectangle(RectangleNodeRec { + active: true, + opacity: 1.0, + blend_mode: LayerBlendMode::PassThrough, + mask: None, + transform: AffineTransform::from_box_center(0.0, 0.0, WIDTH, HEIGHT, 0.0), + size: Size { + width: WIDTH, + height: HEIGHT, + }, + corner_radius: RectangularCornerRadius::default(), + corner_smoothing: CornerSmoothing(0.0), + fills: Paints::new(vec![]), // transparent — only noise + strokes: Paints::new(vec![]), + stroke_style: StrokeStyle::default(), + stroke_width: StrokeWidth::None, + effects: LayerEffects { + noises: vec![FeNoiseEffect { + active: true, + noise_size: 1.5, + density: 0.35, + num_octaves: 3, + seed: 7.0, + coloring: NoiseEffectColors::Mono { + color: CGColor { + r: 255, + g: 255, + b: 255, + a: 18, + }, + }, + blend_mode: BlendMode::Normal, + }], + ..LayerEffects::default() + }, + layout_child: None, + }), + )); // ── Assemble ──────────────────────────────────────────────────────── let roots: Vec = nodes.iter().map(|(nid, _)| *nid).collect(); diff --git a/crates/grida-canvas/examples/fixtures/l0_boolean_operation.rs b/crates/grida-canvas/examples/fixtures/l0_boolean_operation.rs index 09555fd66c..9211335919 100644 --- a/crates/grida-canvas/examples/fixtures/l0_boolean_operation.rs +++ b/crates/grida-canvas/examples/fixtures/l0_boolean_operation.rs @@ -47,7 +47,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(0.0, 0.0, 80.0, 80.0, 0.0), - size: Size { width: 80.0, height: 80.0 }, + size: Size { + width: 80.0, + height: 80.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![]), @@ -66,7 +69,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(40.0, 40.0, 80.0, 80.0, 0.0), - size: Size { width: 80.0, height: 80.0 }, + size: Size { + width: 80.0, + height: 80.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![]), diff --git a/crates/grida-canvas/examples/fixtures/l0_container.rs b/crates/grida-canvas/examples/fixtures/l0_container.rs index 7836a533e5..8116cb0bd9 100644 --- a/crates/grida-canvas/examples/fixtures/l0_container.rs +++ b/crates/grida-canvas/examples/fixtures/l0_container.rs @@ -12,21 +12,31 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(200.0), layout_target_height: Some(150.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, corner_radius: { use cg::cg::types::Radius; let r = Radius::circular(12.0); - RectangularCornerRadius { tl: r, tr: r, bl: r, br: r } + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } }, corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(235, 240, 255, 255)]), @@ -48,21 +58,31 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, right: 0.0, bottom: 0.0, left: 240.0, + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 240.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(200.0), layout_target_height: Some(150.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, corner_radius: { use cg::cg::types::Radius; let r = Radius::circular(12.0); - RectangularCornerRadius { tl: r, tr: r, bl: r, br: r } + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } }, corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(235, 255, 240, 255)]), @@ -84,14 +104,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 180.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 180.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(400.0), layout_target_height: Some(250.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -112,21 +137,31 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 20.0, right: 0.0, bottom: 0.0, left: 20.0, + top: 20.0, + right: 0.0, + bottom: 0.0, + left: 20.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(300.0), layout_target_height: Some(180.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, corner_radius: { use cg::cg::types::Radius; let r = Radius::circular(8.0); - RectangularCornerRadius { tl: r, tr: r, bl: r, br: r } + RectangularCornerRadius { + tl: r, + tr: r, + bl: r, + br: r, + } }, corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(220, 230, 255, 255)]), @@ -144,14 +179,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 15.0, right: 0.0, bottom: 0.0, left: 15.0, + top: 15.0, + right: 0.0, + bottom: 0.0, + left: 15.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(200.0), layout_target_height: Some(120.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -170,19 +210,24 @@ pub fn build() -> Scene { // ── Tree ──────────────────────────────────────────────────────────── let mut links = HashMap::new(); - links.insert(1u64, vec![2u64]); // clip_on → overflow_rect - links.insert(3u64, vec![4u64]); // clip_off → overflow_rect2 - links.insert(5u64, vec![6u64]); // outer → middle - links.insert(6u64, vec![7u64]); // middle → inner - links.insert(7u64, vec![8u64]); // inner → leaf + links.insert(1u64, vec![2u64]); // clip_on → overflow_rect + links.insert(3u64, vec![4u64]); // clip_off → overflow_rect2 + links.insert(5u64, vec![6u64]); // outer → middle + links.insert(6u64, vec![7u64]); // middle → inner + links.insert(7u64, vec![8u64]); // inner → leaf build_scene( "L0 Container", None, vec![ - (1, clip_on), (2, overflow_rect), - (3, clip_off), (4, overflow_rect2), - (5, outer), (6, middle), (7, inner), (8, leaf), + (1, clip_on), + (2, overflow_rect), + (3, clip_off), + (4, overflow_rect2), + (5, outer), + (6, middle), + (7, inner), + (8, leaf), ], links, vec![1, 3, 5], diff --git a/crates/grida-canvas/examples/fixtures/l0_effects.rs b/crates/grida-canvas/examples/fixtures/l0_effects.rs index 0ca9c3c534..f7d8562cbc 100644 --- a/crates/grida-canvas/examples/fixtures/l0_effects.rs +++ b/crates/grida-canvas/examples/fixtures/l0_effects.rs @@ -9,7 +9,10 @@ fn effect_rect(x: f32, effects: LayerEffects) -> Node { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, 0.0, 150.0, 150.0, 0.0), - size: Size { width: 150.0, height: 150.0 }, + size: Size { + width: 150.0, + height: 150.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(180, 180, 180, 255)]), @@ -24,54 +27,93 @@ fn effect_rect(x: f32, effects: LayerEffects) -> Node { pub fn build() -> Scene { let gap = 170.0; - let blur = effect_rect(0.0, LayerEffects { - blur: Some(FeLayerBlur { - active: true, - blur: FeBlur::Gaussian(FeGaussianBlur { radius: 5.0 }), - }), - ..LayerEffects::default() - }); + let blur = effect_rect( + 0.0, + LayerEffects { + blur: Some(FeLayerBlur { + active: true, + blur: FeBlur::Gaussian(FeGaussianBlur { radius: 5.0 }), + }), + ..LayerEffects::default() + }, + ); - let backdrop = effect_rect(gap, LayerEffects { - backdrop_blur: Some(FeBackdropBlur { - active: true, - blur: FeBlur::Gaussian(FeGaussianBlur { radius: 8.0 }), - }), - ..LayerEffects::default() - }); + let backdrop = effect_rect( + gap, + LayerEffects { + backdrop_blur: Some(FeBackdropBlur { + active: true, + blur: FeBlur::Gaussian(FeGaussianBlur { radius: 8.0 }), + }), + ..LayerEffects::default() + }, + ); - let drop_shadow = effect_rect(gap * 2.0, LayerEffects { - shadows: vec![FilterShadowEffect::DropShadow(FeShadow { - dx: 2.0, dy: 2.0, blur: 4.0, spread: 0.0, - color: CGColor { r: 0, g: 0, b: 0, a: 255 }, - active: true, - })], - ..LayerEffects::default() - }); + let drop_shadow = effect_rect( + gap * 2.0, + LayerEffects { + shadows: vec![FilterShadowEffect::DropShadow(FeShadow { + dx: 2.0, + dy: 2.0, + blur: 4.0, + spread: 0.0, + color: CGColor { + r: 0, + g: 0, + b: 0, + a: 255, + }, + active: true, + })], + ..LayerEffects::default() + }, + ); - let inner_shadow = effect_rect(gap * 3.0, LayerEffects { - shadows: vec![FilterShadowEffect::InnerShadow(FeShadow { - dx: 1.0, dy: 1.0, blur: 3.0, spread: 0.0, - color: CGColor { r: 128, g: 128, b: 128, a: 200 }, - active: true, - })], - ..LayerEffects::default() - }); + let inner_shadow = effect_rect( + gap * 3.0, + LayerEffects { + shadows: vec![FilterShadowEffect::InnerShadow(FeShadow { + dx: 1.0, + dy: 1.0, + blur: 3.0, + spread: 0.0, + color: CGColor { + r: 128, + g: 128, + b: 128, + a: 200, + }, + active: true, + })], + ..LayerEffects::default() + }, + ); - let noise = effect_rect(gap * 4.0, LayerEffects { - noises: vec![FeNoiseEffect { - active: true, - noise_size: 1.0, - density: 0.3, - num_octaves: 4, - seed: 42.0, - coloring: NoiseEffectColors::Mono { - color: CGColor { r: 0, g: 0, b: 0, a: 64 }, - }, - blend_mode: BlendMode::Normal, - }], - ..LayerEffects::default() - }); + let noise = effect_rect( + gap * 4.0, + LayerEffects { + noises: vec![FeNoiseEffect { + active: true, + noise_size: 1.0, + density: 0.3, + num_octaves: 4, + seed: 42.0, + coloring: NoiseEffectColors::Mono { + color: CGColor { + r: 0, + g: 0, + b: 0, + a: 64, + }, + }, + blend_mode: BlendMode::Normal, + }], + ..LayerEffects::default() + }, + ); - flat_scene("L0 Effects", vec![blur, backdrop, drop_shadow, inner_shadow, noise]) + flat_scene( + "L0 Effects", + vec![blur, backdrop, drop_shadow, inner_shadow, noise], + ) } diff --git a/crates/grida-canvas/examples/fixtures/l0_effects_glass.rs b/crates/grida-canvas/examples/fixtures/l0_effects_glass.rs index 5a82b05e3a..e4c47e7e49 100644 --- a/crates/grida-canvas/examples/fixtures/l0_effects_glass.rs +++ b/crates/grida-canvas/examples/fixtures/l0_effects_glass.rs @@ -34,10 +34,11 @@ pub fn build() -> Scene { opacity: 1.0, blend_mode: LayerBlendMode::PassThrough, mask: None, - transform: AffineTransform::from_box_center( - padding, padding, glass_size, glass_size, 0.0, - ), - size: Size { width: glass_size, height: glass_size }, + transform: AffineTransform::from_box_center(padding, padding, glass_size, glass_size, 0.0), + size: Size { + width: glass_size, + height: glass_size, + }, corner_radius: RectangularCornerRadius::circular(60.0), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![]), diff --git a/crates/grida-canvas/examples/fixtures/l0_effects_progressive_blur.rs b/crates/grida-canvas/examples/fixtures/l0_effects_progressive_blur.rs index 72f1eb463e..6dd7dbb02e 100644 --- a/crates/grida-canvas/examples/fixtures/l0_effects_progressive_blur.rs +++ b/crates/grida-canvas/examples/fixtures/l0_effects_progressive_blur.rs @@ -8,7 +8,10 @@ fn effect_rect(x: f32, effects: LayerEffects) -> Node { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, 0.0, 200.0, 200.0, 0.0), - size: Size { width: 200.0, height: 200.0 }, + size: Size { + width: 200.0, + height: 200.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(180, 180, 180, 255)]), @@ -24,46 +27,55 @@ pub fn build() -> Scene { let gap = 220.0; // Layer progressive blur: top-left clear → bottom-right blurred - let layer_tl_br = effect_rect(0.0, LayerEffects { - blur: Some(FeLayerBlur { - active: true, - blur: FeBlur::Progressive(FeProgressiveBlur { - start: Alignment::TOP_LEFT, - end: Alignment::BOTTOM_RIGHT, - radius: 0.0, - radius2: 20.0, + let layer_tl_br = effect_rect( + 0.0, + LayerEffects { + blur: Some(FeLayerBlur { + active: true, + blur: FeBlur::Progressive(FeProgressiveBlur { + start: Alignment::TOP_LEFT, + end: Alignment::BOTTOM_RIGHT, + radius: 0.0, + radius2: 20.0, + }), }), - }), - ..LayerEffects::default() - }); + ..LayerEffects::default() + }, + ); // Layer progressive blur: top clear → bottom blurred (vertical gradient) - let layer_top_bottom = effect_rect(gap, LayerEffects { - blur: Some(FeLayerBlur { - active: true, - blur: FeBlur::Progressive(FeProgressiveBlur { - start: Alignment::TOP_CENTER, - end: Alignment::BOTTOM_CENTER, - radius: 0.0, - radius2: 30.0, + let layer_top_bottom = effect_rect( + gap, + LayerEffects { + blur: Some(FeLayerBlur { + active: true, + blur: FeBlur::Progressive(FeProgressiveBlur { + start: Alignment::TOP_CENTER, + end: Alignment::BOTTOM_CENTER, + radius: 0.0, + radius2: 30.0, + }), }), - }), - ..LayerEffects::default() - }); + ..LayerEffects::default() + }, + ); // Layer progressive blur: both ends blurred (non-zero start radius) - let layer_both = effect_rect(gap * 2.0, LayerEffects { - blur: Some(FeLayerBlur { - active: true, - blur: FeBlur::Progressive(FeProgressiveBlur { - start: Alignment::CENTER_LEFT, - end: Alignment::CENTER_RIGHT, - radius: 5.0, - radius2: 25.0, + let layer_both = effect_rect( + gap * 2.0, + LayerEffects { + blur: Some(FeLayerBlur { + active: true, + blur: FeBlur::Progressive(FeProgressiveBlur { + start: Alignment::CENTER_LEFT, + end: Alignment::CENTER_RIGHT, + radius: 5.0, + radius2: 25.0, + }), }), - }), - ..LayerEffects::default() - }); + ..LayerEffects::default() + }, + ); // Background content behind the backdrop card so FeBackdropBlur samples // real pixels. Uses a contrasting fill to make the blur visually apparent. @@ -73,7 +85,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(gap * 3.0, 0.0, 200.0, 200.0, 0.0), - size: Size { width: 200.0, height: 200.0 }, + size: Size { + width: 200.0, + height: 200.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(220, 59, 59, 255)]), @@ -85,21 +100,30 @@ pub fn build() -> Scene { }); // Backdrop progressive blur: center clear → edges blurred - let backdrop = effect_rect(gap * 3.0, LayerEffects { - backdrop_blur: Some(FeBackdropBlur { - active: true, - blur: FeBlur::Progressive(FeProgressiveBlur { - start: Alignment::CENTER, - end: Alignment::BOTTOM_RIGHT, - radius: 0.0, - radius2: 15.0, + let backdrop = effect_rect( + gap * 3.0, + LayerEffects { + backdrop_blur: Some(FeBackdropBlur { + active: true, + blur: FeBlur::Progressive(FeProgressiveBlur { + start: Alignment::CENTER, + end: Alignment::BOTTOM_RIGHT, + radius: 0.0, + radius2: 15.0, + }), }), - }), - ..LayerEffects::default() - }); + ..LayerEffects::default() + }, + ); flat_scene( "L0 Effects Progressive Blur", - vec![layer_tl_br, layer_top_bottom, layer_both, backdrop_bg, backdrop], + vec![ + layer_tl_br, + layer_top_bottom, + layer_both, + backdrop_bg, + backdrop, + ], ) } diff --git a/crates/grida-canvas/examples/fixtures/l0_group.rs b/crates/grida-canvas/examples/fixtures/l0_group.rs index 185150bc9c..9b6f910506 100644 --- a/crates/grida-canvas/examples/fixtures/l0_group.rs +++ b/crates/grida-canvas/examples/fixtures/l0_group.rs @@ -62,20 +62,30 @@ pub fn build() -> Scene { // ── Tree ──────────────────────────────────────────────────────────── let mut links = HashMap::new(); - links.insert(1u64, vec![2, 3]); // g1 → children - links.insert(4u64, vec![5, 6]); // g2 → children - links.insert(7u64, vec![8, 9]); // g3 → children - links.insert(10u64, vec![11, 14]); // g_outer → g_inner + sibling - links.insert(11u64, vec![12, 13]); // g_inner → leaves + links.insert(1u64, vec![2, 3]); // g1 → children + links.insert(4u64, vec![5, 6]); // g2 → children + links.insert(7u64, vec![8, 9]); // g3 → children + links.insert(10u64, vec![11, 14]); // g_outer → g_inner + sibling + links.insert(11u64, vec![12, 13]); // g_inner → leaves build_scene( "L0 Group", None, vec![ - (1, g1), (2, g1_a), (3, g1_b), - (4, g2), (5, g2_a), (6, g2_b), - (7, g3), (8, g3_a), (9, g3_b), - (10, g_outer), (11, g_inner), (12, leaf1), (13, leaf2), (14, sibling), + (1, g1), + (2, g1_a), + (3, g1_b), + (4, g2), + (5, g2_a), + (6, g2_b), + (7, g3), + (8, g3_a), + (9, g3_b), + (10, g_outer), + (11, g_inner), + (12, leaf1), + (13, leaf2), + (14, sibling), ], links, vec![1, 4, 7, 10], diff --git a/crates/grida-canvas/examples/fixtures/l0_image.rs b/crates/grida-canvas/examples/fixtures/l0_image.rs index 439cf6848f..7202b7f777 100644 --- a/crates/grida-canvas/examples/fixtures/l0_image.rs +++ b/crates/grida-canvas/examples/fixtures/l0_image.rs @@ -9,19 +9,41 @@ pub fn build() -> Scene { let img = || ResourceRef::HASH(SYSTEM_IMAGE.to_owned()); // Cover fit - let r1 = rect(0.0, 0.0, s, s, - image_paint_with(img(), ImagePaintFit::Fit(BoxFit::Cover))); + let r1 = rect( + 0.0, + 0.0, + s, + s, + image_paint_with(img(), ImagePaintFit::Fit(BoxFit::Cover)), + ); // Contain fit - let r2 = rect(gap, 0.0, s, s, - image_paint_with(img(), ImagePaintFit::Fit(BoxFit::Contain))); + let r2 = rect( + gap, + 0.0, + s, + s, + image_paint_with(img(), ImagePaintFit::Fit(BoxFit::Contain)), + ); // Transform fit (scale + offset) - let r3 = rect(gap * 2.0, 0.0, s, s, - image_paint_with(img(), ImagePaintFit::Transform(AffineTransform::new(10.0, 20.0, 0.0)))); + let r3 = rect( + gap * 2.0, + 0.0, + s, + s, + image_paint_with( + img(), + ImagePaintFit::Transform(AffineTransform::new(10.0, 20.0, 0.0)), + ), + ); // Quarter turns + alignment + Screen blend - let r4 = rect(gap * 3.0, 0.0, s, s, + let r4 = rect( + gap * 3.0, + 0.0, + s, + s, Paint::Image(ImagePaint { active: true, image: img(), @@ -31,7 +53,8 @@ pub fn build() -> Scene { blend_mode: BlendMode::Screen, quarter_turns: 2, alignement: Alignment::BOTTOM_RIGHT, - })); + }), + ); flat_scene("L0 Image", vec![r1, r2, r3, r4]) } diff --git a/crates/grida-canvas/examples/fixtures/l0_image_filters.rs b/crates/grida-canvas/examples/fixtures/l0_image_filters.rs index 5912556953..02028afa34 100644 --- a/crates/grida-canvas/examples/fixtures/l0_image_filters.rs +++ b/crates/grida-canvas/examples/fixtures/l0_image_filters.rs @@ -8,73 +8,100 @@ pub fn build() -> Scene { let gap = 170.0; // Bright + warm - let bright_warm = rect(0.0, 0.0, s, s, Paint::Image(ImagePaint { - active: true, - image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), - fit: ImagePaintFit::Fit(BoxFit::Cover), - filters: ImageFilters { - exposure: 0.5, - temperature: 0.4, - ..ImageFilters::default() - }, - opacity: 1.0, - blend_mode: BlendMode::Normal, - quarter_turns: 0, - alignement: Alignment::CENTER, - })); + let bright_warm = rect( + 0.0, + 0.0, + s, + s, + Paint::Image(ImagePaint { + active: true, + image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), + fit: ImagePaintFit::Fit(BoxFit::Cover), + filters: ImageFilters { + exposure: 0.5, + temperature: 0.4, + ..ImageFilters::default() + }, + opacity: 1.0, + blend_mode: BlendMode::Normal, + quarter_turns: 0, + alignement: Alignment::CENTER, + }), + ); // High contrast + desaturated - let contrast_desat = rect(gap, 0.0, s, s, Paint::Image(ImagePaint { - active: true, - image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), - fit: ImagePaintFit::Fit(BoxFit::Cover), - filters: ImageFilters { - contrast: 0.25, - saturation: -0.8, - ..ImageFilters::default() - }, - opacity: 1.0, - blend_mode: BlendMode::Normal, - quarter_turns: 0, - alignement: Alignment::CENTER, - })); + let contrast_desat = rect( + gap, + 0.0, + s, + s, + Paint::Image(ImagePaint { + active: true, + image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), + fit: ImagePaintFit::Fit(BoxFit::Cover), + filters: ImageFilters { + contrast: 0.25, + saturation: -0.8, + ..ImageFilters::default() + }, + opacity: 1.0, + blend_mode: BlendMode::Normal, + quarter_turns: 0, + alignement: Alignment::CENTER, + }), + ); // Cool tint + shadow lift - let cool_shadow = rect(gap * 2.0, 0.0, s, s, Paint::Image(ImagePaint { - active: true, - image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), - fit: ImagePaintFit::Fit(BoxFit::Cover), - filters: ImageFilters { - temperature: -0.6, - tint: -0.3, - shadows: 0.5, - ..ImageFilters::default() - }, - opacity: 1.0, - blend_mode: BlendMode::Normal, - quarter_turns: 0, - alignement: Alignment::CENTER, - })); + let cool_shadow = rect( + gap * 2.0, + 0.0, + s, + s, + Paint::Image(ImagePaint { + active: true, + image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), + fit: ImagePaintFit::Fit(BoxFit::Cover), + filters: ImageFilters { + temperature: -0.6, + tint: -0.3, + shadows: 0.5, + ..ImageFilters::default() + }, + opacity: 1.0, + blend_mode: BlendMode::Normal, + quarter_turns: 0, + alignement: Alignment::CENTER, + }), + ); // All filters non-zero - let all_filters = rect(gap * 3.0, 0.0, s, s, Paint::Image(ImagePaint { - active: true, - image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), - fit: ImagePaintFit::Fit(BoxFit::Cover), - filters: ImageFilters { - exposure: 0.2, - contrast: 0.1, - saturation: 0.3, - temperature: -0.2, - tint: 0.15, - highlights: -0.4, - shadows: 0.3, - }, - opacity: 1.0, - blend_mode: BlendMode::Normal, - quarter_turns: 0, - alignement: Alignment::CENTER, - })); + let all_filters = rect( + gap * 3.0, + 0.0, + s, + s, + Paint::Image(ImagePaint { + active: true, + image: ResourceRef::HASH(SYSTEM_IMAGE.to_owned()), + fit: ImagePaintFit::Fit(BoxFit::Cover), + filters: ImageFilters { + exposure: 0.2, + contrast: 0.1, + saturation: 0.3, + temperature: -0.2, + tint: 0.15, + highlights: -0.4, + shadows: 0.3, + }, + opacity: 1.0, + blend_mode: BlendMode::Normal, + quarter_turns: 0, + alignement: Alignment::CENTER, + }), + ); - flat_scene("L0 Image Filters", vec![bright_warm, contrast_desat, cool_shadow, all_filters]) + flat_scene( + "L0 Image Filters", + vec![bright_warm, contrast_desat, cool_shadow, all_filters], + ) } diff --git a/crates/grida-canvas/examples/fixtures/l0_layout_flex.rs b/crates/grida-canvas/examples/fixtures/l0_layout_flex.rs index 185d531916..5749ba1bfd 100644 --- a/crates/grida-canvas/examples/fixtures/l0_layout_flex.rs +++ b/crates/grida-canvas/examples/fixtures/l0_layout_flex.rs @@ -10,7 +10,10 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle { layout_mode: LayoutMode::Flex, @@ -19,13 +22,18 @@ pub fn build() -> Scene { layout_main_axis_alignment: Some(MainAxisAlignment::SpaceBetween), layout_cross_axis_alignment: Some(CrossAxisAlignment::Center), layout_padding: None, - layout_gap: Some(LayoutGap { main_axis_gap: 10.0, cross_axis_gap: 0.0 }), + layout_gap: Some(LayoutGap { + main_axis_gap: 10.0, + cross_axis_gap: 0.0, + }), }, layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(500.0), layout_target_height: Some(80.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -48,7 +56,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(0.0, 0.0, 100.0, 50.0, 0.0), - size: Size { width: 100.0, height: 50.0 }, + size: Size { + width: 100.0, + height: 50.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(59, 180, 75, 255)]), @@ -70,7 +81,10 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 100.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 100.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle { layout_mode: LayoutMode::Flex, @@ -79,15 +93,23 @@ pub fn build() -> Scene { layout_main_axis_alignment: Some(MainAxisAlignment::Center), layout_cross_axis_alignment: Some(CrossAxisAlignment::Stretch), layout_padding: Some(EdgeInsets { - top: 16.0, right: 16.0, bottom: 16.0, left: 16.0, + top: 16.0, + right: 16.0, + bottom: 16.0, + left: 16.0, + }), + layout_gap: Some(LayoutGap { + main_axis_gap: 8.0, + cross_axis_gap: 0.0, }), - layout_gap: Some(LayoutGap { main_axis_gap: 8.0, cross_axis_gap: 0.0 }), }, layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(200.0), layout_target_height: Some(200.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -112,8 +134,13 @@ pub fn build() -> Scene { "L0 Layout Flex", None, vec![ - (1, h_container), (2, h1), (3, h2), (4, h3), - (5, v_container), (6, v1), (7, v2), + (1, h_container), + (2, h1), + (3, h2), + (4, h3), + (5, v_container), + (6, v1), + (7, v2), ], links, vec![1, 5], diff --git a/crates/grida-canvas/examples/fixtures/l0_layout_position.rs b/crates/grida-canvas/examples/fixtures/l0_layout_position.rs index 121e2c10c0..4d24d204e1 100644 --- a/crates/grida-canvas/examples/fixtures/l0_layout_position.rs +++ b/crates/grida-canvas/examples/fixtures/l0_layout_position.rs @@ -9,14 +9,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle::default(), // Normal mode layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(400.0), layout_target_height: Some(300.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -43,7 +48,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(20.0, 150.0, 80.0, 60.0, 0.0), - size: Size { width: 80.0, height: 60.0 }, + size: Size { + width: 80.0, + height: 60.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(59, 180, 75, 255)]), diff --git a/crates/grida-canvas/examples/fixtures/l0_layout_transform.rs b/crates/grida-canvas/examples/fixtures/l0_layout_transform.rs index 38fcf91a7c..3f46182992 100644 --- a/crates/grida-canvas/examples/fixtures/l0_layout_transform.rs +++ b/crates/grida-canvas/examples/fixtures/l0_layout_transform.rs @@ -34,7 +34,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(0.0, 100.0, 120.0, 60.0, 30.0), - size: Size { width: 120.0, height: 60.0 }, + size: Size { + width: 120.0, + height: 60.0, + }, fills: Paints::new(vec![solid(59, 100, 220, 200)]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -63,7 +66,10 @@ pub fn build() -> Scene { mask: None, // from_box_center uses origin (0.5, 0.5) transform: AffineTransform::from_box_center(0.0, 220.0, 100.0, 60.0, 45.0), - size: Size { width: 100.0, height: 60.0 }, + size: Size { + width: 100.0, + height: 60.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(220, 59, 59, 180)]), @@ -81,7 +87,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box(180.0, 220.0, 100.0, 60.0, 45.0, 0.0, 0.0), - size: Size { width: 100.0, height: 60.0 }, + size: Size { + width: 100.0, + height: 60.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(59, 100, 220, 180)]), @@ -99,7 +108,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box(360.0, 220.0, 100.0, 60.0, 45.0, 1.0, 1.0), - size: Size { width: 100.0, height: 60.0 }, + size: Size { + width: 100.0, + height: 60.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(59, 180, 75, 180)]), @@ -124,8 +136,10 @@ pub fn build() -> Scene { layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(200.0), layout_target_height: Some(120.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -156,8 +170,10 @@ pub fn build() -> Scene { layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(200.0), layout_target_height: Some(120.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -185,14 +201,19 @@ pub fn build() -> Scene { mask: None, rotation: 45.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 360.0, right: 0.0, bottom: 0.0, left: 600.0, + top: 360.0, + right: 0.0, + bottom: 0.0, + left: 600.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(120.0), layout_target_height: Some(120.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -237,9 +258,9 @@ pub fn build() -> Scene { ]; let mut links: HashMap> = HashMap::new(); - links.insert(13, vec![14]); // container 90° → child - links.insert(15, vec![16]); // container 30° → child rotated 45° - links.insert(17, vec![18]); // container 45° inset → child + links.insert(13, vec![14]); // container 90° → child + links.insert(15, vec![16]); // container 30° → child rotated 45° + links.insert(17, vec![18]); // container 45° inset → child build_scene( "L0 Layout Transform", diff --git a/crates/grida-canvas/examples/fixtures/l0_masks.rs b/crates/grida-canvas/examples/fixtures/l0_masks.rs index b4d64aa926..da3e64768d 100644 --- a/crates/grida-canvas/examples/fixtures/l0_masks.rs +++ b/crates/grida-canvas/examples/fixtures/l0_masks.rs @@ -19,7 +19,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: Some(LayerMaskType::Image(ImageMaskType::Alpha)), transform: AffineTransform::from_box_center(0.0, 0.0, 100.0, 100.0, 0.0), - size: Size { width: 100.0, height: 100.0 }, + size: Size { + width: 100.0, + height: 100.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(255, 255, 255, 255)]), @@ -45,7 +48,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: Some(LayerMaskType::Geometry), transform: AffineTransform::from_box_center(10.0, 10.0, 80.0, 80.0, 0.0), - size: Size { width: 80.0, height: 80.0 }, + size: Size { + width: 80.0, + height: 80.0, + }, fills: Paints::new(vec![solid(0, 0, 0, 255)]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -66,8 +72,12 @@ pub fn build() -> Scene { "L0 Masks", None, vec![ - (1, group_img), (2, content_img), (3, mask_img), - (4, group_geo), (5, content_geo), (6, mask_geo), + (1, group_img), + (2, content_img), + (3, mask_img), + (4, group_geo), + (5, content_geo), + (6, mask_geo), ], links, vec![1, 4], diff --git a/crates/grida-canvas/examples/fixtures/l0_paints.rs b/crates/grida-canvas/examples/fixtures/l0_paints.rs index 4c54a0d563..e24f230dac 100644 --- a/crates/grida-canvas/examples/fixtures/l0_paints.rs +++ b/crates/grida-canvas/examples/fixtures/l0_paints.rs @@ -10,5 +10,8 @@ pub fn build() -> Scene { let sweep_rect = rect(gap * 3.0, 0.0, s, s, sweep_gradient()); let image_rect = rect(gap * 4.0, 0.0, s, s, image_paint()); - flat_scene("L0 Paints", vec![solid_rect, linear_rect, radial_rect, sweep_rect, image_rect]) + flat_scene( + "L0 Paints", + vec![solid_rect, linear_rect, radial_rect, sweep_rect, image_rect], + ) } diff --git a/crates/grida-canvas/examples/fixtures/l0_paints_stack.rs b/crates/grida-canvas/examples/fixtures/l0_paints_stack.rs index 1929dea919..34d5205df1 100644 --- a/crates/grida-canvas/examples/fixtures/l0_paints_stack.rs +++ b/crates/grida-canvas/examples/fixtures/l0_paints_stack.rs @@ -13,7 +13,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(0.0, 0.0, 200.0, 200.0, 0.0), - size: Size { width: 200.0, height: 200.0 }, + size: Size { + width: 200.0, + height: 200.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![ @@ -27,8 +30,24 @@ pub fn build() -> Scene { tile_mode: TileMode::default(), transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CGColor { r: 255, g: 0, b: 0, a: 255 } }, - GradientStop { offset: 1.0, color: CGColor { r: 255, g: 0, b: 0, a: 0 } }, + GradientStop { + offset: 0.0, + color: CGColor { + r: 255, + g: 0, + b: 0, + a: 255, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 255, + g: 0, + b: 0, + a: 0, + }, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, @@ -38,8 +57,24 @@ pub fn build() -> Scene { active: true, transform: AffineTransform::default(), stops: vec![ - GradientStop { offset: 0.0, color: CGColor { r: 255, g: 255, b: 0, a: 255 } }, - GradientStop { offset: 1.0, color: CGColor { r: 255, g: 255, b: 0, a: 0 } }, + GradientStop { + offset: 0.0, + color: CGColor { + r: 255, + g: 255, + b: 0, + a: 255, + }, + }, + GradientStop { + offset: 1.0, + color: CGColor { + r: 255, + g: 255, + b: 0, + a: 0, + }, + }, ], opacity: 1.0, blend_mode: BlendMode::Normal, diff --git a/crates/grida-canvas/examples/fixtures/l0_shape_arc.rs b/crates/grida-canvas/examples/fixtures/l0_shape_arc.rs index b978e50d19..0dec7101be 100644 --- a/crates/grida-canvas/examples/fixtures/l0_shape_arc.rs +++ b/crates/grida-canvas/examples/fixtures/l0_shape_arc.rs @@ -14,7 +14,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(0.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, fills: Paints::new(vec![solid(59, 100, 220, 255)]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -34,7 +37,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(gap, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, fills: Paints::new(vec![solid(220, 59, 59, 255)]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -54,7 +60,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(gap * 2.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, fills: Paints::new(vec![solid(59, 180, 75, 255)]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -74,7 +83,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(gap * 3.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, fills: Paints::new(vec![solid(255, 200, 40, 255)]), strokes: Paints::new(vec![]), stroke_style: StrokeStyle::default(), @@ -94,7 +106,10 @@ pub fn build() -> Scene { blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(gap * 4.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, fills: Paints::new(vec![solid(128, 60, 200, 255)]), strokes: Paints::new(vec![solid(0, 0, 0, 255)]), stroke_style: StrokeStyle::default(), diff --git a/crates/grida-canvas/examples/fixtures/l0_shape_polygon.rs b/crates/grida-canvas/examples/fixtures/l0_shape_polygon.rs index 75f5fd30ed..c268117e14 100644 --- a/crates/grida-canvas/examples/fixtures/l0_shape_polygon.rs +++ b/crates/grida-canvas/examples/fixtures/l0_shape_polygon.rs @@ -15,7 +15,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(0.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 3, corner_radius: 0.0, fills: Paints::new(vec![solid(220, 59, 59, 255)]), @@ -33,7 +36,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(gap, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 5, corner_radius: 10.0, fills: Paints::new(vec![solid(59, 100, 220, 255)]), @@ -51,7 +57,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(gap * 2.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 6, corner_radius: 5.0, fills: Paints::new(vec![solid(59, 180, 75, 255)]), @@ -69,7 +78,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(gap * 3.0, 0.0, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 8, corner_radius: 0.0, fills: Paints::new(vec![solid(255, 200, 40, 255)]), @@ -87,7 +99,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(0.0, gap, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 4, inner_radius: 0.3, corner_radius: 0.0, @@ -106,7 +121,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(gap, gap, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 5, inner_radius: 0.4, corner_radius: 3.0, @@ -125,7 +143,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(gap * 2.0, gap, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 6, inner_radius: 0.5, corner_radius: 0.0, @@ -144,7 +165,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(gap * 3.0, gap, s, s, 0.0), - size: Size { width: s, height: s }, + size: Size { + width: s, + height: s, + }, point_count: 8, inner_radius: 0.7, corner_radius: 6.0, diff --git a/crates/grida-canvas/examples/fixtures/l0_shapes.rs b/crates/grida-canvas/examples/fixtures/l0_shapes.rs index 444289ca52..2b7059d0ed 100644 --- a/crates/grida-canvas/examples/fixtures/l0_shapes.rs +++ b/crates/grida-canvas/examples/fixtures/l0_shapes.rs @@ -14,7 +14,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(340.0, 0.0, 100.0, 100.0, 0.0), - size: Size { width: 100.0, height: 100.0 }, + size: Size { + width: 100.0, + height: 100.0, + }, point_count: 6, corner_radius: 5.0, fills: Paints::new(vec![solid(59, 180, 75, 255)]), @@ -31,7 +34,10 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects::default(), transform: AffineTransform::from_box_center(460.0, 0.0, 120.0, 120.0, 0.0), - size: Size { width: 120.0, height: 120.0 }, + size: Size { + width: 120.0, + height: 120.0, + }, point_count: 5, inner_radius: 0.4, corner_radius: 3.0, @@ -56,10 +62,30 @@ pub fn build() -> Scene { network: VectorNetwork { vertices: vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)], segments: vec![ - VectorNetworkSegment { a: 0, b: 1, ta: (30.0, 0.0), tb: (-30.0, 0.0) }, - VectorNetworkSegment { a: 1, b: 2, ta: (0.0, 30.0), tb: (0.0, -30.0) }, - VectorNetworkSegment { a: 2, b: 3, ta: (-30.0, 0.0), tb: (30.0, 0.0) }, - VectorNetworkSegment { a: 3, b: 0, ta: (0.0, -30.0), tb: (0.0, 30.0) }, + VectorNetworkSegment { + a: 0, + b: 1, + ta: (30.0, 0.0), + tb: (-30.0, 0.0), + }, + VectorNetworkSegment { + a: 1, + b: 2, + ta: (0.0, 30.0), + tb: (0.0, -30.0), + }, + VectorNetworkSegment { + a: 2, + b: 3, + ta: (-30.0, 0.0), + tb: (30.0, 0.0), + }, + VectorNetworkSegment { + a: 3, + b: 0, + ta: (0.0, -30.0), + tb: (0.0, 30.0), + }, ], regions: vec![VectorNetworkRegion { loops: vec![VectorNetworkLoop(vec![0, 1, 2, 3])], @@ -82,5 +108,8 @@ pub fn build() -> Scene { layout_child: None, }); - flat_scene("L0 Shapes", vec![rectangle, ell, polygon, star, ln, txt, vector]) + flat_scene( + "L0 Shapes", + vec![rectangle, ell, polygon, star, ln, txt, vector], + ) } diff --git a/crates/grida-canvas/examples/fixtures/l0_strokes.rs b/crates/grida-canvas/examples/fixtures/l0_strokes.rs index 0e4853ece9..213b86ebe3 100644 --- a/crates/grida-canvas/examples/fixtures/l0_strokes.rs +++ b/crates/grida-canvas/examples/fixtures/l0_strokes.rs @@ -3,9 +3,14 @@ use cg::cg::stroke_dasharray::StrokeDashArray; use math2::transform::AffineTransform; fn stroked_rect( - x: f32, y: f32, - align: StrokeAlign, cap: StrokeCap, join: StrokeJoin, - width: f32, color: Paint, dash: Option, + x: f32, + y: f32, + align: StrokeAlign, + cap: StrokeCap, + join: StrokeJoin, + width: f32, + color: Paint, + dash: Option, ) -> Node { Node::Rectangle(RectangleNodeRec { active: true, @@ -13,7 +18,10 @@ fn stroked_rect( blend_mode: LayerBlendMode::PassThrough, mask: None, transform: AffineTransform::from_box_center(x, y, 120.0, 80.0, 0.0), - size: Size { width: 120.0, height: 80.0 }, + size: Size { + width: 120.0, + height: 80.0, + }, corner_radius: RectangularCornerRadius::default(), corner_smoothing: CornerSmoothing(0.0), fills: Paints::new(vec![solid(240, 240, 240, 255)]), @@ -31,10 +39,7 @@ fn stroked_rect( }) } -fn marker_line( - x: f32, y: f32, - start: StrokeMarkerPreset, end: StrokeMarkerPreset, -) -> Node { +fn marker_line(x: f32, y: f32, start: StrokeMarkerPreset, end: StrokeMarkerPreset) -> Node { Node::Line(LineNodeRec { active: true, opacity: 1.0, @@ -42,7 +47,10 @@ fn marker_line( mask: None, effects: LayerEffects::default(), transform: AffineTransform::new(x, y, 0.0), - size: Size { width: 150.0, height: 0.0 }, + size: Size { + width: 150.0, + height: 0.0, + }, strokes: Paints::new(vec![solid(0, 0, 0, 255)]), stroke_width: 2.0, stroke_cap: StrokeCap::Round, @@ -58,24 +66,59 @@ fn marker_line( pub fn build() -> Scene { let gap = 140.0; - let r1 = stroked_rect(0.0, 0.0, - StrokeAlign::Center, StrokeCap::Butt, StrokeJoin::Miter, - 2.0, solid(0, 0, 0, 255), None); - let r2 = stroked_rect(gap, 0.0, - StrokeAlign::Inside, StrokeCap::Round, StrokeJoin::Round, - 3.0, solid(59, 100, 220, 255), None); - let r3 = stroked_rect(gap * 2.0, 0.0, - StrokeAlign::Outside, StrokeCap::Square, StrokeJoin::Bevel, - 4.0, solid(59, 180, 75, 255), None); - let r4 = stroked_rect(gap * 3.0, 0.0, - StrokeAlign::Center, StrokeCap::Round, StrokeJoin::Miter, - 2.0, solid(0, 0, 0, 255), - Some(StrokeDashArray(vec![10.0, 5.0]))); + let r1 = stroked_rect( + 0.0, + 0.0, + StrokeAlign::Center, + StrokeCap::Butt, + StrokeJoin::Miter, + 2.0, + solid(0, 0, 0, 255), + None, + ); + let r2 = stroked_rect( + gap, + 0.0, + StrokeAlign::Inside, + StrokeCap::Round, + StrokeJoin::Round, + 3.0, + solid(59, 100, 220, 255), + None, + ); + let r3 = stroked_rect( + gap * 2.0, + 0.0, + StrokeAlign::Outside, + StrokeCap::Square, + StrokeJoin::Bevel, + 4.0, + solid(59, 180, 75, 255), + None, + ); + let r4 = stroked_rect( + gap * 3.0, + 0.0, + StrokeAlign::Center, + StrokeCap::Round, + StrokeJoin::Miter, + 2.0, + solid(0, 0, 0, 255), + Some(StrokeDashArray(vec![10.0, 5.0])), + ); - let l1 = marker_line(0.0, 120.0, - StrokeMarkerPreset::Circle, StrokeMarkerPreset::EquilateralTriangle); - let l2 = marker_line(200.0, 120.0, - StrokeMarkerPreset::Diamond, StrokeMarkerPreset::VerticalBar); + let l1 = marker_line( + 0.0, + 120.0, + StrokeMarkerPreset::Circle, + StrokeMarkerPreset::EquilateralTriangle, + ); + let l2 = marker_line( + 200.0, + 120.0, + StrokeMarkerPreset::Diamond, + StrokeMarkerPreset::VerticalBar, + ); flat_scene("L0 Strokes", vec![r1, r2, r3, r4, l1, l2]) } diff --git a/crates/grida-canvas/examples/fixtures/l0_strokes_rect.rs b/crates/grida-canvas/examples/fixtures/l0_strokes_rect.rs index 4af15eef9b..545b65dd1b 100644 --- a/crates/grida-canvas/examples/fixtures/l0_strokes_rect.rs +++ b/crates/grida-canvas/examples/fixtures/l0_strokes_rect.rs @@ -10,14 +10,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(300.0), layout_target_height: Some(200.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -27,7 +32,10 @@ pub fn build() -> Scene { strokes: Paints::new(vec![solid(0, 0, 0, 255)]), stroke_style: StrokeStyle::default(), stroke_width: StrokeWidth::Rectangular(RectangularStrokeWidth { - stroke_top_width: 1.0, stroke_right_width: 2.0, stroke_bottom_width: 3.0, stroke_left_width: 4.0, + stroke_top_width: 1.0, + stroke_right_width: 2.0, + stroke_bottom_width: 3.0, + stroke_left_width: 4.0, }), effects: LayerEffects::default(), clip: false, @@ -41,14 +49,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 0.0, right: 0.0, bottom: 0.0, left: 320.0, + top: 0.0, + right: 0.0, + bottom: 0.0, + left: 320.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(300.0), layout_target_height: Some(200.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -78,14 +91,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 220.0, right: 0.0, bottom: 0.0, left: 0.0, + top: 220.0, + right: 0.0, + bottom: 0.0, + left: 0.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(300.0), layout_target_height: Some(200.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -98,10 +116,15 @@ pub fn build() -> Scene { stroke_cap: StrokeCap::Round, stroke_join: StrokeJoin::Round, stroke_miter_limit: StrokeMiterLimit(4.0), - stroke_dash_array: Some(cg::cg::stroke_dasharray::StrokeDashArray(vec![12.0, 6.0, 4.0, 6.0])), + stroke_dash_array: Some(cg::cg::stroke_dasharray::StrokeDashArray(vec![ + 12.0, 6.0, 4.0, 6.0, + ])), }, stroke_width: StrokeWidth::Rectangular(RectangularStrokeWidth { - stroke_top_width: 2.0, stroke_right_width: 4.0, stroke_bottom_width: 6.0, stroke_left_width: 8.0, + stroke_top_width: 2.0, + stroke_right_width: 4.0, + stroke_bottom_width: 6.0, + stroke_left_width: 8.0, }), effects: LayerEffects::default(), clip: false, @@ -115,14 +138,19 @@ pub fn build() -> Scene { mask: None, rotation: 0.0, position: LayoutPositioningBasis::Inset(EdgeInsets { - top: 220.0, right: 0.0, bottom: 0.0, left: 320.0, + top: 220.0, + right: 0.0, + bottom: 0.0, + left: 320.0, }), layout_container: LayoutContainerStyle::default(), layout_dimensions: LayoutDimensionStyle { layout_target_width: Some(300.0), layout_target_height: Some(200.0), - layout_min_width: None, layout_max_width: None, - layout_min_height: None, layout_max_height: None, + layout_min_width: None, + layout_max_width: None, + layout_min_height: None, + layout_max_height: None, layout_target_aspect_ratio: None, }, layout_child: None, @@ -146,12 +174,21 @@ pub fn build() -> Scene { stroke_dash_array: Some(cg::cg::stroke_dasharray::StrokeDashArray(vec![8.0, 4.0])), }, stroke_width: StrokeWidth::Rectangular(RectangularStrokeWidth { - stroke_top_width: 1.0, stroke_right_width: 3.0, stroke_bottom_width: 5.0, stroke_left_width: 3.0, + stroke_top_width: 1.0, + stroke_right_width: 3.0, + stroke_bottom_width: 5.0, + stroke_left_width: 3.0, }), effects: LayerEffects::default(), clip: false, }); let pairs = vec![(1u64, c1), (2u64, c2), (3u64, c3), (4u64, c4)]; - build_scene("L0 Strokes Rect", None, pairs, HashMap::new(), vec![1, 2, 3, 4]) + build_scene( + "L0 Strokes Rect", + None, + pairs, + HashMap::new(), + vec![1, 2, 3, 4], + ) } diff --git a/crates/grida-canvas/examples/fixtures/l0_strokes_varwidth.rs b/crates/grida-canvas/examples/fixtures/l0_strokes_varwidth.rs index 9aae807776..96ec8c957d 100644 --- a/crates/grida-canvas/examples/fixtures/l0_strokes_varwidth.rs +++ b/crates/grida-canvas/examples/fixtures/l0_strokes_varwidth.rs @@ -15,9 +15,24 @@ pub fn build() -> Scene { network: VectorNetwork { vertices: vec![(0.0, 100.0), (66.0, 0.0), (133.0, 100.0), (200.0, 0.0)], segments: vec![ - VectorNetworkSegment { a: 0, b: 1, ta: (22.0, -40.0), tb: (-22.0, 20.0) }, - VectorNetworkSegment { a: 1, b: 2, ta: (22.0, 20.0), tb: (-22.0, -40.0) }, - VectorNetworkSegment { a: 2, b: 3, ta: (22.0, -40.0), tb: (-22.0, 20.0) }, + VectorNetworkSegment { + a: 0, + b: 1, + ta: (22.0, -40.0), + tb: (-22.0, 20.0), + }, + VectorNetworkSegment { + a: 1, + b: 2, + ta: (22.0, 20.0), + tb: (-22.0, -40.0), + }, + VectorNetworkSegment { + a: 2, + b: 3, + ta: (22.0, -40.0), + tb: (-22.0, 20.0), + }, ], regions: vec![], }, @@ -40,52 +55,72 @@ pub fn build() -> Scene { let gap_y = 130.0; // Taper: thick start → thin end - let taper = s_curve(0.0, 0.0, cg::cg::varwidth::VarWidthProfile { - base: 3.0, - stops: vec![ - cg::cg::varwidth::WidthStop { u: 0.0, r: 6.0 }, - cg::cg::varwidth::WidthStop { u: 1.0, r: 0.5 }, - ], - }); + let taper = s_curve( + 0.0, + 0.0, + cg::cg::varwidth::VarWidthProfile { + base: 3.0, + stops: vec![ + cg::cg::varwidth::WidthStop { u: 0.0, r: 6.0 }, + cg::cg::varwidth::WidthStop { u: 1.0, r: 0.5 }, + ], + }, + ); // Reverse taper: thin start → thick end - let reverse_taper = s_curve(0.0, gap_y, cg::cg::varwidth::VarWidthProfile { - base: 3.0, - stops: vec![ - cg::cg::varwidth::WidthStop { u: 0.0, r: 0.5 }, - cg::cg::varwidth::WidthStop { u: 1.0, r: 6.0 }, - ], - }); + let reverse_taper = s_curve( + 0.0, + gap_y, + cg::cg::varwidth::VarWidthProfile { + base: 3.0, + stops: vec![ + cg::cg::varwidth::WidthStop { u: 0.0, r: 0.5 }, + cg::cg::varwidth::WidthStop { u: 1.0, r: 6.0 }, + ], + }, + ); // Bulge: thin → thick → thin (calligraphy / brush-pen feel) - let bulge = s_curve(0.0, gap_y * 2.0, cg::cg::varwidth::VarWidthProfile { - base: 3.0, - stops: vec![ - cg::cg::varwidth::WidthStop { u: 0.0, r: 1.0 }, - cg::cg::varwidth::WidthStop { u: 0.5, r: 8.0 }, - cg::cg::varwidth::WidthStop { u: 1.0, r: 1.0 }, - ], - }); + let bulge = s_curve( + 0.0, + gap_y * 2.0, + cg::cg::varwidth::VarWidthProfile { + base: 3.0, + stops: vec![ + cg::cg::varwidth::WidthStop { u: 0.0, r: 1.0 }, + cg::cg::varwidth::WidthStop { u: 0.5, r: 8.0 }, + cg::cg::varwidth::WidthStop { u: 1.0, r: 1.0 }, + ], + }, + ); // Multi-stop: irregular width profile (4 stops) - let multi_stop = s_curve(0.0, gap_y * 3.0, cg::cg::varwidth::VarWidthProfile { - base: 3.0, - stops: vec![ - cg::cg::varwidth::WidthStop { u: 0.0, r: 2.0 }, - cg::cg::varwidth::WidthStop { u: 0.25, r: 7.0 }, - cg::cg::varwidth::WidthStop { u: 0.6, r: 1.0 }, - cg::cg::varwidth::WidthStop { u: 1.0, r: 5.0 }, - ], - }); + let multi_stop = s_curve( + 0.0, + gap_y * 3.0, + cg::cg::varwidth::VarWidthProfile { + base: 3.0, + stops: vec![ + cg::cg::varwidth::WidthStop { u: 0.0, r: 2.0 }, + cg::cg::varwidth::WidthStop { u: 0.25, r: 7.0 }, + cg::cg::varwidth::WidthStop { u: 0.6, r: 1.0 }, + cg::cg::varwidth::WidthStop { u: 1.0, r: 5.0 }, + ], + }, + ); // Uniform profile: single stop (should render like a regular constant-width stroke) - let uniform = s_curve(0.0, gap_y * 4.0, cg::cg::varwidth::VarWidthProfile { - base: 3.0, - stops: vec![ - cg::cg::varwidth::WidthStop { u: 0.0, r: 3.0 }, - cg::cg::varwidth::WidthStop { u: 1.0, r: 3.0 }, - ], - }); + let uniform = s_curve( + 0.0, + gap_y * 4.0, + cg::cg::varwidth::VarWidthProfile { + base: 3.0, + stops: vec![ + cg::cg::varwidth::WidthStop { u: 0.0, r: 3.0 }, + cg::cg::varwidth::WidthStop { u: 1.0, r: 3.0 }, + ], + }, + ); flat_scene( "L0 Strokes VarWidth", diff --git a/crates/grida-canvas/examples/fixtures/l0_type.rs b/crates/grida-canvas/examples/fixtures/l0_type.rs index 5d0a6609e0..7c396f6bde 100644 --- a/crates/grida-canvas/examples/fixtures/l0_type.rs +++ b/crates/grida-canvas/examples/fixtures/l0_type.rs @@ -3,7 +3,8 @@ use cg::cg::color::CGColor; use cg::cg::fe::*; fn tspan( - x: f32, y: f32, + x: f32, + y: f32, content: &str, font_size: f32, font_weight: u32, @@ -43,28 +44,56 @@ pub fn build() -> Scene { let y_gap = 50.0; // Regular 16px, Left/Top - let t1 = tspan(0.0, 0.0, - "Regular 16px", 16.0, 400, - TextAlign::Left, TextAlignVertical::Top, None, None); + let t1 = tspan( + 0.0, + 0.0, + "Regular 16px", + 16.0, + 400, + TextAlign::Left, + TextAlignVertical::Top, + None, + None, + ); // Bold 24px, Center/Center - let t2 = tspan(0.0, y_gap, - "Bold 24px", 24.0, 700, - TextAlign::Center, TextAlignVertical::Center, - Some(300.0), Some(40.0)); + let t2 = tspan( + 0.0, + y_gap, + "Bold 24px", + 24.0, + 700, + TextAlign::Center, + TextAlignVertical::Center, + Some(300.0), + Some(40.0), + ); // Right aligned - let t3 = tspan(0.0, y_gap * 2.0, - "Right Aligned", 16.0, 400, - TextAlign::Right, TextAlignVertical::Bottom, - Some(300.0), Some(30.0)); + let t3 = tspan( + 0.0, + y_gap * 2.0, + "Right Aligned", + 16.0, + 400, + TextAlign::Right, + TextAlignVertical::Bottom, + Some(300.0), + Some(30.0), + ); // Justified text - let t4 = tspan(0.0, y_gap * 3.0, + let t4 = tspan( + 0.0, + y_gap * 3.0, "Justified Text with enough words to wrap across multiple lines for demonstration.", - 14.0, 400, - TextAlign::Justify, TextAlignVertical::Top, - Some(200.0), None); + 14.0, + 400, + TextAlign::Justify, + TextAlignVertical::Top, + Some(200.0), + None, + ); // Text with stroke let t5 = Node::TextSpan(TextSpanNodeRec { @@ -119,8 +148,16 @@ pub fn build() -> Scene { mask: None, effects: LayerEffects { shadows: vec![FilterShadowEffect::DropShadow(FeShadow { - dx: 2.0, dy: 2.0, blur: 4.0, spread: 0.0, - color: CGColor { r: 0, g: 0, b: 0, a: 153 }, + dx: 2.0, + dy: 2.0, + blur: 4.0, + spread: 0.0, + color: CGColor { + r: 0, + g: 0, + b: 0, + a: 153, + }, active: true, })], ..LayerEffects::default() @@ -194,7 +231,12 @@ pub fn build() -> Scene { let mut ts = TextStyleRec::from_font("Inter", 18.0); ts.text_decoration = Some(TextDecorationRec { text_decoration_line: TextDecorationLine::Underline, - text_decoration_color: Some(CGColor { r: 220, g: 59, b: 59, a: 255 }), + text_decoration_color: Some(CGColor { + r: 220, + g: 59, + b: 59, + a: 255, + }), text_decoration_style: Some(TextDecorationStyle::Solid), text_decoration_skip_ink: Some(true), text_decoration_thickness: Some(2.0), diff --git a/crates/grida-canvas/examples/fixtures/l0_type_features.rs b/crates/grida-canvas/examples/fixtures/l0_type_features.rs index cc8cbde935..80203daa15 100644 --- a/crates/grida-canvas/examples/fixtures/l0_type_features.rs +++ b/crates/grida-canvas/examples/fixtures/l0_type_features.rs @@ -14,9 +14,10 @@ pub fn build() -> Scene { text: "ffi ffl — liga off".to_owned(), text_style: { let mut ts = TextStyleRec::from_font("Inter", 20.0); - ts.font_features = Some(vec![ - FontFeature { tag: "liga".to_owned(), value: false }, - ]); + ts.font_features = Some(vec![FontFeature { + tag: "liga".to_owned(), + value: false, + }]); ts }, text_align: TextAlign::Left, @@ -43,9 +44,10 @@ pub fn build() -> Scene { text: "ffi ffl — liga on".to_owned(), text_style: { let mut ts = TextStyleRec::from_font("Inter", 20.0); - ts.font_features = Some(vec![ - FontFeature { tag: "liga".to_owned(), value: true }, - ]); + ts.font_features = Some(vec![FontFeature { + tag: "liga".to_owned(), + value: true, + }]); ts }, text_align: TextAlign::Left, @@ -72,9 +74,10 @@ pub fn build() -> Scene { text: "Small Caps Text — smcp".to_owned(), text_style: { let mut ts = TextStyleRec::from_font("Inter", 20.0); - ts.font_features = Some(vec![ - FontFeature { tag: "smcp".to_owned(), value: true }, - ]); + ts.font_features = Some(vec![FontFeature { + tag: "smcp".to_owned(), + value: true, + }]); ts }, text_align: TextAlign::Left, @@ -101,9 +104,10 @@ pub fn build() -> Scene { text: "Stylistic Set 01 — ss01".to_owned(), text_style: { let mut ts = TextStyleRec::from_font("Inter", 20.0); - ts.font_features = Some(vec![ - FontFeature { tag: "ss01".to_owned(), value: true }, - ]); + ts.font_features = Some(vec![FontFeature { + tag: "ss01".to_owned(), + value: true, + }]); ts }, text_align: TextAlign::Left, @@ -131,8 +135,14 @@ pub fn build() -> Scene { text_style: { let mut ts = TextStyleRec::from_font("Inter", 20.0); ts.font_features = Some(vec![ - FontFeature { tag: "tnum".to_owned(), value: true }, - FontFeature { tag: "zero".to_owned(), value: true }, + FontFeature { + tag: "tnum".to_owned(), + value: true, + }, + FontFeature { + tag: "zero".to_owned(), + value: true, + }, ]); ts }, diff --git a/crates/grida-canvas/examples/fixtures/l0_type_fvar.rs b/crates/grida-canvas/examples/fixtures/l0_type_fvar.rs index 0652f3b7d3..9fc6a00c60 100644 --- a/crates/grida-canvas/examples/fixtures/l0_type_fvar.rs +++ b/crates/grida-canvas/examples/fixtures/l0_type_fvar.rs @@ -110,9 +110,18 @@ pub fn build() -> Scene { text_style: { let mut ts = TextStyleRec::from_font("Roboto Flex", 20.0); ts.font_variations = Some(vec![ - FontVariation { axis: "wght".to_owned(), value: 600.0 }, - FontVariation { axis: "wdth".to_owned(), value: 80.0 }, - FontVariation { axis: "GRAD".to_owned(), value: 50.0 }, + FontVariation { + axis: "wght".to_owned(), + value: 600.0, + }, + FontVariation { + axis: "wdth".to_owned(), + value: 80.0, + }, + FontVariation { + axis: "GRAD".to_owned(), + value: 50.0, + }, ]); ts }, diff --git a/crates/grida-canvas/examples/fixtures/l0_vector.rs b/crates/grida-canvas/examples/fixtures/l0_vector.rs index a6e47ef10c..ca7ce46b81 100644 --- a/crates/grida-canvas/examples/fixtures/l0_vector.rs +++ b/crates/grida-canvas/examples/fixtures/l0_vector.rs @@ -16,10 +16,30 @@ pub fn build() -> Scene { network: VectorNetwork { vertices: vec![(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)], segments: vec![ - VectorNetworkSegment { a: 0, b: 1, ta: (30.0, 0.0), tb: (-30.0, 0.0) }, - VectorNetworkSegment { a: 1, b: 2, ta: (0.0, 30.0), tb: (0.0, -30.0) }, - VectorNetworkSegment { a: 2, b: 3, ta: (-30.0, 0.0), tb: (30.0, 0.0) }, - VectorNetworkSegment { a: 3, b: 0, ta: (0.0, -30.0), tb: (0.0, 30.0) }, + VectorNetworkSegment { + a: 0, + b: 1, + ta: (30.0, 0.0), + tb: (-30.0, 0.0), + }, + VectorNetworkSegment { + a: 1, + b: 2, + ta: (0.0, 30.0), + tb: (0.0, -30.0), + }, + VectorNetworkSegment { + a: 2, + b: 3, + ta: (-30.0, 0.0), + tb: (30.0, 0.0), + }, + VectorNetworkSegment { + a: 3, + b: 0, + ta: (0.0, -30.0), + tb: (0.0, 30.0), + }, ], regions: vec![VectorNetworkRegion { loops: vec![VectorNetworkLoop(vec![0, 1, 2, 3])], @@ -53,8 +73,18 @@ pub fn build() -> Scene { network: VectorNetwork { vertices: vec![(0.0, 80.0), (60.0, 0.0), (120.0, 80.0)], segments: vec![ - VectorNetworkSegment { a: 0, b: 1, ta: (20.0, -30.0), tb: (-20.0, 10.0) }, - VectorNetworkSegment { a: 1, b: 2, ta: (20.0, 10.0), tb: (-20.0, -30.0) }, + VectorNetworkSegment { + a: 0, + b: 1, + ta: (20.0, -30.0), + tb: (-20.0, 10.0), + }, + VectorNetworkSegment { + a: 1, + b: 2, + ta: (20.0, 10.0), + tb: (-20.0, -30.0), + }, ], regions: vec![], }, @@ -84,9 +114,24 @@ pub fn build() -> Scene { network: VectorNetwork { vertices: vec![(50.0, 0.0), (100.0, 100.0), (0.0, 100.0)], segments: vec![ - VectorNetworkSegment { a: 0, b: 1, ta: (0.0, 0.0), tb: (0.0, 0.0) }, - VectorNetworkSegment { a: 1, b: 2, ta: (0.0, 0.0), tb: (0.0, 0.0) }, - VectorNetworkSegment { a: 2, b: 0, ta: (0.0, 0.0), tb: (0.0, 0.0) }, + VectorNetworkSegment { + a: 0, + b: 1, + ta: (0.0, 0.0), + tb: (0.0, 0.0), + }, + VectorNetworkSegment { + a: 1, + b: 2, + ta: (0.0, 0.0), + tb: (0.0, 0.0), + }, + VectorNetworkSegment { + a: 2, + b: 0, + ta: (0.0, 0.0), + tb: (0.0, 0.0), + }, ], regions: vec![VectorNetworkRegion { loops: vec![VectorNetworkLoop(vec![0, 1, 2])], @@ -120,9 +165,24 @@ pub fn build() -> Scene { network: VectorNetwork { vertices: vec![(0.0, 40.0), (60.0, 0.0), (120.0, 40.0), (60.0, 80.0)], segments: vec![ - VectorNetworkSegment { a: 0, b: 1, ta: (20.0, -20.0), tb: (-20.0, 10.0) }, - VectorNetworkSegment { a: 1, b: 2, ta: (20.0, 10.0), tb: (-20.0, -20.0) }, - VectorNetworkSegment { a: 2, b: 3, ta: (-20.0, 20.0), tb: (20.0, -10.0) }, + VectorNetworkSegment { + a: 0, + b: 1, + ta: (20.0, -20.0), + tb: (-20.0, 10.0), + }, + VectorNetworkSegment { + a: 1, + b: 2, + ta: (20.0, 10.0), + tb: (-20.0, -20.0), + }, + VectorNetworkSegment { + a: 2, + b: 3, + ta: (-20.0, 20.0), + tb: (20.0, -10.0), + }, ], regions: vec![], }, diff --git a/crates/grida-canvas/examples/fixtures/mod.rs b/crates/grida-canvas/examples/fixtures/mod.rs index 1982bfdedb..005a0ba0de 100644 --- a/crates/grida-canvas/examples/fixtures/mod.rs +++ b/crates/grida-canvas/examples/fixtures/mod.rs @@ -10,6 +10,7 @@ pub use crate::fixture_helpers::*; +pub mod cover; pub mod l0_boolean_operation; pub mod l0_container; pub mod l0_effects; @@ -34,4 +35,3 @@ pub mod l0_type; pub mod l0_type_features; pub mod l0_type_fvar; pub mod l0_vector; -pub mod cover; diff --git a/crates/grida-canvas/examples/golden_grain_noise_effect.rs b/crates/grida-canvas/examples/golden_grain_noise_effect.rs index b98e7f9c70..1d384aedeb 100644 --- a/crates/grida-canvas/examples/golden_grain_noise_effect.rs +++ b/crates/grida-canvas/examples/golden_grain_noise_effect.rs @@ -99,6 +99,7 @@ fn main() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, width, height)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); diff --git a/crates/grida-canvas/examples/golden_outline_mode.rs b/crates/grida-canvas/examples/golden_outline_mode.rs index 650b6e01ec..19d7077e4a 100644 --- a/crates/grida-canvas/examples/golden_outline_mode.rs +++ b/crates/grida-canvas/examples/golden_outline_mode.rs @@ -95,6 +95,7 @@ fn render_with_outline_mode(outline: bool) -> skia_safe::Image { camera, RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); diff --git a/crates/grida-canvas/examples/golden_pdf.rs b/crates/grida-canvas/examples/golden_pdf.rs index 31d890ec0a..a247fdcf21 100644 --- a/crates/grida-canvas/examples/golden_pdf.rs +++ b/crates/grida-canvas/examples/golden_pdf.rs @@ -277,6 +277,7 @@ async fn main() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, scene_width, scene_height)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); renderer.load_scene(scene); diff --git a/crates/grida-canvas/examples/golden_pixel_preview.rs b/crates/grida-canvas/examples/golden_pixel_preview.rs index b8ffcc4022..045ffcb4c2 100644 --- a/crates/grida-canvas/examples/golden_pixel_preview.rs +++ b/crates/grida-canvas/examples/golden_pixel_preview.rs @@ -82,6 +82,7 @@ fn render_with_pixel_preview(scale: u8) -> skia_safe::Image { camera, RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); @@ -142,7 +143,12 @@ fn main() { &border, ); canvas.draw_rect( - Rect::from_xywh(right_x, top_y, normal.width() as f32, normal.height() as f32), + Rect::from_xywh( + right_x, + top_y, + normal.width() as f32, + normal.height() as f32, + ), &border, ); @@ -156,4 +162,3 @@ fn main() { ) .expect("write png"); } - diff --git a/crates/grida-canvas/examples/golden_pixel_preview_stability.rs b/crates/grida-canvas/examples/golden_pixel_preview_stability.rs index 70cade420f..455bba073d 100644 --- a/crates/grida-canvas/examples/golden_pixel_preview_stability.rs +++ b/crates/grida-canvas/examples/golden_pixel_preview_stability.rs @@ -63,6 +63,7 @@ fn render_frame(scale: u8, zoom: f32, cx: f32, cy: f32) -> skia_safe::Image { camera, RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); @@ -136,10 +137,7 @@ fn main() { border.set_color(Color::from_argb(255, 220, 220, 220)); border.set_style(skia_safe::paint::Style::Stroke); border.set_stroke_width(1.0); - canvas.draw_rect( - Rect::from_xywh(x, y + label_h, cell_w, cell_h), - &border, - ); + canvas.draw_rect(Rect::from_xywh(x, y + label_h, cell_w, cell_h), &border); } let image = surface.image_snapshot(); @@ -147,9 +145,11 @@ fn main() { .encode(None, skia_safe::EncodedImageFormat::PNG, None) .expect("encode"); std::fs::write( - concat!(env!("CARGO_MANIFEST_DIR"), "/goldens/pixel_preview_stability.png"), + concat!( + env!("CARGO_MANIFEST_DIR"), + "/goldens/pixel_preview_stability.png" + ), data.as_bytes(), ) .expect("write png"); } - diff --git a/crates/grida-canvas/examples/golden_svg.rs b/crates/grida-canvas/examples/golden_svg.rs index b3262a6363..53cf8ecd41 100644 --- a/crates/grida-canvas/examples/golden_svg.rs +++ b/crates/grida-canvas/examples/golden_svg.rs @@ -244,6 +244,7 @@ async fn main() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, scene_width, scene_height)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); renderer.load_scene(scene); diff --git a/crates/grida-canvas/examples/golden_svg_gradients.rs b/crates/grida-canvas/examples/golden_svg_gradients.rs index 7c9c87c833..114ac51782 100644 --- a/crates/grida-canvas/examples/golden_svg_gradients.rs +++ b/crates/grida-canvas/examples/golden_svg_gradients.rs @@ -58,6 +58,7 @@ fn render_scene( Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, width, height)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); renderer.load_scene(scene); diff --git a/crates/grida-canvas/examples/golden_type_stroke.rs b/crates/grida-canvas/examples/golden_type_stroke.rs index a57a859691..f559804fce 100644 --- a/crates/grida-canvas/examples/golden_type_stroke.rs +++ b/crates/grida-canvas/examples/golden_type_stroke.rs @@ -106,6 +106,7 @@ async fn main() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, width, height)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); renderer.load_scene(scene); diff --git a/crates/grida-canvas/examples/tool_gen_cover.rs b/crates/grida-canvas/examples/tool_gen_cover.rs index d6aa9a3aae..c808688dfb 100644 --- a/crates/grida-canvas/examples/tool_gen_cover.rs +++ b/crates/grida-canvas/examples/tool_gen_cover.rs @@ -40,6 +40,7 @@ fn main() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, width, height)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); diff --git a/crates/grida-canvas/src/cache/mipmap.rs b/crates/grida-canvas/src/cache/mipmap.rs deleted file mode 100644 index beffbfe95c..0000000000 --- a/crates/grida-canvas/src/cache/mipmap.rs +++ /dev/null @@ -1,144 +0,0 @@ -use skia_safe::{surfaces, Image, Paint as SkPaint, Rect}; - -/// Strategy for generating the scale levels for mipmaps. -#[derive(Debug, Clone)] -pub enum MipmapLevels { - /// Use the provided fixed scale steps. - Fixed(Vec), - /// Generate a power-of-two chain down to 1x1 for each image. - FullChain, -} - -/// Configuration for generating mipmaps for images. -#[derive(Debug, Clone)] -pub struct MipmapConfig { - pub levels: MipmapLevels, - /// Whether to progressively resize from the previously generated level. - /// This is usually faster for long chains. - pub chained: bool, -} - -impl Default for MipmapConfig { - fn default() -> Self { - Self { - levels: MipmapLevels::FullChain, - chained: true, - } - } -} - -#[derive(Debug, Clone)] -pub struct ImageMipmaps { - levels: Vec<(f32, Image)>, -} - -impl ImageMipmaps { - pub fn from_image(image: Image, config: &MipmapConfig) -> Self { - let mut levels = Vec::new(); - - let mut scales: Vec = match &config.levels { - MipmapLevels::Fixed(steps) => steps.clone(), - MipmapLevels::FullChain => { - let max_dim = image.width().max(image.height()).max(1) as f32; - let levels = max_dim.log2().ceil() as u32 + 1; - (0..levels).map(|i| 1.0 / 2f32.powi(i as i32)).collect() - } - }; - - if !scales.is_empty() { - // ensure the scales are sorted from large to small for efficient chaining - scales.sort_by(|a, b| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)); - - if config.chained { - // start from the original image or the first scale - let mut prev_scale = 1.0; - let mut prev_img = image.clone(); - - for &scale in &scales { - if (scale - 1.0).abs() > f32::EPSILON { - let ratio = scale / prev_scale; - prev_img = scale_image(&prev_img, ratio); - prev_scale = scale; - } - - levels.push((scale, prev_img.clone())); - } - } else { - for &scale in &scales { - let img = if (scale - 1.0).abs() < f32::EPSILON { - image.clone() - } else { - scale_image(&image, scale) - }; - levels.push((scale, img)); - } - } - } - Self { levels } - } - - /// Dimensions of the base (original) image. - pub fn dimensions(&self) -> Option<(u32, u32)> { - self.levels - .first() - .map(|(_, img)| (img.width() as u32, img.height() as u32)) - } - - /// Number of mipmap levels. - pub fn level_count(&self) -> usize { - self.levels.len() - } - - /// Returns the image for the smallest mip level. - pub fn last_level_image(&self) -> Option<&Image> { - self.levels.last().map(|(_, img)| img) - } - - pub fn best_for_zoom(&self, zoom: f32) -> Option<&Image> { - if self.levels.is_empty() { - return None; - } - - for (scale, image) in self.levels.iter().rev() { - if zoom <= *scale { - return Some(image); - } - } - Some(&self.levels[0].1) - } - - pub fn best_for_size(&self, width: f32, height: f32) -> Option<&Image> { - if self.levels.is_empty() { - return None; - } - - let base_width = self.levels[0].1.width() as f32; - let base_height = self.levels[0].1.height() as f32; - let required_scale = (width / base_width).max(height / base_height); - - for (scale, image) in self.levels.iter().rev() { - if required_scale <= *scale { - return Some(image); - } - } - - Some(&self.levels[0].1) - } -} - -fn scale_image(image: &Image, scale: f32) -> Image { - let width = ((image.width() as f32 * scale).round() as i32).max(1); - let height = ((image.height() as f32 * scale).round() as i32).max(1); - let Some(mut surface) = surfaces::raster_n32_premul((width, height)) else { - return image.clone(); - }; - let canvas = surface.canvas(); - let paint = SkPaint::default(); - canvas.draw_image_rect( - image, - None, - Rect::from_xywh(0.0, 0.0, width as f32, height as f32), - &paint, - ); - surface.image_snapshot() -} diff --git a/crates/grida-canvas/src/cache/mod.rs b/crates/grida-canvas/src/cache/mod.rs index c152874557..33130963d1 100644 --- a/crates/grida-canvas/src/cache/mod.rs +++ b/crates/grida-canvas/src/cache/mod.rs @@ -1,7 +1,6 @@ pub mod atlas; pub mod compositor; pub mod geometry; -pub mod mipmap; pub mod paragraph; pub mod picture; pub mod scene; diff --git a/crates/grida-canvas/src/cache/paragraph.rs b/crates/grida-canvas/src/cache/paragraph.rs index 6662dfda5b..d2a4bf3874 100644 --- a/crates/grida-canvas/src/cache/paragraph.rs +++ b/crates/grida-canvas/src/cache/paragraph.rs @@ -113,7 +113,7 @@ pub struct BaselineInfo { /// /// This struct contains all available measurement results from the Skia Paragraph API, /// providing complete geometric information for layout calculations and rendering. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct LayoutMeasurements { // Basic dimensions /// Total height of the paragraph @@ -156,12 +156,26 @@ pub struct ParagraphCacheEntry { // For now, we just store the paragraph and compute measurements on demand } +/// Accumulated statistics from `measure()` calls — for benchmarking only. +/// All fields are simple integer counters (zero-cost increments). +#[derive(Default, Debug, Clone)] +pub struct ParagraphMeasureStats { + pub calls: u64, + pub cache_hits: u64, + pub cache_misses: u64, +} + #[derive(Default, Debug, Clone)] pub struct ParagraphCache { // ID-based cache for text nodes (primary usage) entries_measurement_by_id: HashMap, // Shape-key-based cache for flexible usage (not currently used) entries_measurement_by_shapekey_unstable: HashMap, + /// Benchmark statistics — zero-cost when not read. + pub stats: ParagraphMeasureStats, + /// When true, `measure()` returns a zero-size stub without calling Skia. + /// For benchmarking only — isolates text shaping cost from layout cost. + pub skip_text_measure: bool, } impl ParagraphCache { @@ -169,6 +183,8 @@ impl ParagraphCache { Self { entries_measurement_by_id: HashMap::new(), entries_measurement_by_shapekey_unstable: HashMap::new(), + stats: ParagraphMeasureStats::default(), + skip_text_measure: false, } } @@ -209,13 +225,20 @@ impl ParagraphCache { fonts: &FontRepository, id: Option<&NodeId>, ) -> LayoutMeasurements { + if self.skip_text_measure { + return LayoutMeasurements::default(); + } + let fonts_gen = fonts.generation(); + self.stats.calls += 1; + // Check if we have a cached paragraph if let Some(node_id) = id { // Use ID-based cache if let Some(entry) = self.entries_measurement_by_id.get(node_id) { if entry.font_generation == fonts_gen { + self.stats.cache_hits += 1; // Use the cached paragraph and compute measurements let paragraph_rc = entry.paragraph.clone(); return Self::compute_measurements(paragraph_rc, width); @@ -226,12 +249,14 @@ impl ParagraphCache { let hash = Self::shape_key(text, style, align, max_lines); if let Some(entry) = self.entries_measurement_by_shapekey_unstable.get(&hash) { if entry.font_generation == fonts_gen { + self.stats.cache_hits += 1; // Use the cached paragraph and compute measurements let paragraph_rc = entry.paragraph.clone(); return Self::compute_measurements(paragraph_rc, width); } } } + self.stats.cache_misses += 1; // Build the paragraph (expensive operation) - no paint for measurement let mut paragraph_style = textlayout::ParagraphStyle::new(); diff --git a/crates/grida-canvas/src/cache/scene.rs b/crates/grida-canvas/src/cache/scene.rs index 1c9554902c..c9bdda7cad 100644 --- a/crates/grida-canvas/src/cache/scene.rs +++ b/crates/grida-canvas/src/cache/scene.rs @@ -199,5 +199,4 @@ impl SceneCache { .map(|il| il.index) .collect() } - } diff --git a/crates/grida-canvas/src/layout/cache.rs b/crates/grida-canvas/src/layout/cache.rs index 06ba13745b..a78709fedf 100644 --- a/crates/grida-canvas/src/layout/cache.rs +++ b/crates/grida-canvas/src/layout/cache.rs @@ -37,6 +37,19 @@ impl LayoutResult { pub fn clear(&mut self) { self.layouts.clear(); } + + pub fn iter(&self) -> impl Iterator { + self.layouts.iter() + } + + /// Pre-allocate storage for `capacity` layout entries. + /// + /// Call after `clear()` when the upcoming node count is known. + pub fn reserve(&mut self, capacity: usize) { + if self.layouts.capacity() < capacity { + self.layouts.reserve(capacity); + } + } } impl Default for LayoutResult { diff --git a/crates/grida-canvas/src/layout/engine.rs b/crates/grida-canvas/src/layout/engine.rs index 8deb32e90b..35343aa5cb 100644 --- a/crates/grida-canvas/src/layout/engine.rs +++ b/crates/grida-canvas/src/layout/engine.rs @@ -94,11 +94,18 @@ impl LayoutEngine { viewport_size: Size, mut text_measure: Option>, ) -> &LayoutResult { - // Clear previous state + // Clear previous state and pre-allocate for the new scene size. + // HashMap::clear() preserves capacity, so subsequent calls with + // similar-sized scenes pay no allocation cost. The reserve() calls + // only allocate when the new scene exceeds prior capacity. self.tree.clear(); self.result.clear(); let graph = &scene.graph; + let node_count = graph.node_count(); + self.tree.reserve(node_count); + self.result.reserve(node_count); + let roots: Vec = graph.roots().to_vec(); // Build and compute layout for each root subtree @@ -142,6 +149,54 @@ impl LayoutEngine { &self.result } + /// Produce layout results directly from schema positions and sizes, + /// bypassing the Taffy layout engine entirely. + /// + /// This is a fast path for documents where all nodes are absolutely + /// positioned (e.g. Figma imports without auto-layout). Each node's + /// schema position and size are copied verbatim into the layout result. + /// + /// Only nodes reachable from the scene's roots are included (same + /// scope as the full Taffy path). + pub fn compute_schema_only(&mut self, scene: &crate::node::schema::Scene) -> &LayoutResult { + self.result.clear(); + let graph = &scene.graph; + let node_count = graph.node_count(); + self.result.reserve(node_count); + + let roots: Vec = graph.roots().to_vec(); + for root_id in &roots { + self.extract_schema_only_recursive(root_id, graph); + } + &self.result + } + + /// Recursively extract schema positions/sizes for a node and its children. + fn extract_schema_only_recursive( + &mut self, + id: &NodeId, + graph: &crate::node::scene_graph::SceneGraph, + ) { + 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, + }, + ); + } + if let Some(children) = graph.get_children(id) { + for child_id in children { + self.extract_schema_only_recursive(child_id, graph); + } + } + } + /// Get the full layout result pub fn result(&self) -> &LayoutResult { &self.result diff --git a/crates/grida-canvas/src/layout/tree.rs b/crates/grida-canvas/src/layout/tree.rs index a2f6071c64..a79bb839c2 100644 --- a/crates/grida-canvas/src/layout/tree.rs +++ b/crates/grida-canvas/src/layout/tree.rs @@ -52,6 +52,22 @@ impl LayoutTree { } } + /// Pre-allocate internal storage for `capacity` nodes. + /// + /// Call after `clear()` when the upcoming node count is known. + /// Avoids repeated reallocation of the taffy slab and both hash maps + /// during tree construction — significant for large scenes (100k+ nodes). + pub(crate) fn reserve(&mut self, capacity: usize) { + // TaffyTree::with_capacity pre-allocates its internal SlotMaps. + // After clear() the backing vecs keep their old capacity, so we + // only need to replace the tree when the new count exceeds it. + if self.scene_to_taffy.capacity() < capacity { + self.taffy = TaffyTree::with_capacity(capacity); + self.scene_to_taffy.reserve(capacity); + self.taffy_to_scene.reserve(capacity); + } + } + /// Create a leaf node in the layout tree /// /// Maps the scene node ID to a taffy node ID and returns it diff --git a/crates/grida-canvas/src/painter/image.rs b/crates/grida-canvas/src/painter/image.rs index b4c6c3a508..b1abdad2d1 100644 --- a/crates/grida-canvas/src/painter/image.rs +++ b/crates/grida-canvas/src/painter/image.rs @@ -4,7 +4,7 @@ use crate::{ sk, }; use math2::transform::AffineTransform; -use skia_safe::{self, shaders, Color, SamplingOptions, Shader, TileMode}; +use skia_safe::{self, shaders, Color, FilterMode, MipmapMode, SamplingOptions, Shader, TileMode}; fn tile_modes_for_repeat(repeat: ImageRepeat) -> (TileMode, TileMode) { match repeat { @@ -36,7 +36,11 @@ pub fn image_shader( (image.width() as f32, image.height() as f32), size, )); - let sampling = SamplingOptions::default(); + // Use Skia's built-in mipmap support. The image stored in ImageRepository + // has a mipmap chain attached via `with_default_mipmaps()`. Skia evaluates + // the LOD at rasterization time based on the final canvas transform, so + // this works correctly with PictureCache playback at different zoom levels. + let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::Nearest); // Extract repeat mode based on the fit variant let tile_modes = match &img.fit { diff --git a/crates/grida-canvas/src/painter/paint.rs b/crates/grida-canvas/src/painter/paint.rs index ca5caea68d..0c469f3f64 100644 --- a/crates/grida-canvas/src/painter/paint.rs +++ b/crates/grida-canvas/src/painter/paint.rs @@ -140,7 +140,9 @@ pub fn shader_from_paint( let key = match &img.image { ResourceRef::RID(r) | ResourceRef::HASH(r) => r, }; - let skia_image = repo.get_by_size(key, size.0, size.1)?; + // Skia's built-in mipmaps handle LOD selection at rasterization + // time based on the final canvas transform. No zoom needed here. + let skia_image = repo.get(key)?; image::image_shader(img, skia_image, size) } } diff --git a/crates/grida-canvas/src/painter/painter.rs b/crates/grida-canvas/src/painter/painter.rs index a7c0e5a056..f4d84aac89 100644 --- a/crates/grida-canvas/src/painter/painter.rs +++ b/crates/grida-canvas/src/painter/painter.rs @@ -88,7 +88,7 @@ impl<'a> Painter<'a> { } } - /// Create a new Painter that uses the SceneCache's paragraph cache + /// Create a new Painter that uses the SceneCache's paragraph cache. pub fn new_with_scene_cache( canvas: &'a skia_safe::Canvas, fonts: &'a FontRepository, diff --git a/crates/grida-canvas/src/runtime/changes.rs b/crates/grida-canvas/src/runtime/changes.rs new file mode 100644 index 0000000000..4f5d7653f2 --- /dev/null +++ b/crates/grida-canvas/src/runtime/changes.rs @@ -0,0 +1,192 @@ +//! Central change-tracking for the renderer. +//! +//! Instead of each mutation site manually deciding which caches to +//! invalidate, callers declare *what changed* via [`ChangeFlags`] and the +//! renderer's [`apply_changes`](super::scene::Renderer::apply_changes) +//! method translates those flags into the correct invalidation for every +//! cache layer — once per frame, in one place. +//! +//! This eliminates two classes of bugs: +//! - **Over-invalidation**: e.g. resize nuking per-node caches that are +//! viewport-independent. +//! - **Under-invalidation**: a new mutation site forgetting to invalidate a +//! cache, producing stale artifacts. + +use crate::node::schema::NodeId; + +// --------------------------------------------------------------------------- +// ChangeFlags — bitflags for broad change categories +// --------------------------------------------------------------------------- + +/// Bitflags describing what changed since the last frame. +/// +/// Multiple flags can be combined with `|`. The central +/// [`apply_changes`](super::scene::Renderer::apply_changes) dispatcher +/// reads these flags to decide which caches need invalidation. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub struct ChangeFlags(u32); + +#[allow(non_upper_case_globals)] +impl ChangeFlags { + /// Nothing changed. + pub const NONE: Self = Self(0); + + /// The viewport/window was resized. + pub const VIEWPORT_SIZE: Self = Self(1 << 0); + + /// A new scene was loaded (full reset). + pub const SCENE_LOAD: Self = Self(1 << 1); + + /// Node content changed (fills, strokes, effects — but not text). + /// Pair with [`ChangeSet::push_node`] for surgical per-node invalidation. + pub const NODE_CONTENT: Self = Self(1 << 2); + + /// Text content changed on a node. + /// Pair with [`ChangeSet::push_node`] for surgical per-node invalidation. + pub const NODE_TEXT: Self = Self(1 << 3); + + /// A font resource was loaded / font config changed. + pub const FONT_LOADED: Self = Self(1 << 4); + + /// An image resource was loaded. + pub const IMAGE_LOADED: Self = Self(1 << 5); + + /// Runtime configuration changed (compositing toggle, atlas toggle, etc.). + pub const CONFIG: Self = Self(1 << 6); + + /// Layout inputs changed (node resize, auto-layout property edit, etc.). + pub const LAYOUT_DIRTY: Self = Self(1 << 7); + + // -- helpers -- + + pub const fn is_empty(self) -> bool { + self.0 == 0 + } + + pub const fn contains(self, other: Self) -> bool { + (self.0 & other.0) == other.0 + } + + pub const fn intersects(self, other: Self) -> bool { + (self.0 & other.0) != 0 + } + + pub const fn union(self, other: Self) -> Self { + Self(self.0 | other.0) + } +} + +impl std::ops::BitOr for ChangeFlags { + type Output = Self; + fn bitor(self, rhs: Self) -> Self { + self.union(rhs) + } +} + +impl std::ops::BitOrAssign for ChangeFlags { + fn bitor_assign(&mut self, rhs: Self) { + *self = self.union(rhs); + } +} + +// --------------------------------------------------------------------------- +// ChangeSet — accumulated changes between frames +// --------------------------------------------------------------------------- + +/// Accumulates changes between frames. +/// +/// Callers push flags (and optionally specific node IDs) via +/// [`mark`] / [`push_node`]. At frame time the renderer calls +/// [`take`] to consume the set, then dispatches invalidation +/// based on the contents. +#[derive(Debug, Clone, Default)] +pub struct ChangeSet { + flags: ChangeFlags, + /// Specific nodes that changed (for surgical per-node invalidation). + /// Empty when the change is scene-wide (e.g. font loaded). + nodes: Vec, +} + +impl ChangeSet { + pub fn new() -> Self { + Self::default() + } + + /// Record a broad change category. + pub fn mark(&mut self, flags: ChangeFlags) { + self.flags |= flags; + } + + /// Record a change targeting a specific node. + /// + /// The node ID is stored for surgical invalidation in caches that + /// support per-node invalidation (picture cache, compositor, atlas). + pub fn push_node(&mut self, id: NodeId, flags: ChangeFlags) { + self.flags |= flags; + self.nodes.push(id); + } + + /// True when no changes have been recorded. + pub fn is_empty(&self) -> bool { + self.flags.is_empty() + } + + /// Read the accumulated flags. + pub fn flags(&self) -> ChangeFlags { + self.flags + } + + /// Read the per-node change list. + pub fn nodes(&self) -> &[NodeId] { + &self.nodes + } + + /// Consume the change set, returning it and resetting to empty. + pub fn take(&mut self) -> ChangeSet { + std::mem::take(self) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_by_default() { + let cs = ChangeSet::new(); + assert!(cs.is_empty()); + assert!(cs.flags().is_empty()); + assert!(cs.nodes().is_empty()); + } + + #[test] + fn mark_combines_flags() { + let mut cs = ChangeSet::new(); + cs.mark(ChangeFlags::VIEWPORT_SIZE); + cs.mark(ChangeFlags::FONT_LOADED); + assert!(cs.flags().contains(ChangeFlags::VIEWPORT_SIZE)); + assert!(cs.flags().contains(ChangeFlags::FONT_LOADED)); + assert!(!cs.flags().contains(ChangeFlags::SCENE_LOAD)); + } + + #[test] + fn push_node_records_id_and_flags() { + let mut cs = ChangeSet::new(); + cs.push_node(42, ChangeFlags::NODE_CONTENT); + assert!(cs.flags().contains(ChangeFlags::NODE_CONTENT)); + assert_eq!(cs.nodes(), &[42]); + } + + #[test] + fn take_resets() { + let mut cs = ChangeSet::new(); + cs.mark(ChangeFlags::IMAGE_LOADED); + cs.push_node(7, ChangeFlags::NODE_TEXT); + let taken = cs.take(); + assert!(cs.is_empty()); + assert!(cs.nodes().is_empty()); + assert!(!taken.is_empty()); + assert!(taken.flags().contains(ChangeFlags::IMAGE_LOADED)); + assert_eq!(taken.nodes(), &[7]); + } +} diff --git a/crates/grida-canvas/src/runtime/config.rs b/crates/grida-canvas/src/runtime/config.rs index 7d14cf9ecc..18eecabbaa 100644 --- a/crates/grida-canvas/src/runtime/config.rs +++ b/crates/grida-canvas/src/runtime/config.rs @@ -42,6 +42,12 @@ pub struct RuntimeRendererConfig { pub pixel_preview_strategy: PixelPreviewStrategy, /// Render policy describing how content/effects/compositing should be rendered. pub render_policy: super::render_policy::RenderPolicy, + /// When true, `load_scene` skips the Taffy layout computation and instead + /// derives each node's layout directly from its schema position and size. + /// + /// Use this for documents where all positioning is absolute (e.g. SVG). Eliminates the layout phase entirely, + /// which is the dominant cost in `load_scene` for large documents. + pub skip_layout: bool, } impl Default for RuntimeRendererConfig { @@ -53,6 +59,7 @@ impl Default for RuntimeRendererConfig { pixel_preview_scale: 0, pixel_preview_strategy: PixelPreviewStrategy::Stable, render_policy: Default::default(), + skip_layout: false, } } } diff --git a/crates/grida-canvas/src/runtime/image_repository.rs b/crates/grida-canvas/src/runtime/image_repository.rs index 4713eaceb5..a134748d4f 100644 --- a/crates/grida-canvas/src/runtime/image_repository.rs +++ b/crates/grida-canvas/src/runtime/image_repository.rs @@ -6,19 +6,20 @@ use std::{ use skia_safe::{self, Image}; -use crate::{ - cache::mipmap::{ImageMipmaps, MipmapConfig}, - resources::ByteStore, -}; +use crate::resources::ByteStore; -/// A repository for managing images with automatic ID indexing. +/// A repository for managing images with Skia built-in mipmaps. +/// +/// Images are stored with Skia's native mipmap chain attached via +/// [`Image::with_default_mipmaps()`]. Mipmap level selection happens +/// automatically at rasterization time based on the final canvas transform, +/// which correctly handles `Picture` playback at different zoom levels. #[derive(Debug, Clone)] pub struct ImageRepository { - images: HashMap, - config: MipmapConfig, + images: HashMap, store: Arc>, /// Image refs encountered during render that were not found in the repository. - /// Uses `RefCell` for interior mutability — `get_by_size` remains `&self` so the + /// Uses `RefCell` for interior mutability — `get` remains `&self` so the /// painter can hold an immutable borrow while recording misses. missing_refs: RefCell>, /// Image refs already surfaced to the caller via `drain_missing`. @@ -31,18 +32,6 @@ impl ImageRepository { pub fn new(store: Arc>) -> Self { Self { images: HashMap::new(), - config: MipmapConfig::default(), - store, - missing_refs: RefCell::new(HashSet::new()), - reported_refs: HashSet::new(), - } - } - - /// Creates a repository with custom mipmap configuration. - pub fn with_config(store: Arc>, config: MipmapConfig) -> Self { - Self { - images: HashMap::new(), - config, store, missing_refs: RefCell::new(HashSet::new()), reported_refs: HashSet::new(), @@ -55,14 +44,20 @@ impl ImageRepository { } /// Adds an image to the repository from bytes referenced by `hash`. + /// + /// The image is decoded and a Skia-native mipmap chain is attached. + /// Skia evaluates the mipmap LOD at rasterization time based on the + /// final transform, so this works correctly with `PictureCache`. pub fn insert(&mut self, src: String, hash: u64) -> Option<(u32, u32)> { if let Some(bytes) = self.store.lock().unwrap().get(hash) { let data = skia_safe::Data::new_copy(bytes); if let Some(image) = Image::from_encoded(data) { let width = image.width() as u32; let height = image.height() as u32; - let set = ImageMipmaps::from_image(image, &self.config); - self.images.insert(src.clone(), set); + // Attach Skia's built-in mipmap chain. Falls back to the + // original image if generation fails (e.g. 1×1 images). + let mipmapped = image.with_default_mipmaps().unwrap_or(image); + self.images.insert(src.clone(), mipmapped); // Clear from tracking — this ref is now satisfied. self.missing_refs.borrow_mut().remove(&src); self.reported_refs.remove(&src); @@ -72,11 +67,11 @@ impl ImageRepository { None } - /// Gets a reference to an image by its source URL and desired size. + /// Gets a reference to an image by its source URL. /// Records the ref as missing if not found (for lazy loading). - pub fn get_by_size(&self, src: &str, width: f32, height: f32) -> Option<&Image> { - if let Some(set) = self.images.get(src) { - set.best_for_size(width, height) + pub fn get(&self, src: &str) -> Option<&Image> { + if let Some(image) = self.images.get(src) { + Some(image) } else { self.missing_refs.borrow_mut().insert(src.to_owned()); None @@ -85,11 +80,13 @@ impl ImageRepository { /// Gets the dimensions of an image by its source URL. pub fn get_size(&self, src: &str) -> Option<(u32, u32)> { - self.images.get(src).and_then(|set| set.dimensions()) + self.images + .get(src) + .map(|img| (img.width() as u32, img.height() as u32)) } /// Removes an image from the repository by its source URL. - pub fn remove(&mut self, src: &str) -> Option { + pub fn remove(&mut self, src: &str) -> Option { self.images.remove(src) } diff --git a/crates/grida-canvas/src/runtime/mod.rs b/crates/grida-canvas/src/runtime/mod.rs index a7b2900a7a..6584dd5762 100644 --- a/crates/grida-canvas/src/runtime/mod.rs +++ b/crates/grida-canvas/src/runtime/mod.rs @@ -1,4 +1,5 @@ pub mod camera; +pub mod changes; pub mod config; pub mod counter; pub mod effect_tree; diff --git a/crates/grida-canvas/src/runtime/scene.rs b/crates/grida-canvas/src/runtime/scene.rs index 85ca25ad4f..1449246499 100644 --- a/crates/grida-canvas/src/runtime/scene.rs +++ b/crates/grida-canvas/src/runtime/scene.rs @@ -2,6 +2,7 @@ use crate::cg::prelude::*; use crate::node::{scene_graph::SceneGraph, schema::*}; use crate::painter::Painter; use crate::runtime::camera::CameraChangeKind; +use crate::runtime::changes::{ChangeFlags, ChangeSet}; use crate::runtime::counter::FrameCounter; use crate::runtime::render_policy::RenderPolicy; use crate::sk; @@ -75,22 +76,30 @@ fn detect_image_mime(bytes: &[u8]) -> &'static str { /// Callback type used to request a redraw from the host window. pub type RequestRedrawCallback = Arc; -/// Options controlling renderer behaviour. +/// Options controlling renderer behaviour at construction time. +/// +/// Passed through the entire init chain: TS → C ABI → Application → Renderer. +/// Fields here are applied once during construction. Most can also be changed +/// at runtime via individual setters on `Renderer` / `ApplicationApi`. #[derive(Clone, Copy)] pub struct RendererOptions { /// When true, embedded fonts will be registered. pub use_embedded_fonts: bool, + /// Initial renderer configuration. Applied at construction; individual + /// fields remain mutable via `Renderer::set_*` / `ApplicationApi` setters. + pub config: super::config::RuntimeRendererConfig, } impl Default for RendererOptions { fn default() -> Self { Self { use_embedded_fonts: false, + config: Default::default(), } } } -fn collect_scene_font_families(scene: &Scene) -> HashSet { +pub fn collect_scene_font_families(scene: &Scene) -> HashSet { fn walk(id: &NodeId, graph: &SceneGraph, set: &mut HashSet) { if let Ok(node) = graph.get_node(id) { match node { @@ -333,6 +342,12 @@ pub struct Renderer { /// Cached composited frame for zoom fast path. /// See [`ZoomImageCache`] for details. zoom_image_cache: Option, + /// Accumulated changes since the last frame. + /// + /// Mutation sites call [`mark_changed`] to declare what changed; + /// [`apply_changes`] consumes the set once per frame and performs + /// the correct invalidation for every cache layer. + changes: ChangeSet, } impl Renderer { @@ -354,11 +369,7 @@ impl Renderer { } #[inline] - fn prefill_picture_cache_for_plan( - &mut self, - plan: &FramePlan, - policy: RenderPolicy, - ) { + fn prefill_picture_cache_for_plan(&mut self, plan: &FramePlan, policy: RenderPolicy) { let variant_key = policy.variant_key(); // Prefill picture cache for visible layers so Painter can reuse pictures even with masks. // Fast path: skip clone + recording when the picture is already cached (common case @@ -433,12 +444,7 @@ impl Renderer { } } else if let Some(img) = layer_img.individual_image() { // Individual texture path. - let src_rect = Rect::new( - 0.0, - 0.0, - img.width() as f32, - img.height() as f32, - ); + let src_rect = Rect::new(0.0, 0.0, img.width() as f32, img.height() as f32); Some(crate::painter::PromotedBlit { image: std::rc::Rc::clone(img), src_rect, @@ -547,7 +553,7 @@ impl Renderer { request_redraw, fc: FrameCounter::new(), plan: None, - config: RuntimeRendererConfig::default(), + config: options.config, layout_engine: crate::layout::engine::LayoutEngine::new(), window_context: RendererWindowContext::new(viewport_size), compositor_surface: None, @@ -558,6 +564,7 @@ impl Renderer { downscale_dims: (0, 0), pan_image_cache: None, zoom_image_cache: None, + changes: ChangeSet::new(), } } @@ -635,14 +642,9 @@ impl Renderer { .borrow_mut() .invalidate_by_id(node_id); - // Invalidate the picture cache for this node so the Painter - // doesn't use a stale cached picture. - self.scene_cache.picture.invalidate_node(node_id); - - // Invalidate the compositor layer image for this node. - self.scene_cache.compositor.invalidate(&node_id); - // Free the atlas slot — it will be re-allocated on next capture. - self.compositor_atlas.free_node(&node_id); + // Record the change. apply_changes() will handle per-node + // picture/compositor/atlas invalidation and viewport caches. + self.mark_node_changed(node_id, ChangeFlags::NODE_TEXT); } pub fn canvas(&self) -> &Canvas { @@ -717,6 +719,7 @@ impl Renderer { if !enable { self.scene_cache.compositor.clear(); self.compositor_atlas.clear(); + self.mark_changed(ChangeFlags::CONFIG); } } @@ -748,8 +751,7 @@ impl Renderer { // re-captured as individual textures on the next frame. self.scene_cache.compositor.invalidate_all(); self.compositor_atlas.clear(); - self.pan_image_cache = None; - self.zoom_image_cache = None; + self.mark_changed(ChangeFlags::CONFIG); } } @@ -833,6 +835,18 @@ impl Renderer { self.config.render_policy = policy; } + /// Enable or disable layout computation during `load_scene`. + /// + /// When `skip` is true, `load_scene` bypasses the Taffy flexbox engine + /// and derives layout results directly from each node's schema position + /// and size. This is appropriate for documents with only absolute + /// positioning (e.g. imported Figma files without auto-layout). + /// + /// This flag must be set **before** calling `load_scene` / `switch_scene`. + pub fn set_skip_layout(&mut self, skip: bool) { + self.config.skip_layout = skip; + } + /// Render the queued frame if any and return the completed statistics. /// Intended to be called by the host when a redraw request is received. /// @@ -967,15 +981,19 @@ impl Renderer { && self.zoom_image_cache.is_some() && camera_change.zoom_changed() { - let zoom_cache_hit = self.try_zoom_cache_blit(surface, scene, &FramePlan { - stable, - camera_change, - promoted: Vec::new(), - regions: Vec::new(), - compositor_indices: Vec::new(), - display_list_duration: Duration::ZERO, - display_list_size_estimated: 0, - }); + let zoom_cache_hit = self.try_zoom_cache_blit( + surface, + scene, + &FramePlan { + stable, + camera_change, + promoted: Vec::new(), + regions: Vec::new(), + compositor_indices: Vec::new(), + display_list_duration: Duration::ZERO, + display_list_size_estimated: 0, + }, + ); if let Some((mid_flush_duration, frame_duration)) = zoom_cache_hit { let plan = FramePlan { stable, @@ -1130,8 +1148,7 @@ impl Renderer { // Reuse or create a downscaled offscreen for interaction rendering. let interaction_scale = self.config.interaction_render_scale; - let use_downscale = - !plan.stable && interaction_scale > 0.0 && interaction_scale < 1.0; + let use_downscale = !plan.stable && interaction_scale > 0.0 && interaction_scale < 1.0; if use_downscale { let sw = (width * interaction_scale).ceil() as i32; let sh = (height * interaction_scale).ceil() as i32; @@ -1249,8 +1266,7 @@ impl Renderer { let zoom_ratio = current_zoom / cache.zoom; // Only use cache if zoom ratio is within acceptable range. - if !((1.0 / ZOOM_IMAGE_CACHE_MAX_RATIO)..=ZOOM_IMAGE_CACHE_MAX_RATIO) - .contains(&zoom_ratio) + if !((1.0 / ZOOM_IMAGE_CACHE_MAX_RATIO)..=ZOOM_IMAGE_CACHE_MAX_RATIO).contains(&zoom_ratio) { // Too extreme — invalidate and fall through. self.zoom_image_cache = None; @@ -1280,12 +1296,7 @@ impl Renderer { ); let mut paint = SkPaint::default(); paint.set_anti_alias(false); - canvas.draw_image_with_sampling_options( - &cache.image, - (0.0, 0.0), - sampling, - Some(&paint), - ); + canvas.draw_image_with_sampling_options(&cache.image, (0.0, 0.0), sampling, Some(&paint)); canvas.restore(); let mid_flush_start = Instant::now(); @@ -1338,7 +1349,12 @@ impl Renderer { let viewport_size = self.window_context.viewport_size; // 1. Compute layout phase - { + if self.config.skip_layout { + // Fast path: derive layout directly from schema positions/sizes. + // Skips Taffy tree construction, flexbox computation, and text + // measurement — O(n) with minimal per-node work. + self.layout_engine.compute_schema_only(scene); + } else { let mut paragraph_cache = self.scene_cache.paragraph.borrow_mut(); self.layout_engine.compute( scene, @@ -1365,6 +1381,12 @@ impl Renderer { // 4. Build layers self.scene_cache.update_layers(scene); } + // Record SCENE_LOAD so apply_changes() knows to clear picture/paragraph/ + // path/compositor caches on the next frame. The scene_cache was already + // replaced above (empty), so the caches are naturally fresh — but the + // flag is still needed for viewport snapshot caches and any future + // apply_changes() logic. + self.mark_changed(ChangeFlags::SCENE_LOAD); self.queue_stable(); } @@ -1487,6 +1509,10 @@ impl Renderer { } /// Clear the cached scene picture. + /// + /// **Prefer [`mark_changed`] + [`apply_changes`]** for new code. + /// This method is retained for the few call sites that have not yet + /// been migrated to the central change-tracking system. pub fn invalidate_cache(&mut self) { self.scene_cache.invalidate(); // Also invalidate all compositor layer images so they re-rasterize. @@ -1496,6 +1522,139 @@ impl Renderer { self.zoom_image_cache = None; } + // ------------------------------------------------------------------- + // Central change-tracking + // ------------------------------------------------------------------- + + /// Declare that something changed. + /// + /// Callers set the appropriate [`ChangeFlags`] to describe the + /// mutation. The renderer accumulates them until the next frame, + /// when [`apply_changes`] translates the flags into precise + /// per-cache invalidation. + pub fn mark_changed(&mut self, flags: ChangeFlags) { + self.changes.mark(flags); + } + + /// Declare that a specific node changed. + /// + /// Same as [`mark_changed`] but also records the node ID for + /// surgical per-node cache invalidation. + pub fn mark_node_changed(&mut self, id: NodeId, flags: ChangeFlags) { + self.changes.push_node(id, flags); + } + + /// Consume accumulated changes and perform cache invalidation. + /// + /// Called once per frame (at the start of `Application::frame()`) + /// before building the frame plan. This is the **single source of + /// truth** for which caches are invalidated by which mutations. + /// + /// The `camera_change` parameter is folded in so that zoom/pan + /// invalidation lives in the same dispatch table. + pub fn apply_changes(&mut self, camera_change: CameraChangeKind, stable: bool) { + let cs = self.changes.take(); + let flags = cs.flags(); + + // Fast path: nothing changed (pure camera move handled below). + let has_data_changes = !flags.is_empty(); + + // ----- Layout ----- + // Scene load handles its own layout in load_scene(); skip here. + // Viewport resize needs layout only when the scene has ICB nodes + // or auto-sized roots (the common infinite-canvas case has neither). + if has_data_changes && !flags.contains(ChangeFlags::SCENE_LOAD) { + let needs_layout = flags.intersects(ChangeFlags::LAYOUT_DIRTY) + || (flags.contains(ChangeFlags::VIEWPORT_SIZE) + && self.scene_has_viewport_dependent_layout()); + if needs_layout { + self.rebuild_scene_caches(); + } + } + + // ----- Picture cache (per-node recorded Skia Pictures) ----- + if flags.intersects( + ChangeFlags::SCENE_LOAD | ChangeFlags::FONT_LOADED | ChangeFlags::IMAGE_LOADED, + ) { + self.scene_cache.picture.invalidate(); + } else if has_data_changes { + for &id in cs.nodes() { + self.scene_cache.picture.invalidate_node(id); + } + } + + // ----- Paragraph cache (text layout) ----- + if flags.intersects(ChangeFlags::SCENE_LOAD | ChangeFlags::FONT_LOADED) { + self.scene_cache.paragraph.borrow_mut().invalidate(); + } + // Per-node paragraph invalidation is handled by update_layer_text + // which runs before mark_changed, so we don't repeat it here. + + // ----- Vector path cache ----- + if flags.contains(ChangeFlags::SCENE_LOAD) { + self.scene_cache.path.borrow_mut().invalidate(); + } + + // ----- Compositor (LayerImageCache) + Atlas ----- + if flags.contains(ChangeFlags::SCENE_LOAD) { + self.scene_cache.compositor.clear(); + self.compositor_atlas.clear(); + } else if flags.intersects(ChangeFlags::FONT_LOADED | ChangeFlags::IMAGE_LOADED) { + self.scene_cache.compositor.invalidate_all(); + self.compositor_atlas.clear(); + } else if flags.contains(ChangeFlags::CONFIG) { + // Config changes (e.g. atlas toggle off) may need full compositor reset. + // The config-change call site sets compositor state directly before + // marking CONFIG; this handles the residual viewport cache clearing. + } + // Zoom-triggered compositor staleness + if camera_change.zoom_changed() && self.config.layer_compositing { + self.scene_cache.compositor.mark_all_stale(); + } + // Per-node compositor invalidation + for &id in cs.nodes() { + self.scene_cache.compositor.invalidate(&id); + self.compositor_atlas.free_node(&id); + } + + // ----- Viewport snapshot caches (pan/zoom image caches) ----- + // These are the ONLY caches that truly depend on viewport dimensions. + let invalidate_pan = has_data_changes || camera_change.zoom_changed() || stable; + let invalidate_zoom = has_data_changes || stable; + + if invalidate_pan { + self.pan_image_cache = None; + } + if invalidate_zoom { + self.zoom_image_cache = None; + } + } + + /// Check whether the current scene has layout that depends on viewport size. + /// + /// Returns `true` if any root node is an `InitialContainer` (ICB) — the + /// only node type whose Taffy style size is derived from viewport dimensions. + /// + /// When this returns `false`, `rebuild_scene_caches()` produces identical + /// output regardless of viewport size, so it can be skipped on resize. + fn scene_has_viewport_dependent_layout(&self) -> bool { + if self.config.skip_layout { + // compute_schema_only doesn't use viewport_size at all. + return false; + } + let Some(scene) = self.scene.as_ref() else { + return false; + }; + for &root_id in scene.graph.roots() { + if let Ok(node) = scene.graph.get_node(&root_id) { + if matches!(node, Node::InitialContainer(_)) { + return true; + } + } + } + false + } + /// 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) { @@ -1503,7 +1662,9 @@ impl Renderer { let viewport_size = self.window_context.viewport_size; // 1. Recompute layout - { + if self.config.skip_layout { + self.layout_engine.compute_schema_only(scene); + } else { let mut paragraph_cache = self.scene_cache.paragraph.borrow_mut(); self.layout_engine.compute( scene, @@ -1563,6 +1724,9 @@ impl Renderer { bounds.y + bounds.height, ); let canvas = recorder.begin_recording(sk_bounds, true); + // Skia's built-in mipmaps evaluate LOD at rasterization time based on + // the final canvas transform, so Picture playback at different zoom + // levels automatically selects the correct mipmap level. let painter = Painter::new_with_scene_cache( canvas, &self.fonts, @@ -1581,12 +1745,7 @@ impl Renderer { variant_key: u64, draw: impl FnOnce(&Painter), ) -> Option { - self.with_recording_cached_with_policy( - id, - variant_key, - self.config.render_policy, - draw, - ) + self.with_recording_cached_with_policy(id, variant_key, self.config.render_policy, draw) } fn with_recording_cached_with_policy( @@ -1633,8 +1792,8 @@ impl Renderer { ) -> FramePlan { let __start = Instant::now(); - let effective_layer_compositing = self.config.layer_compositing - && self.config.render_policy.allows_layer_compositing(); + let effective_layer_compositing = + self.config.layer_compositing && self.config.render_policy.allows_layer_compositing(); let mut promoted_ids: Vec = Vec::new(); let mut regions: Vec<(rect::Rectangle, Vec)> = Vec::new(); @@ -1798,9 +1957,14 @@ impl Renderer { let scaled_w = offscreen.width(); let scaled_h = offscreen.height(); let result = self.draw_to_offscreen_and_upscale( - canvas, offscreen, plan, background_color, - width, height, - scaled_w, scaled_h, + canvas, + offscreen, + plan, + background_color, + width, + height, + scaled_w, + scaled_h, scale, ); if let Some(r) = result { @@ -2023,11 +2187,7 @@ impl Renderer { /// during unstable (interactive) frames, and without limit on stable /// (settled) frames. Stale entries (zoom mismatch) are blitted with /// GPU texture stretching until re-rasterized. - fn update_compositor( - &mut self, - parent_surface: &mut Surface, - visible_indices: &[usize], - ) { + fn update_compositor(&mut self, parent_surface: &mut Surface, visible_indices: &[usize]) { self.update_compositor_inner(parent_surface, false, visible_indices); } @@ -2076,8 +2236,7 @@ impl Renderer { // Time budget for stale re-rasterization during interactive frames. // 8ms leaves headroom within a 16ms frame budget (60fps target). - const ZOOM_RERASTER_BUDGET: std::time::Duration = - std::time::Duration::from_micros(8000); + const ZOOM_RERASTER_BUDGET: std::time::Duration = std::time::Duration::from_micros(8000); let budget_start = std::time::Instant::now(); for &idx in visible_indices { @@ -2216,11 +2375,11 @@ impl Renderer { } else { // Shouldn't reach here if our needs_new_page check was correct, // but fall back to creating from the compositor surface. - let info = skia_safe::ImageInfo::new_n32_premul( - (w as i32, h as i32), - None, - ); - self.compositor_surface.as_mut().and_then(|cs| cs.new_surface(&info)) + let info = + skia_safe::ImageInfo::new_n32_premul((w as i32, h as i32), None); + self.compositor_surface + .as_mut() + .and_then(|cs| cs.new_surface(&info)) } }) .is_some(); @@ -2239,7 +2398,11 @@ impl Renderer { slot_canvas.scale((zoom, zoom)); slot_canvas.translate((-render_bounds.x, -render_bounds.y)); let painter = Painter::new_with_scene_cache( - slot_canvas, fonts, images, scene_cache, policy, + slot_canvas, + fonts, + images, + scene_cache, + policy, ); painter.draw_layer(&entry_layer); }); @@ -2291,12 +2454,16 @@ impl Renderer { let bounds = skia_safe::IRect::from_wh(pixel_width, pixel_height); if let Some(image) = offscreen.image_snapshot_with_bounds(bounds) { self.scene_cache.compositor.insert( - id, image, zoom, render_bounds, node_opacity, node_blend, + id, + image, + zoom, + render_bounds, + node_opacity, + node_blend, ); } } } - } } diff --git a/crates/grida-canvas/src/vectornetwork/vn_painter.rs b/crates/grida-canvas/src/vectornetwork/vn_painter.rs index a0efa0e425..a2bd9baabe 100644 --- a/crates/grida-canvas/src/vectornetwork/vn_painter.rs +++ b/crates/grida-canvas/src/vectornetwork/vn_painter.rs @@ -369,9 +369,11 @@ mod tests { #[test] fn image_stroke_renders() { - // Prepare image repository with a simple blue image + // Prepare image repository with a blue image. + // Use a larger image so bilinear+mipmap sampling doesn't blend + // edge pixels with the transparent Decal border. let mut store = ByteStore::new(); - let mut img_surface = surfaces::raster_n32_premul((2, 2)).expect("surface"); + let mut img_surface = surfaces::raster_n32_premul((32, 32)).expect("surface"); img_surface.canvas().clear(Color::BLUE); let img = img_surface.image_snapshot(); let data = img.encode(None, EncodedImageFormat::PNG, None).unwrap(); diff --git a/crates/grida-canvas/src/window/application.rs b/crates/grida-canvas/src/window/application.rs index 800b1f34ae..87f3b2214c 100644 --- a/crates/grida-canvas/src/window/application.rs +++ b/crates/grida-canvas/src/window/application.rs @@ -7,18 +7,19 @@ use crate::export::{export_node_as, ExportAs, Exported}; use crate::io::io_grida::{self, JSONFlattenResult}; use crate::io::io_grida_patch::{self, TransactionApplyReport}; use crate::node::schema::*; +use crate::query::Hierarchy; use crate::resources::{FontMessage, ImageMessage}; use crate::runtime::camera::Camera2D; -use crate::runtime::scene::{Backend, FrameFlushResult, Renderer}; +use crate::runtime::changes::ChangeFlags; use crate::runtime::frame_loop::{FrameLoop, FrameQuality}; +use crate::runtime::scene::{Backend, FrameFlushResult, Renderer}; use crate::sys::clock; use crate::sys::timer::TimerMgr; use crate::text; use crate::vectornetwork::VectorNetwork; +use crate::window::command::ApplicationCommand; use grida_text_edit::layout::ManagedTextLayout; use grida_text_edit::TextLayoutEngine; -use crate::query::Hierarchy; -use crate::window::command::ApplicationCommand; /// A no-op hierarchy for when no scene graph is loaded. struct NoHierarchy; @@ -29,11 +30,30 @@ impl Hierarchy for NoHierarchy { } #[cfg(not(target_arch = "wasm32"))] use futures::channel::mpsc; -use math2::{rect::Rectangle, transform::AffineTransform, vector2::Vector2}; +use math2::{rect, rect::Rectangle, transform::AffineTransform, vector2::Vector2}; use serde_json::Value; use skia_safe::{Matrix, Surface}; use std::sync::Arc; +/// Target for bounding box queries. +pub enum BoundsTarget<'a> { + /// A specific node by its user-facing ID. + Node(&'a str), + /// The active scene — returns the union of all scene root children's bounds. + Scene, +} + +impl<'a> BoundsTarget<'a> { + /// Parse from a string, recognizing `""` as the scene target. + pub fn from_str(s: &'a str) -> Self { + if s == "" { + Self::Scene + } else { + Self::Node(s) + } + } +} + pub trait ApplicationApi { fn tick(&mut self, time: f64); fn resize(&mut self, width: u32, height: u32); @@ -54,7 +74,7 @@ pub trait ApplicationApi { fn get_node_ids_from_point(&mut self, point: Vector2) -> Vec; /// returns all node ids intersecting with the envelope in canvas space. fn get_node_ids_from_envelope(&mut self, envelope: Rectangle) -> Vec; - fn get_node_absolute_bounding_box(&mut self, id: &str) -> Option; + fn get_node_absolute_bounding_box(&mut self, target: BoundsTarget) -> Option; fn export_node_as(&mut self, id: &str, format: ExportAs) -> Option; fn to_vector_network(&mut self, id: &str) -> Option; @@ -83,6 +103,12 @@ pub trait ApplicationApi { flags: crate::runtime::render_policy::RenderPolicyFlags, ); + /// Skip layout computation during scene loading. + /// + /// When enabled, `load_scene` derives layout from schema positions/sizes + /// instead of running the Taffy flexbox engine. Set **before** loading a scene. + fn runtime_renderer_set_skip_layout(&mut self, skip: bool); + /// Enable or disable rendering of tile overlays. fn devtools_rendering_set_show_tiles(&mut self, debug: bool); fn devtools_rendering_set_show_fps_meter(&mut self, show: bool); @@ -245,6 +271,11 @@ impl ApplicationApi for UnknownTargetApplication { } /// Update backing resources after a window resize. + /// + /// Only recreates the GPU surface and updates viewport/camera state. + /// Cache invalidation is deferred to [`apply_changes`] at the start + /// of the next frame — this avoids the expensive full-cache nuke + /// that made resize drag janky (45ms/cycle → within 16ms budget). fn resize(&mut self, width: u32, height: u32) { self.state.resize(width as i32, height as i32); self.renderer.backend = self.state.backend(); @@ -259,10 +290,9 @@ impl ApplicationApi for UnknownTargetApplication { height: height as f32, }); - // Rebuild caches - ICB layout computed automatically from viewport context - self.renderer.rebuild_scene_caches(); - - self.renderer.invalidate_cache(); + // Declare *what* changed; apply_changes() in frame() will handle + // the correct invalidation (viewport caches only, no full nuke). + self.renderer.mark_changed(ChangeFlags::VIEWPORT_SIZE); self.queue(); } @@ -401,12 +431,31 @@ impl ApplicationApi for UnknownTargetApplication { self.internal_ids_to_user(internal_ids) } - fn get_node_absolute_bounding_box(&mut self, id: &str) -> Option { - let internal_id = self.user_id_to_internal(id)?; - self.renderer - .get_cache() - .geometry() - .get_world_bounds(&internal_id) + fn get_node_absolute_bounding_box(&mut self, target: BoundsTarget) -> Option { + match target { + BoundsTarget::Scene => { + let scene = self.renderer.scene.as_ref()?; + let geometry = self.renderer.get_cache().geometry(); + let roots = scene.graph.roots(); + let mut union: Option = None; + for root_id in roots { + if let Some(bounds) = geometry.get_world_bounds(root_id) { + union = Some(match union { + Some(u) => rect::union(&[u, bounds]), + None => bounds, + }); + } + } + union + } + BoundsTarget::Node(id) => { + let internal_id = self.user_id_to_internal(id)?; + self.renderer + .get_cache() + .geometry() + .get_world_bounds(&internal_id) + } + } } fn export_node_as(&mut self, id: &str, format: ExportAs) -> Option { @@ -430,7 +479,11 @@ impl ApplicationApi for UnknownTargetApplication { if let Ok(node) = scene.graph.get_node(&internal_id) { /// Convert a positive corner radius to `Some`, zero/negative to `None`. fn nonzero_radius(r: f32) -> Option { - if r > 0.0 { Some(r) } else { None } + if r > 0.0 { + Some(r) + } else { + None + } } let result: Option<(VectorNetwork, Option)> = match node { @@ -521,6 +574,10 @@ impl ApplicationApi for UnknownTargetApplication { self.queue(); } + fn runtime_renderer_set_skip_layout(&mut self, skip: bool) { + self.renderer.set_skip_layout(skip); + } + fn devtools_rendering_set_show_tiles(&mut self, debug: bool) { self.devtools_rendering_show_tiles = debug; } @@ -601,7 +658,10 @@ impl ApplicationApi for UnknownTargetApplication { eprintln!( "switch_scene: scene '{}' not found (available: {:?})", scene_id, - self.loaded_scenes.iter().map(|(id, _)| id).collect::>() + self.loaded_scenes + .iter() + .map(|(id, _)| id) + .collect::>() ); } } @@ -691,11 +751,15 @@ impl UnknownTargetApplication { // borrowing `self` as a whole. let (hit_tester, response) = if let Some(scene) = self.renderer.scene.as_ref() { let ht = crate::hittest::HitTester::with_graph(self.renderer.get_cache(), &scene.graph); - let r = self.surface.dispatch(event, &ht, &scene.graph, &self.ui_hit_regions); + let r = self + .surface + .dispatch(event, &ht, &scene.graph, &self.ui_hit_regions); (ht, r) } else { let ht = crate::hittest::HitTester::new(self.renderer.get_cache()); - let r = self.surface.dispatch(event, &ht, &NoHierarchy, &self.ui_hit_regions); + let r = self + .surface + .dispatch(event, &ht, &NoHierarchy, &self.ui_hit_regions); (ht, r) }; drop(hit_tester); @@ -882,8 +946,8 @@ impl UnknownTargetApplication { text_edit: None, text_edit_decorations: None, surface: crate::surface::SurfaceState::new(), - surface_overlay_config: - crate::devtools::surface_overlay::SurfaceOverlayConfig::default(), + surface_overlay_config: crate::devtools::surface_overlay::SurfaceOverlayConfig::default( + ), ui_hit_regions: crate::surface::ui::HitRegions::new(), } } @@ -1059,15 +1123,18 @@ impl UnknownTargetApplication { // Prepare camera change for the renderer let camera_change = self.renderer.camera.change_kind(); - if camera_change.zoom_changed() { - self.renderer.invalidate_compositor_on_zoom(); - } // Promote to stable quality when the camera didn't change — there is // no reason to render at reduced resolution for non-camera // invalidations (hit-test highlight, scene edits, etc.). let stable = quality == FrameQuality::Stable || !camera_change.any_changed(); + // Central invalidation dispatch: consume all accumulated changes + // (viewport resize, font/image loads, text edits, config changes) + // and camera state in one place. This replaces the ad-hoc + // invalidate_compositor_on_zoom() call and all per-site cache nuking. + self.renderer.apply_changes(camera_change, stable); + // Warm the camera cache once per frame so view_matrix(), rect(), and // screen_to_canvas_point() are essentially free for the rest of this frame. self.renderer.camera.warm_cache(); @@ -1075,7 +1142,9 @@ impl UnknownTargetApplication { // Build frame plan lazily let rect = self.renderer.camera.rect(); let zoom = self.renderer.camera.get_zoom(); - let plan = self.renderer.build_frame_plan(rect, zoom, stable, camera_change); + let plan = self + .renderer + .build_frame_plan(rect, zoom, stable, camera_change); // Consume the camera change so the next frame sees None // (unless a new mutation occurs before then). @@ -1114,7 +1183,7 @@ impl UnknownTargetApplication { updated = true; } if updated { - self.renderer.invalidate_cache(); + self.renderer.mark_changed(ChangeFlags::IMAGE_LOADED); } } @@ -1138,7 +1207,7 @@ impl UnknownTargetApplication { updated = true; } if updated { - self.renderer.invalidate_cache(); + self.renderer.mark_changed(ChangeFlags::FONT_LOADED); if font_count > 0 { self.print_font_repository_info(); } @@ -1229,7 +1298,7 @@ impl UnknownTargetApplication { pub fn set_default_fallback_fonts(&mut self, fonts: Vec) { self.renderer.fonts.set_user_fallback_families(fonts); - self.renderer.invalidate_cache(); + self.renderer.mark_changed(ChangeFlags::FONT_LOADED); } pub fn get_default_fallback_fonts(&self) -> Vec { @@ -1246,7 +1315,7 @@ impl UnknownTargetApplication { /// supported (e.g. Regular, Bold, Italic per family). pub fn add_font(&mut self, family: &str, data: &[u8]) { self.renderer.add_font(family, data); - self.renderer.invalidate_cache(); + self.renderer.mark_changed(ChangeFlags::FONT_LOADED); } /// Register image bytes with the renderer and return metadata. @@ -1542,14 +1611,7 @@ impl UnknownTargetApplication { &self.renderer.fonts, ); - let te = ActiveTextEdit::new( - node_id, - text, - text_style_rec, - fill, - paragraph_style, - layout, - ); + let te = ActiveTextEdit::new(node_id, text, text_style_rec, fill, paragraph_style, layout); self.text_edit = Some(te); self.text_edit_refresh_decorations(); @@ -1580,7 +1642,9 @@ impl UnknownTargetApplication { /// Returns the current text of the active editing session, or `None` /// if no session is active. pub fn text_edit_get_text(&self) -> Option<&str> { - self.text_edit.as_ref().map(|te| te.session.state.text.as_str()) + self.text_edit + .as_ref() + .map(|te| te.session.state.text.as_str()) } /// Dispatch an editing command. @@ -1595,10 +1659,7 @@ impl UnknownTargetApplication { /// /// Returns `true` if the session had something to undo. pub fn text_edit_undo(&mut self) -> bool { - let performed = self - .text_edit - .as_mut() - .is_some_and(|te| te.session.undo()); + let performed = self.text_edit.as_mut().is_some_and(|te| te.session.undo()); self.text_edit_refresh_decorations(); performed } @@ -1607,10 +1668,7 @@ impl UnknownTargetApplication { /// /// Returns `true` if the session had something to redo. pub fn text_edit_redo(&mut self) -> bool { - let performed = self - .text_edit - .as_mut() - .is_some_and(|te| te.session.redo()); + let performed = self.text_edit.as_mut().is_some_and(|te| te.session.redo()); self.text_edit_refresh_decorations(); performed } @@ -1682,7 +1740,9 @@ impl UnknownTargetApplication { /// Paste plain text. pub fn text_edit_paste_text(&mut self, text: &str) { - if text.is_empty() { return; } + if text.is_empty() { + return; + } if let Some(te) = self.text_edit.as_mut() { te.session.apply_with_kind( grida_text_edit::EditingCommand::Insert(text.to_owned()), @@ -1694,10 +1754,13 @@ impl UnknownTargetApplication { /// Paste HTML with formatting. pub fn text_edit_paste_html(&mut self, html: &str) { - if html.is_empty() { return; } + if html.is_empty() { + return; + } if let Some(te) = self.text_edit.as_mut() { let base_style = te.session.content.default_style().clone(); - match grida_text_edit::attributed_text::html::html_to_attributed_text(html, base_style) { + match grida_text_edit::attributed_text::html::html_to_attributed_text(html, base_style) + { Ok(pasted) if !pasted.is_empty() => { te.session.paste_attributed(&pasted); } @@ -1717,43 +1780,62 @@ impl UnknownTargetApplication { pub fn text_edit_get_selection_rects(&mut self) -> Option> { let te = self.text_edit.as_mut()?; let (lo, hi) = te.session.selection_range()?; - let rects = te.session.layout.selection_rects_for_range(&te.session.state.text, lo, hi); - if rects.is_empty() { None } else { Some(rects) } + let rects = te + .session + .layout + .selection_rects_for_range(&te.session.state.text, lo, hi); + if rects.is_empty() { + None + } else { + Some(rects) + } } /// Toggle bold on selection/caret. pub fn text_edit_toggle_bold(&mut self) { - if let Some(te) = self.text_edit.as_mut() { te.session.toggle_bold(); } + if let Some(te) = self.text_edit.as_mut() { + te.session.toggle_bold(); + } self.text_edit_after_style_change(); } /// Toggle italic on selection/caret. pub fn text_edit_toggle_italic(&mut self) { - if let Some(te) = self.text_edit.as_mut() { te.session.toggle_italic(); } + if let Some(te) = self.text_edit.as_mut() { + te.session.toggle_italic(); + } self.text_edit_after_style_change(); } /// Toggle underline on selection/caret. pub fn text_edit_toggle_underline(&mut self) { - if let Some(te) = self.text_edit.as_mut() { te.session.toggle_underline(); } + if let Some(te) = self.text_edit.as_mut() { + te.session.toggle_underline(); + } self.text_edit_after_style_change(); } /// Toggle strikethrough on selection/caret. pub fn text_edit_toggle_strikethrough(&mut self) { - if let Some(te) = self.text_edit.as_mut() { te.session.toggle_strikethrough(); } + if let Some(te) = self.text_edit.as_mut() { + te.session.toggle_strikethrough(); + } self.text_edit_after_style_change(); } /// Set font size on selection/caret. pub fn text_edit_set_font_size(&mut self, size: f32) { - if let Some(te) = self.text_edit.as_mut() { te.session.set_font_size(size); } + if let Some(te) = self.text_edit.as_mut() { + te.session.set_font_size(size); + } self.text_edit_after_style_change(); } /// Set font family on selection/caret. pub fn text_edit_set_font_family(&mut self, family: &str) { - if let Some(te) = self.text_edit.as_mut() { te.session.set_font_family(family); } + if let Some(te) = self.text_edit.as_mut() { + te.session.set_font_family(family); + } self.text_edit_after_style_change(); } @@ -1846,10 +1928,23 @@ impl UnknownTargetApplication { }; let caret = te.session.caret_rect(); let visible = te.session.should_show_caret(); - let selection_rects = te.session.selection_range().map(|(lo, hi)| { - te.session.layout.selection_rects_for_range(&te.session.state.text, lo, hi) - }).unwrap_or_default(); - (node_id, paragraph_height, display_text, caret, visible, selection_rects) + let selection_rects = te + .session + .selection_range() + .map(|(lo, hi)| { + te.session + .layout + .selection_rects_for_range(&te.session.state.text, lo, hi) + }) + .unwrap_or_default(); + ( + node_id, + paragraph_height, + display_text, + caret, + visible, + selection_rects, + ) }); if let Some((node_id, paragraph_height, display_text, caret, visible, selection_rects)) = @@ -1857,8 +1952,7 @@ impl UnknownTargetApplication { { self.renderer.update_layer_text(node_id, &display_text); - let y_offset = - Self::compute_text_y_offset(&self.renderer, node_id, paragraph_height); + let y_offset = Self::compute_text_y_offset(&self.renderer, node_id, paragraph_height); use crate::devtools::text_edit_decoration_overlay::{ CaretDecoration, TextEditingDecorations, @@ -1866,7 +1960,10 @@ impl UnknownTargetApplication { self.text_edit_decorations = Some(TextEditingDecorations { node_id, - caret: Some(CaretDecoration { rect: caret, visible }), + caret: Some(CaretDecoration { + rect: caret, + visible, + }), selection_rects, y_offset, }); @@ -1875,11 +1972,7 @@ impl UnknownTargetApplication { } /// Compute the text vertical alignment offset for a text node. - fn compute_text_y_offset( - renderer: &Renderer, - node_id: NodeId, - paragraph_height: f32, - ) -> f32 { + fn compute_text_y_offset(renderer: &Renderer, node_id: NodeId, paragraph_height: f32) -> f32 { use crate::node::schema::Node; let scene = match renderer.scene.as_ref() { Some(s) => s, diff --git a/crates/grida-canvas/src/window/application_emscripten.rs b/crates/grida-canvas/src/window/application_emscripten.rs index 615f3a34c3..ca1f564574 100644 --- a/crates/grida-canvas/src/window/application_emscripten.rs +++ b/crates/grida-canvas/src/window/application_emscripten.rs @@ -3,10 +3,10 @@ use crate::io::io_grida_patch::TransactionApplyReport; #[cfg(not(target_arch = "wasm32"))] use crate::resources::{FontMessage, ImageMessage}; use crate::runtime::camera::Camera2D; +use crate::runtime::changes::ChangeFlags; use crate::runtime::scene::Backend; use crate::runtime::scene::RendererOptions; -use crate::window::application::ApplicationApi; -use crate::window::application::UnknownTargetApplication; +use crate::window::application::{ApplicationApi, BoundsTarget, UnknownTargetApplication}; use crate::window::command::ApplicationCommand; use crate::window::state::{self, GpuState, SurfaceState}; #[cfg(not(target_arch = "wasm32"))] @@ -212,8 +212,8 @@ impl ApplicationApi for EmscriptenApplication { self.base.get_node_ids_from_envelope(rect) } - fn get_node_absolute_bounding_box(&mut self, id: &str) -> Option { - self.base.get_node_absolute_bounding_box(id) + fn get_node_absolute_bounding_box(&mut self, target: BoundsTarget) -> Option { + self.base.get_node_absolute_bounding_box(target) } fn export_node_as( @@ -247,6 +247,10 @@ impl ApplicationApi for EmscriptenApplication { self.base.runtime_renderer_set_render_policy_flags(flags); } + fn runtime_renderer_set_skip_layout(&mut self, skip: bool) { + self.base.runtime_renderer_set_skip_layout(skip); + } + fn set_main_camera_transform(&mut self, transform: AffineTransform) { self.base.set_main_camera_transform(transform); } @@ -403,10 +407,10 @@ impl EmscriptenApplication { /// supported (e.g. Regular, Bold, Italic per family). pub fn add_font(&mut self, family: &str, data: &[u8]) { self.base.renderer.add_font(family, data); - // Newly registered fonts may affect cached text layout; invalidate any - // existing cache so that the renderer re-computes geometry using the - // new typeface. - self.base.renderer.invalidate_cache(); + // Newly registered fonts may affect cached text layout; the central + // apply_changes() dispatcher will invalidate paragraph + picture + + // compositor caches on the next frame. + self.base.renderer.mark_changed(ChangeFlags::FONT_LOADED); } /// Register an image with the renderer and return metadata. diff --git a/crates/grida-canvas/tests/mipmap.rs b/crates/grida-canvas/tests/mipmap.rs index 3d27148fd6..b116893484 100644 --- a/crates/grida-canvas/tests/mipmap.rs +++ b/crates/grida-canvas/tests/mipmap.rs @@ -1,53 +1,53 @@ -use cg::cache::mipmap::{ImageMipmaps, MipmapConfig, MipmapLevels}; use skia_safe::surfaces; +/// Verifies that `Image::with_default_mipmaps()` produces a mipmapped image. #[test] -fn full_chain_down_to_1x1() { - let width = 300; - let height = 200; - - let mut surface = surfaces::raster_n32_premul((width, height)).unwrap(); +fn skia_with_default_mipmaps_works() { + let mut surface = surfaces::raster_n32_premul((512, 512)).unwrap(); let image = surface.image_snapshot(); - let config = MipmapConfig { - levels: MipmapLevels::FullChain, - chained: true, - }; - let mip = ImageMipmaps::from_image(image, &config); - let expected_levels = (width.max(height) as f32).log2().ceil() as usize + 1; - assert_eq!(mip.level_count(), expected_levels); + let mipmapped = image.with_default_mipmaps(); + assert!( + mipmapped.is_some(), + "with_default_mipmaps() should succeed for a 512×512 image" + ); - let last = mip.last_level_image().unwrap(); - assert!(last.width() <= 1 && last.height() <= 1); + let mipmapped = mipmapped.unwrap(); + // The mipmapped image should have the same dimensions as the original. + assert_eq!(mipmapped.width(), 512); + assert_eq!(mipmapped.height(), 512); } +/// Verifies that mipmapped images can be used to create shaders with mipmap sampling. #[test] -fn best_for_size_selects_correct_level() { - let mut surface = surfaces::raster_n32_premul((300, 200)).unwrap(); +fn mipmapped_image_shader_creation() { + use skia_safe::{FilterMode, MipmapMode, SamplingOptions, TileMode}; + + let mut surface = surfaces::raster_n32_premul((256, 256)).unwrap(); let image = surface.image_snapshot(); + let mipmapped = image.with_default_mipmaps().unwrap(); - let config = MipmapConfig { - levels: MipmapLevels::FullChain, - chained: true, - }; - let mip = ImageMipmaps::from_image(image, &config); - let level = mip.best_for_size(100.0, 100.0).unwrap(); + let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::Nearest); + let tile_modes = (TileMode::Decal, TileMode::Decal); - assert_eq!(level.width(), 150); - assert_eq!(level.height(), 100); + let shader = mipmapped.to_shader(Some(tile_modes), sampling, None); + assert!( + shader.is_some(), + "Shader creation should succeed with mipmapped image" + ); } +/// Verifies that 1×1 images handle mipmaps gracefully. #[test] -fn handles_nan_in_levels() { - let mut surface = surfaces::raster_n32_premul((100, 100)).unwrap(); +fn tiny_image_mipmaps() { + let mut surface = surfaces::raster_n32_premul((1, 1)).unwrap(); let image = surface.image_snapshot(); - let config = MipmapConfig { - levels: MipmapLevels::Fixed(vec![1.0, f32::NAN, 0.5]), - chained: true, - }; - - // should not panic - let mip = ImageMipmaps::from_image(image, &config); - assert_eq!(mip.level_count(), 3); + // with_default_mipmaps() may return None for 1×1 images — that's fine, + // the fallback in ImageRepository handles this. + let result = image.with_default_mipmaps(); + // Either succeeds or falls back gracefully. + let img = result.unwrap_or(image); + assert_eq!(img.width(), 1, "Fallback image should be 1px wide"); + assert_eq!(img.height(), 1, "Fallback image should be 1px tall"); } diff --git a/crates/grida-canvas/tests/resources_image_rid.rs b/crates/grida-canvas/tests/resources_image_rid.rs index 30e0ef7ce6..a9b5e749fa 100644 --- a/crates/grida-canvas/tests/resources_image_rid.rs +++ b/crates/grida-canvas/tests/resources_image_rid.rs @@ -19,6 +19,7 @@ fn add_image_with_rid_succeeds_and_retrieval_works() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, 64.0, 64.0)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); @@ -26,10 +27,16 @@ fn add_image_with_rid_succeeds_and_retrieval_works() { let result = renderer.add_image_with_rid(IMAGE_DATA, rid); assert!(result.is_some(), "add_image_with_rid should succeed"); let (width, height, _) = result.unwrap(); - assert!(width > 0 && height > 0, "image dimensions should be positive"); + assert!( + width > 0 && height > 0, + "image dimensions should be positive" + ); let bytes = renderer.get_image_bytes(rid); - assert!(bytes.is_some(), "get_image_bytes by full RID should return bytes"); + assert!( + bytes.is_some(), + "get_image_bytes by full RID should return bytes" + ); assert_eq!(bytes.as_ref().unwrap(), IMAGE_DATA); let bytes_normalized = renderer.get_image_bytes("test-logo"); @@ -48,15 +55,20 @@ fn add_image_with_rid_rejects_invalid_rid() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, 64.0, 64.0)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); assert!( - renderer.add_image_with_rid(IMAGE_DATA, "invalid-no-prefix").is_none(), + renderer + .add_image_with_rid(IMAGE_DATA, "invalid-no-prefix") + .is_none(), "rid without res:// or system:// should be rejected" ); assert!( - renderer.add_image_with_rid(IMAGE_DATA, "mem://abc123").is_none(), + renderer + .add_image_with_rid(IMAGE_DATA, "mem://abc123") + .is_none(), "mem:// is content-addressed; custom RID should use res:// or system://" ); } @@ -98,6 +110,7 @@ fn add_image_with_rid_scene_renders() { Camera2D::new_from_bounds(Rectangle::from_xywh(0.0, 0.0, 64.0, 64.0)), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); diff --git a/crates/grida-dev/AGENTS.md b/crates/grida-dev/AGENTS.md index 7c8b75eaef..8af1d82f2c 100644 --- a/crates/grida-dev/AGENTS.md +++ b/crates/grida-dev/AGENTS.md @@ -20,6 +20,10 @@ cargo run -p grida-dev -- icon.svg # Headless GPU benchmark (no window, prints per-frame stats) cargo run -p grida-dev --release -- bench ./fixtures/test-grida/bench.grida cargo run -p grida-dev --release -- bench --size 100 + +# Resize benchmark — measures resize() + redraw() cost per cycle +cargo run -p grida-dev --release -- bench ./fixtures/test-grida/bench.grida --resize +cargo run -p grida-dev --release -- bench --size 100 --resize --frames 50 ``` ## Performance measurement diff --git a/crates/grida-dev/examples/sys_camera.rs b/crates/grida-dev/examples/sys_camera.rs index 46e4fc4144..a0bf211999 100644 --- a/crates/grida-dev/examples/sys_camera.rs +++ b/crates/grida-dev/examples/sys_camera.rs @@ -236,6 +236,7 @@ fn main() { }), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); diff --git a/crates/grida-dev/src/bench/args.rs b/crates/grida-dev/src/bench/args.rs index 0a61c9ee1f..c8e3b98307 100644 --- a/crates/grida-dev/src/bench/args.rs +++ b/crates/grida-dev/src/bench/args.rs @@ -22,6 +22,9 @@ pub struct BenchArgs { /// Viewport height. #[arg(long = "height", default_value_t = 1000)] pub height: i32, + /// Run the resize benchmark (alternates between two viewport sizes). + #[arg(long = "resize", default_value_t = false)] + pub resize: bool, } #[derive(Args, Debug)] diff --git a/crates/grida-dev/src/bench/load_bench.rs b/crates/grida-dev/src/bench/load_bench.rs new file mode 100644 index 0000000000..c03f9a7560 --- /dev/null +++ b/crates/grida-dev/src/bench/load_bench.rs @@ -0,0 +1,543 @@ +//! `load-bench` — measures `load_scene()` per-stage timings. +//! +//! This subcommand loads one or more `.grida` files and reports the time +//! spent in each stage of `Renderer::load_scene()`: +//! - fonts: collecting font families +//! - layout: Taffy flexbox + Skia paragraph measurement +//! - geometry: DFS transform/bounds propagation +//! - effects: effect tree classification +//! - layers: flatten + clip path + sort + RTree +//! +//! The breakdown helps identify which stage dominates cold-start cost. +//! +//! # Usage +//! +//! ```sh +//! cargo run -p grida-dev --release -- load-bench path/to/file.grida +//! cargo run -p grida-dev --release -- load-bench path/to/file.grida --scene 0 +//! cargo run -p grida-dev --release -- load-bench path/to/file.grida --list-scenes +//! cargo run -p grida-dev --release -- load-bench path/to/file.grida --iterations 5 +//! ``` + +use super::runner::AsyncSceneLoader; +use anyhow::{anyhow, Result}; +use cg::cache; +use cg::layout::engine::LayoutEngine; +use cg::node::schema::{Scene, Size}; +use cg::resources::ByteStore; +use cg::runtime::camera::Camera2D; +use cg::runtime::font_repository::FontRepository; +use cg::runtime::scene::{Backend, Renderer}; +use clap::Args; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +#[derive(Args, Debug)] +pub struct LoadBenchArgs { + /// Path to a `.grida` file. + pub path: String, + /// Scene index to benchmark (0-based). Use --list-scenes to see available. + #[arg(long = "scene")] + pub scene_index: Option, + /// List available scene names and exit. + #[arg(long = "list-scenes", default_value_t = false)] + pub list_scenes: bool, + /// Number of iterations for averaging. + #[arg(long = "iterations", default_value_t = 3)] + pub iterations: usize, + /// Viewport width. + #[arg(long = "width", default_value_t = 1000)] + pub width: i32, + /// Viewport height. + #[arg(long = "height", default_value_t = 1000)] + pub height: i32, + /// Skip Skia text measurement (returns zero-size stubs). For A/B comparison. + #[arg(long = "skip-text", default_value_t = false)] + pub skip_text: bool, + /// Skip Taffy layout entirely — derive layout from schema positions/sizes. + /// Simulates `RuntimeRendererConfig::skip_layout = true`. + #[arg(long = "skip-layout", default_value_t = false)] + pub skip_layout: bool, + /// Compare layout results between full Taffy and schema-only paths. + /// Reports all nodes where the computed layout differs. + #[arg(long = "layout-diff", default_value_t = false)] + pub layout_diff: bool, + /// Threshold for layout-diff: ignore differences smaller than this (in px). + /// Default 0.01. Use e.g. `--layout-diff-threshold 1.0` to ignore sub-pixel diffs. + #[arg(long = "layout-diff-threshold", default_value_t = 0.01)] + pub layout_diff_threshold: f32, +} + +struct StageTimings { + fonts_us: u64, + layout_us: u64, + geometry_us: u64, + effects_us: u64, + layers_us: u64, + total_us: u64, + /// Paragraph measurement stats from the layout phase. + para_stats: cg::cache::paragraph::ParagraphMeasureStats, +} + +/// Measure load_scene by running each stage independently with timing. +fn measure_load_scene( + scene: &Scene, + width: i32, + height: i32, + skip_text: bool, + skip_layout: bool, +) -> StageTimings { + let viewport_size = Size { + width: width as f32, + height: height as f32, + }; + let store = Arc::new(Mutex::new(ByteStore::new())); + let fonts = FontRepository::new(store); + + let t_total = Instant::now(); + + // Stage 0: Collect font families + let t0 = Instant::now(); + { + let requested = cg::runtime::scene::collect_scene_font_families(scene); + // Simulate set_requested_families cost (just drop the result) + std::hint::black_box(requested); + } + let fonts_us = t0.elapsed().as_micros() as u64; + + // Stage 1: Layout + let t1 = Instant::now(); + let mut engine = LayoutEngine::new(); + let mut paragraph_cache = cache::paragraph::ParagraphCache::new(); + paragraph_cache.skip_text_measure = skip_text; + if skip_layout { + engine.compute_schema_only(scene); + } else { + engine.compute( + scene, + viewport_size, + Some(cg::layout::tree::TextMeasureProvider { + paragraph_cache: &mut paragraph_cache, + fonts: &fonts, + }), + ); + } + let layout_us = t1.elapsed().as_micros() as u64; + let para_stats = paragraph_cache.stats.clone(); + + // Stage 2: Geometry + let t2 = Instant::now(); + let layout_result = engine.result(); + let mut scene_cache = cache::scene::SceneCache::new(); + scene_cache.update_geometry_with_layout(scene, &fonts, layout_result, viewport_size); + let geometry_us = t2.elapsed().as_micros() as u64; + + // Stage 3: Effect tree + let t3 = Instant::now(); + scene_cache.update_effect_tree(scene); + let effects_us = t3.elapsed().as_micros() as u64; + + // Stage 4: Layers + let t4 = Instant::now(); + scene_cache.update_layers(scene); + let layers_us = t4.elapsed().as_micros() as u64; + + let total_us = t_total.elapsed().as_micros() as u64; + + StageTimings { + fonts_us, + layout_us, + geometry_us, + effects_us, + layers_us, + total_us, + para_stats, + } +} + +/// Compare layout results between full Taffy path and schema-only path. +/// Reports every node where the computed layout differs beyond `threshold` px. +fn layout_diff(scene: &Scene, width: i32, height: i32, threshold: f32) { + let viewport_size = Size { + width: width as f32, + height: height as f32, + }; + let store = Arc::new(Mutex::new(ByteStore::new())); + let fonts = FontRepository::new(store); + + // Full Taffy path + let mut engine_full = LayoutEngine::new(); + let mut paragraph_cache = cache::paragraph::ParagraphCache::new(); + engine_full.compute( + scene, + viewport_size, + Some(cg::layout::tree::TextMeasureProvider { + paragraph_cache: &mut paragraph_cache, + fonts: &fonts, + }), + ); + let result_full = engine_full.result().clone(); + + // Schema-only path + let mut engine_schema = LayoutEngine::new(); + engine_schema.compute_schema_only(scene); + let result_schema = engine_schema.result(); + + // Compare + let mut diffs = Vec::new(); + let eps = threshold; + for (id, full) in result_full.iter() { + if let Some(schema) = result_schema.get(id) { + let dx = (full.x - schema.x).abs(); + let dy = (full.y - schema.y).abs(); + let dw = (full.width - schema.width).abs(); + let dh = (full.height - schema.height).abs(); + if dx > eps || dy > eps || dw > eps || dh > eps { + let node_type = scene + .graph + .get_node(id) + .map(|n| format!("{:?}", std::mem::discriminant(n))) + .unwrap_or_else(|_| "?".to_string()); + + // Get node type name cleanly + let type_name = scene + .graph + .get_node(id) + .map(|n| match n { + cg::node::schema::Node::Container(_) => "Container", + cg::node::schema::Node::Rectangle(_) => "Rectangle", + cg::node::schema::Node::Ellipse(_) => "Ellipse", + cg::node::schema::Node::Image(_) => "Image", + cg::node::schema::Node::Line(_) => "Line", + cg::node::schema::Node::Polygon(_) => "Polygon", + cg::node::schema::Node::RegularPolygon(_) => "RegularPolygon", + cg::node::schema::Node::RegularStarPolygon(_) => "RegularStarPolygon", + cg::node::schema::Node::TextSpan(_) => "TextSpan", + cg::node::schema::Node::Vector(_) => "Vector", + cg::node::schema::Node::Path(_) => "Path", + cg::node::schema::Node::Group(_) => "Group", + cg::node::schema::Node::BooleanOperation(_) => "BoolOp", + cg::node::schema::Node::InitialContainer(_) => "ICB", + cg::node::schema::Node::Error(_) => "Error", + }) + .unwrap_or("?"); + let _ = node_type; // suppress unused + + // Check if parent is a container (to understand context) + let parent_info = scene + .graph + .get_parent(id) + .and_then(|pid| { + scene.graph.get_node(&pid).ok().map(|p| match p { + cg::node::schema::Node::Container(_) => "Container", + cg::node::schema::Node::Group(_) => "Group", + cg::node::schema::Node::BooleanOperation(_) => "BoolOp", + cg::node::schema::Node::InitialContainer(_) => "ICB", + _ => "Leaf", + }) + }) + .unwrap_or("root"); + + diffs.push(( + *id, + type_name, + parent_info, + *full, + *schema, + (dx, dy, dw, dh), + )); + } + } else { + // Node in full but not in schema — should not happen + eprintln!( + " WARN: node {:?} in full result but missing from schema result", + id + ); + } + } + + // Also check for nodes in schema but not in full + for (id, _) in result_schema.iter() { + if result_full.get(id).is_none() { + eprintln!( + " WARN: node {:?} in schema result but missing from full result", + id + ); + } + } + + if diffs.is_empty() { + println!( + " layout-diff: IDENTICAL ({} nodes compared, threshold={:.2}px)", + result_full.len(), + threshold + ); + } else { + // Sort by largest delta for readability + diffs.sort_by(|a, b| { + let max_a = a.5 .0.max(a.5 .1).max(a.5 .2).max(a.5 .3); + let max_b = b.5 .0.max(b.5 .1).max(b.5 .2).max(b.5 .3); + max_b.partial_cmp(&max_a).unwrap() + }); + + println!( + " layout-diff: {} DIFFERENCES out of {} nodes (threshold={:.2}px):\n", + diffs.len(), + result_full.len(), + threshold + ); + println!( + " {:>8} {:>12} {:>8} {:>36} {:>36} {:>24}", + "NodeId", + "Type", + "Parent", + "Full (x, y, w, h)", + "Schema (x, y, w, h)", + "Delta (dx, dy, dw, dh)" + ); + println!(" {}", "-".repeat(130)); + + let show_limit = 50; + for (i, (id, type_name, parent_info, full, schema, delta)) in diffs.iter().enumerate() { + if i >= show_limit { + println!(" ... and {} more", diffs.len() - show_limit); + break; + } + println!( + " {:>8} {:>12} {:>8} ({:>8.1}, {:>8.1}, {:>8.1}, {:>8.1}) ({:>8.1}, {:>8.1}, {:>8.1}, {:>8.1}) ({:>+7.1}, {:>+7.1}, {:>+7.1}, {:>+7.1})", + id, type_name, parent_info, + full.x, full.y, full.width, full.height, + schema.x, schema.y, schema.width, schema.height, + delta.0, delta.1, delta.2, delta.3, + ); + } + + // Summary by node type + let mut type_counts: std::collections::HashMap<&str, usize> = + std::collections::HashMap::new(); + for (_, type_name, _, _, _, _) in &diffs { + *type_counts.entry(type_name).or_default() += 1; + } + println!("\n By node type:"); + let mut type_vec: Vec<_> = type_counts.into_iter().collect(); + type_vec.sort_by(|a, b| b.1.cmp(&a.1)); + for (t, count) in &type_vec { + println!(" {}: {}", t, count); + } + + // Examples per type (3 each) + println!("\n Examples per type:"); + for (t, _) in &type_vec { + let examples: Vec<_> = diffs + .iter() + .filter(|(_, tn, _, _, _, _)| tn == t) + .take(3) + .collect(); + for (id, type_name, parent_info, full, schema, delta) in examples { + println!( + " {:>8} {:>12} {:>8} ({:>8.1}, {:>8.1}, {:>8.1}, {:>8.1}) ({:>8.1}, {:>8.1}, {:>8.1}, {:>8.1}) ({:>+7.1}, {:>+7.1}, {:>+7.1}, {:>+7.1})", + id, type_name, parent_info, + full.x, full.y, full.width, full.height, + schema.x, schema.y, schema.width, schema.height, + delta.0, delta.1, delta.2, delta.3, + ); + } + } + + // Summary by parent type + let mut parent_counts: std::collections::HashMap<&str, usize> = + std::collections::HashMap::new(); + for (_, _, parent_info, _, _, _) in &diffs { + *parent_counts.entry(parent_info).or_default() += 1; + } + println!(" By parent type:"); + let mut parent_vec: Vec<_> = parent_counts.into_iter().collect(); + parent_vec.sort_by(|a, b| b.1.cmp(&a.1)); + for (p, count) in &parent_vec { + println!(" {}: {}", p, count); + } + } +} + +/// Also measure via Renderer::load_scene for an end-to-end comparison. +fn measure_load_scene_via_renderer(scene: Scene, width: i32, height: i32) -> u64 { + let mut renderer = Renderer::new( + Backend::new_from_raster(width, height), + None, + Camera2D::new(Size { + width: width as f32, + height: height as f32, + }), + ); + let t = Instant::now(); + renderer.load_scene(scene); + let elapsed = t.elapsed().as_micros() as u64; + renderer.free(); + elapsed +} + +fn print_timings(label: &str, timings: &StageTimings) { + let total_ms = timings.total_us as f64 / 1000.0; + let fonts_ms = timings.fonts_us as f64 / 1000.0; + let layout_ms = timings.layout_us as f64 / 1000.0; + let geometry_ms = timings.geometry_us as f64 / 1000.0; + let effects_ms = timings.effects_us as f64 / 1000.0; + let layers_ms = timings.layers_us as f64 / 1000.0; + + let pct = |v: f64| -> f64 { v / total_ms * 100.0 }; + + println!(" {label}:"); + println!(" total: {total_ms:>10.1} ms"); + println!( + " fonts: {:>10.1} ms ({:>5.1}%)", + fonts_ms, + pct(fonts_ms) + ); + println!( + " layout: {:>10.1} ms ({:>5.1}%)", + layout_ms, + pct(layout_ms) + ); + println!( + " geometry: {:>10.1} ms ({:>5.1}%)", + geometry_ms, + pct(geometry_ms) + ); + println!( + " effects: {:>10.1} ms ({:>5.1}%)", + effects_ms, + pct(effects_ms) + ); + println!( + " layers: {:>10.1} ms ({:>5.1}%)", + layers_ms, + pct(layers_ms) + ); + + // Paragraph measurement sub-stats (within layout phase) + let ps = &timings.para_stats; + if ps.calls > 0 { + println!(" --- paragraph stats (within layout) ---"); + println!( + " calls: {} (hits: {}, misses: {})", + ps.calls, ps.cache_hits, ps.cache_misses + ); + } +} + +pub async fn run_load_bench(args: LoadBenchArgs, loader: impl AsyncSceneLoader) -> Result<()> { + let scenes = loader.load(&args.path).await?; + + if args.list_scenes { + println!("Available scenes ({}):", scenes.len()); + for (i, s) in scenes.iter().enumerate() { + println!(" [{}] {} ({} nodes)", i, s.name, s.graph.node_count()); + } + return Ok(()); + } + + // Determine which scenes to bench + let scene_indices: Vec = if let Some(idx) = args.scene_index { + if idx >= scenes.len() { + return Err(anyhow!( + "scene index {} out of range (0..{}). Use --list-scenes.", + idx, + scenes.len() + )); + } + vec![idx] + } else { + (0..scenes.len()).collect() + }; + + println!( + "load-bench: {} scene(s), {} iteration(s), viewport {}x{}{}\n", + scene_indices.len(), + args.iterations, + args.width, + args.height, + match (args.skip_text, args.skip_layout) { + (true, true) => ", skip_text=ON, skip_layout=ON", + (true, false) => ", skip_text=ON", + (false, true) => ", skip_layout=ON", + (false, false) => "", + } + ); + + for &idx in &scene_indices { + let scene = &scenes[idx]; + let node_count = scene.graph.node_count(); + + // Count text nodes + let text_count = scene + .graph + .nodes_iter() + .filter(|(_, n)| matches!(n, cg::node::schema::Node::TextSpan(_))) + .count(); + let container_count = scene + .graph + .nodes_iter() + .filter(|(_, n)| matches!(n, cg::node::schema::Node::Container(_))) + .count(); + + println!( + "Scene [{}] \"{}\" — {} nodes ({} containers, {} text spans)", + idx, scene.name, node_count, container_count, text_count + ); + + // Per-stage breakdown (multiple iterations) + let mut all_timings: Vec = Vec::new(); + for i in 0..args.iterations { + let timings = measure_load_scene( + scene, + args.width, + args.height, + args.skip_text, + args.skip_layout, + ); + print_timings(&format!("iter {}", i + 1), &timings); + all_timings.push(timings); + } + + // Averages + if args.iterations > 1 { + let n = all_timings.len() as u64; + let avg = StageTimings { + fonts_us: all_timings.iter().map(|t| t.fonts_us).sum::() / n, + layout_us: all_timings.iter().map(|t| t.layout_us).sum::() / n, + geometry_us: all_timings.iter().map(|t| t.geometry_us).sum::() / n, + effects_us: all_timings.iter().map(|t| t.effects_us).sum::() / n, + layers_us: all_timings.iter().map(|t| t.layers_us).sum::() / n, + total_us: all_timings.iter().map(|t| t.total_us).sum::() / n, + para_stats: cg::cache::paragraph::ParagraphMeasureStats { + calls: all_timings.iter().map(|t| t.para_stats.calls).sum::() / n, + cache_hits: all_timings + .iter() + .map(|t| t.para_stats.cache_hits) + .sum::() + / n, + cache_misses: all_timings + .iter() + .map(|t| t.para_stats.cache_misses) + .sum::() + / n, + }, + }; + print_timings("average", &avg); + } + + // Layout diff A/B comparison + if args.layout_diff { + layout_diff(scene, args.width, args.height, args.layout_diff_threshold); + } + + // End-to-end via Renderer::load_scene + let e2e_us = measure_load_scene_via_renderer(scene.clone(), args.width, args.height); + println!( + " renderer load_scene (e2e): {:.1} ms\n", + e2e_us as f64 / 1000.0 + ); + } + + Ok(()) +} diff --git a/crates/grida-dev/src/bench/mod.rs b/crates/grida-dev/src/bench/mod.rs index 540edd51ac..d4260148d9 100644 --- a/crates/grida-dev/src/bench/mod.rs +++ b/crates/grida-dev/src/bench/mod.rs @@ -1,6 +1,8 @@ pub mod args; +pub mod load_bench; pub mod report; pub mod runner; pub use args::{BenchArgs, BenchReportArgs}; +pub use load_bench::{run_load_bench, LoadBenchArgs}; pub use runner::{run_bench, run_bench_report}; diff --git a/crates/grida-dev/src/bench/runner.rs b/crates/grida-dev/src/bench/runner.rs index 44e7559592..1dc4cd129f 100644 --- a/crates/grida-dev/src/bench/runner.rs +++ b/crates/grida-dev/src/bench/runner.rs @@ -197,6 +197,128 @@ fn measure_settle(renderer: &mut cg::runtime::scene::Renderer) -> u64 { t.elapsed().as_micros() as u64 } +/// Simulate a resize cycle on the Renderer. +/// +/// Reproduces the work that `Application::resize()` + `frame()` does, +/// minus the GPU surface recreation (which is owned by the window/headless host): +/// 1. update_viewport_size + camera.set_size +/// 2. mark_changed(VIEWPORT_SIZE) +/// 3. apply_changes (central dispatch — selective invalidation) +/// 4. queue_unstable + flush (scene repaint with surviving caches) +fn measure_resize( + renderer: &mut cg::runtime::scene::Renderer, + width: i32, + height: i32, +) -> Option<(u64, u64, u64, u64)> { + use cg::runtime::camera::CameraChangeKind; + use cg::runtime::changes::ChangeFlags; + + let t0 = Instant::now(); + + renderer.update_viewport_size(width as f32, height as f32); + renderer.camera.set_size(Size { + width: width as f32, + height: height as f32, + }); + renderer.mark_changed(ChangeFlags::VIEWPORT_SIZE); + + // apply_changes replaces the old rebuild_scene_caches + invalidate_cache + let t_apply = Instant::now(); + renderer.apply_changes(CameraChangeKind::None, false); + let apply_us = t_apply.elapsed().as_micros() as u64; + + // invalidate_us is now effectively zero (no separate step) + let invalidate_us = 0u64; + + renderer.queue_unstable(); + let t_flush = Instant::now(); + match renderer.flush() { + FrameFlushResult::OK(_) => { + let flush_us = t_flush.elapsed().as_micros() as u64; + let total_us = t0.elapsed().as_micros() as u64; + Some((total_us, apply_us, invalidate_us, flush_us)) + } + _ => None, + } +} + +/// Run a resize pass: alternate between two viewport sizes for N iterations. +/// +/// Simulates what happens on every ResizeObserver callback in the browser: +/// the full resize() + redraw() path fires per frame during a window drag. +fn run_resize_pass( + renderer: &mut cg::runtime::scene::Renderer, + frames: u32, + size_a: (i32, i32), + size_b: (i32, i32), +) -> ResizePassStats { + let wall_start = Instant::now(); + let mut total_us_acc = Vec::with_capacity(frames as usize); + let mut rebuild_us_acc = Vec::with_capacity(frames as usize); + let mut invalidate_us_acc = Vec::with_capacity(frames as usize); + let mut flush_us_acc = Vec::with_capacity(frames as usize); + + for i in 0..frames { + let (w, h) = if i % 2 == 0 { size_a } else { size_b }; + if let Some((total, rebuild, invalidate, flush)) = measure_resize(renderer, w, h) { + total_us_acc.push(total); + rebuild_us_acc.push(rebuild); + invalidate_us_acc.push(invalidate); + flush_us_acc.push(flush); + } + } + + let wall = wall_start.elapsed(); + compute_resize_stats( + &total_us_acc, + &rebuild_us_acc, + &invalidate_us_acc, + &flush_us_acc, + wall, + ) +} + +struct ResizePassStats { + avg_us: u64, + min_us: u64, + p50_us: u64, + p95_us: u64, + max_us: u64, + rebuild_us: u64, + invalidate_us: u64, + flush_us: u64, + wall: std::time::Duration, +} + +fn compute_resize_stats( + total: &[u64], + rebuild: &[u64], + invalidate: &[u64], + flush: &[u64], + wall: std::time::Duration, +) -> ResizePassStats { + if total.is_empty() { + return ResizePassStats { + avg_us: 0, min_us: 0, p50_us: 0, p95_us: 0, max_us: 0, + rebuild_us: 0, invalidate_us: 0, flush_us: 0, wall, + }; + } + let mut sorted = total.to_vec(); + sorted.sort(); + let n = sorted.len(); + ResizePassStats { + avg_us: wall.as_micros() as u64 / n as u64, + min_us: sorted[0], + p50_us: sorted[n / 2], + p95_us: sorted[n * 95 / 100], + max_us: sorted[n - 1], + rebuild_us: rebuild.iter().sum::() / n as u64, + invalidate_us: invalidate.iter().sum::() / n as u64, + flush_us: flush.iter().sum::() / n as u64, + wall, + } +} + fn compute_pass_stats( frame_times: &[u64], queue_us_acc: &[u64], @@ -1105,6 +1227,35 @@ pub async fn run_bench( comp_stats.memory_bytes as f64 / 1024.0, ); + // --- Resize benchmark (--resize) --- + if args.resize { + let size_a = (args.width, args.height); + // Second size: ~66% of the primary viewport (simulates browser resize drag) + let size_b = ( + std::cmp::max(1, args.width * 2 / 3), + std::cmp::max(1, args.height * 2 / 3), + ); + println!( + "\n=== Resize benchmark ({} frames, {}x{} <-> {}x{}) ===", + args.frames, size_a.0, size_a.1, size_b.0, size_b.1 + ); + let r = run_resize_pass(&mut renderer, args.frames, size_a, size_b); + println!(" wall: {:>10.1} ms", r.wall.as_micros() as f64 / 1000.0); + println!(" avg: {:>10} us", r.avg_us); + println!(" min: {:>10} us", r.min_us); + println!(" p50: {:>10} us", r.p50_us); + println!(" p95: {:>10} us", r.p95_us); + println!(" MAX: {:>10} us", r.max_us); + println!(" --- per-cycle breakdown (avg) ---"); + println!(" apply_changes: {:>7} us", r.rebuild_us); + println!(" (invalidate legacy): {:>7} us", r.invalidate_us); + println!(" flush (redraw): {:>7} us", r.flush_us); + + drop(renderer); + println!("\nDone."); + return Ok(()); + } + // --- Legacy Pan --- println!("=== Pan benchmark ({} frames, continuous) ===", args.frames); let pan = run_pan_pass(&mut renderer, args.frames); diff --git a/crates/grida-dev/src/main.rs b/crates/grida-dev/src/main.rs index 4d0cf5c29c..6ee9dee2b5 100644 --- a/crates/grida-dev/src/main.rs +++ b/crates/grida-dev/src/main.rs @@ -46,6 +46,9 @@ enum Command { /// Bulk benchmark — runs all scenes in all `.grida` files, outputs a compact JSON report. /// Accepts a single `.grida` file or a directory (recursively finds `*.grida` files). BenchReport(bench::BenchReportArgs), + /// Measure `load_scene()` per-stage timings (layout, geometry, effects, layers). + /// Identifies cold-start bottlenecks without GPU rendering. + LoadBench(bench::LoadBenchArgs), } #[tokio::main] @@ -57,6 +60,7 @@ async fn main() -> Result<()> { Some(Command::Reftest(args)) => reftest::run(args).await?, Some(Command::SvgToGrida(args)) => run_svg_to_grida(args), Some(Command::BenchReport(args)) => bench::run_bench_report(args, loader).await?, + Some(Command::LoadBench(args)) => bench::run_load_bench(args, loader).await?, None => run_interactive(cli.file).await?, } Ok(()) @@ -96,11 +100,7 @@ struct SvgToGridaArgs { fn run_svg_to_grida(args: SvgToGridaArgs) { use cg::io::io_svg::svg_to_grida_bytes; - let input_dir = PathBuf::from( - args.path - .as_deref() - .unwrap_or("fixtures/test-svg/L0"), - ); + let input_dir = PathBuf::from(args.path.as_deref().unwrap_or("fixtures/test-svg/L0")); let output_dir = PathBuf::from( args.output .as_deref() @@ -168,7 +168,9 @@ fn run_svg_to_grida(args: SvgToGridaArgs) { } println!( "\n{} converted, {} skipped → {}", - ok, skip, output_dir.display() + ok, + skip, + output_dir.display() ); } @@ -234,10 +236,7 @@ async fn run_interactive(file: Option) -> Result<()> { vec![build_empty_scene()] }; - let first = initial_scenes - .first() - .cloned() - .expect("at least one scene"); + let first = initial_scenes.first().cloned().expect("at least one scene"); let (drop_tx, drop_rx) = unbounded_channel::(); let (scenes_tx, scenes_rx) = unbounded_channel::>(); @@ -264,8 +263,8 @@ async fn run_interactive(file: Option) -> Result<()> { fn scene_from_svg_path(path: &Path) -> Result { use cg::cg::prelude::CGColor; - let svg_source = - std::fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?; + let svg_source = std::fs::read_to_string(path) + .with_context(|| format!("failed to read {}", path.display()))?; let graph = pack::from_svg_str(&svg_source) .map_err(|err| anyhow::anyhow!("failed to convert SVG {}: {err}", path.display()))?; diff --git a/crates/grida-dev/src/platform/native_demo.rs b/crates/grida-dev/src/platform/native_demo.rs index 1a367f745c..a2584abd35 100644 --- a/crates/grida-dev/src/platform/native_demo.rs +++ b/crates/grida-dev/src/platform/native_demo.rs @@ -48,8 +48,14 @@ pub async fn run_demo_window_with_drop( winit::event_loop::EventLoopProxy, ), { - run_demo_window_core_multi(scene.clone(), vec![scene], init, Some(drop_tx), Some(scenes_rx)) - .await; + run_demo_window_core_multi( + scene.clone(), + vec![scene], + init, + Some(drop_tx), + Some(scenes_rx), + ) + .await; } async fn run_demo_window_core_multi( @@ -80,6 +86,7 @@ async fn run_demo_window_core_multi( font_rx, cg::runtime::scene::RendererOptions { use_embedded_fonts: true, + ..Default::default() }, file_drop_tx.clone(), file_drop_tx.is_some(), diff --git a/crates/grida-dev/src/reftest/render.rs b/crates/grida-dev/src/reftest/render.rs index 2751e7c9e1..e677084cff 100644 --- a/crates/grida-dev/src/reftest/render.rs +++ b/crates/grida-dev/src/reftest/render.rs @@ -231,6 +231,7 @@ pub fn render_svg_to_png( store.clone(), RendererOptions { use_embedded_fonts: true, + ..Default::default() }, ); diff --git a/docs/wg/feat-2d/optimization.md b/docs/wg/feat-2d/optimization.md index 97af0a61e6..4e4a573a93 100644 --- a/docs/wg/feat-2d/optimization.md +++ b/docs/wg/feat-2d/optimization.md @@ -1,12 +1,12 @@ --- title: Rendering Optimization Strategies +format: md tags: - internal - wg - canvas - performance - rendering - --- # Rendering Optimization Strategies @@ -857,17 +857,78 @@ missing the cheapest possible camera-change path. --- +## Scene Loading & Layout + +Scene loading (`Renderer::load_scene`) is the cold-start path that runs +before the first frame. For large documents (100K–150K+ nodes), the +layout phase dominates (~95%+ of `load_scene` time). The remaining +stages (geometry, effects, layers) are comparatively cheap. + +**Pipeline:** `load_scene` runs five stages in order: font collection, +layout (Taffy tree build + flexbox + text measurement), geometry +propagation, effect classification, and layer flattening. + +WASM runs ~5x slower than native for this workload due to allocator +and single-thread overhead. + +34. **Skip Layout for Absolute-Position Documents** ✅ IMPLEMENTED + + `skip_layout` bypasses the Taffy flexbox engine entirely. + `compute_schema_only()` walks the scene graph once and copies + schema positions/sizes — O(n) with no allocations, no text + measurement, no tree construction. + + Correct for absolute-positioned documents. Documents with + auto-layout/flex containers require the full Taffy path. + + ~5x layout speedup on small scenes; orders of magnitude on + 100K+ node scenes where Taffy + text measurement dominate. + + CLI: `cargo run -p grida-dev --release -- load-bench file.grida --skip-layout` + +35. **Pre-Allocate Layout Data Structures** ✅ IMPLEMENTED + + `LayoutTree::reserve(node_count)` pre-allocates the TaffyTree slab + and ID-mapping HashMaps before tree construction. Avoids ~17 + doubling reallocations for 100K+ node scenes. More impactful in + WASM where per-reallocation cost is higher. + +36. **Deferred / Viewport-Only Layout** (future) + + Compute layout only for viewport-visible nodes on cold start. + Remaining nodes computed lazily as the user pans. Requires + bounding-box estimates from schema data. + +37. **Pre-Measure Text Before Taffy** (future) + + Decouple text measurement from the Taffy measure callback. + Pre-measure all text nodes in a single pass, then run Taffy + with a lookup-table measure function. Eliminates repeated + Skia calls and enables future parallelization on native. + +38. **Cache Text Measurements by Width Constraint** (future) + + Add a secondary cache keyed on `(node_id, width_constraint)` + that returns measurements directly, skipping Skia entirely for + repeated queries with the same width. + +**Diagnostic tooling:** `load-bench` CLI (`grida-dev load-bench`) +for per-stage timing; `cargo bench -p cg --bench bench_load_scene` +for Criterion benchmarks at synthetic scale. + +--- + ## Engine-Level -34. **Precomputed World Transforms** +39. **Precomputed World Transforms** - Avoid recalculating transforms per draw call. - Essential for random-access rendering. -35. **Flat Table Architecture** +40. **Flat Table Architecture** - All node data (transforms, bounds, styles) stored in flat maps. - Enables fast diffing, syncing, and concurrent access. -36. **Scene Planner & Scheduler** +41. **Scene Planner & Scheduler** - Builds the render pass list per frame. - Reacts to scene changes, memory pressure, frame budget. - Drives decisions to re-record, cache, evict, or degrade fidelity. @@ -876,7 +937,7 @@ missing the cheapest possible camera-change path. ## Future: Worker-Thread Rasterization -37. **Multithreaded Rasterization** +42. **Multithreaded Rasterization** Move SkPicture recording and/or render surface rasterization to worker threads. This is the single largest performance gap vs. Chromium: @@ -898,11 +959,11 @@ missing the cheapest possible camera-change path. specifically to compensate for this. Multithreaded rasterization applies only to native (desktop) builds. -38. **BVH or Quadtree Spatial Index** +43. **BVH or Quadtree Spatial Index** - Build dynamic index from `world_bounds` for fast spatial queries. - Currently using R-tree (rstar crate). -39. **CRDT-Ready Data Stores** +44. **CRDT-Ready Data Stores** - Flat table model enables future collaboration support. --- diff --git a/editor/app/(embed)/embed/v1/refig/page.tsx b/editor/app/(embed)/embed/v1/refig/page.tsx index 2bbef149b2..c3c727eadb 100644 --- a/editor/app/(embed)/embed/v1/refig/page.tsx +++ b/editor/app/(embed)/embed/v1/refig/page.tsx @@ -9,7 +9,10 @@ import { } from "@/grida-canvas-react"; import { TooltipProvider } from "@/components/ui/tooltip"; import { FontFamilyListProvider } from "@/scaffolds/sidecontrol/controls/font-family"; -import { useRefigEditor } from "@/scaffolds/embed/use-refig-editor"; +import { + useRefigEditor, + decodeSyntheticFigmaId, +} from "@/scaffolds/embed/use-refig-editor"; import { RefigCanvas } from "@/scaffolds/embed/refig-shared"; import { useEmbedBridge } from "@/grida-canvas-react/use-embed-bridge"; @@ -129,7 +132,11 @@ function RefigEmbedInner({ remoteFileUrl }: { remoteFileUrl?: string }) { onFile, } = useRefigEditor(); - useEmbedBridge(instance, { canvasReady, onFile }); + useEmbedBridge(instance, { + canvasReady, + onFile, + __dangerously_transform_node_id: decodeSyntheticFigmaId, + }); const remoteFetchGen = useRef(0); diff --git a/editor/grida-canvas-hosted/playground/uxhost-menu.tsx b/editor/grida-canvas-hosted/playground/uxhost-menu.tsx index be3bc92ae8..a1b2f214a1 100644 --- a/editor/grida-canvas-hosted/playground/uxhost-menu.tsx +++ b/editor/grida-canvas-hosted/playground/uxhost-menu.tsx @@ -525,7 +525,7 @@ function ViewMenuContent({ instance.camera.fit("*")} + onSelect={() => instance.camera.fit("")} className="text-xs" > Zoom to fit diff --git a/editor/grida-canvas-react/renderer.tsx b/editor/grida-canvas-react/renderer.tsx index 4f1176c5d5..6eaa3ce19b 100644 --- a/editor/grida-canvas-react/renderer.tsx +++ b/editor/grida-canvas-react/renderer.tsx @@ -164,7 +164,7 @@ function useFitInitiallyEffect() { const sceneId = useEditorState(editor, (state) => state.scene_id); useEffect(() => { - editor.camera.fit("*"); + editor.camera.fit(""); }, [documentKey, sceneId]); } diff --git a/editor/grida-canvas-react/use-embed-bridge.ts b/editor/grida-canvas-react/use-embed-bridge.ts index e3be85fcde..fc7feb7542 100644 --- a/editor/grida-canvas-react/use-embed-bridge.ts +++ b/editor/grida-canvas-react/use-embed-bridge.ts @@ -12,14 +12,22 @@ export function useEmbedBridge( { canvasReady, onFile, + __dangerously_transform_node_id, }: { canvasReady: boolean; onFile?: (file: File) => void; + /** + * Optional transform applied to every node ID before it is emitted to the + * host via postMessage. See {@link EmbedBridge} for details. + */ + __dangerously_transform_node_id?: (id: string) => string; } ): void { - // Store onFile in a ref to avoid recreating the bridge when it changes. + // Store callbacks in refs to avoid recreating the bridge when they change. const onFileRef = useRef(onFile); onFileRef.current = onFile; + const transformRef = useRef(__dangerously_transform_node_id); + transformRef.current = __dangerously_transform_node_id; useEffect(() => { // Don't create bridge until canvas is ready — notifyReady needs to be @@ -28,6 +36,8 @@ export function useEmbedBridge( const bridge = new EmbedBridge(ed, { onFile: (file) => onFileRef.current?.(file), + __dangerously_transform_node_id: (id) => + transformRef.current ? transformRef.current(id) : id, }); bridge.notifyReady(); diff --git a/editor/grida-canvas-react/viewport/hotkeys.tsx b/editor/grida-canvas-react/viewport/hotkeys.tsx index 7a9b9f6750..c5a35b3496 100644 --- a/editor/grida-canvas-react/viewport/hotkeys.tsx +++ b/editor/grida-canvas-react/viewport/hotkeys.tsx @@ -894,7 +894,7 @@ export function useEditorHotKeys() { }); useHotkeys("shift+1, shift+9", (e) => { - editor.camera.fit("*", { margin: 64 }); + editor.camera.fit("", { margin: 64 }); toast.success(`Zoom to fit`); }); diff --git a/editor/grida-canvas/backends/dom-content.ts b/editor/grida-canvas/backends/dom-content.ts index 47c6abbb53..42b108cefc 100644 --- a/editor/grida-canvas/backends/dom-content.ts +++ b/editor/grida-canvas/backends/dom-content.ts @@ -78,7 +78,12 @@ export class DOMGeometryQueryInterfaceProvider return contained; } - getNodeAbsoluteBoundingRect(node_id: string): cmath.Rectangle | null { + getNodeAbsoluteBoundingRect( + target: (string & {}) | "" + ): cmath.Rectangle | null { + // DOM backend does not support "" — fall back to null + if (target === "") return null; + const node_id = target; const contentrect = this.content.getBoundingClientRect()!; const noderect = window.document .getElementById(node_id) diff --git a/editor/grida-canvas/backends/wasm.ts b/editor/grida-canvas/backends/wasm.ts index 612d16413b..5c1b380cbd 100644 --- a/editor/grida-canvas/backends/wasm.ts +++ b/editor/grida-canvas/backends/wasm.ts @@ -39,8 +39,10 @@ export class CanvasWasmGeometryQueryInterfaceProvider return this.getNodeIdsFromPoint(p); } - getNodeAbsoluteBoundingRect(node_id: string): cmath.Rectangle | null { - return this.surface.getNodeAbsoluteBoundingBox(node_id); + getNodeAbsoluteBoundingRect( + target: (string & {}) | "" + ): cmath.Rectangle | null { + return this.surface.getNodeAbsoluteBoundingBox(target); } } diff --git a/editor/grida-canvas/editor.i.ts b/editor/grida-canvas/editor.i.ts index f44b7bca68..77280d5286 100644 --- a/editor/grida-canvas/editor.i.ts +++ b/editor/grida-canvas/editor.i.ts @@ -2582,11 +2582,14 @@ export namespace editor.api { getNodeIdsFromEnvelope(envelope: cmath.Rectangle): string[]; /** - * returns a bounding rect of the node in canvas space - * @param node_id - * @returns + * Returns the absolute bounding rect of a node or the active scene. + * + * @param target - A node ID, or `""` to get the union bounds of the + * active scene's root children. */ - getNodeAbsoluteBoundingRect(node_id: string): cmath.Rectangle | null; + getNodeAbsoluteBoundingRect( + target: (string & {}) | "" + ): cmath.Rectangle | null; } export interface IDocumentImageExportInterfaceProvider { diff --git a/editor/grida-canvas/editor.ts b/editor/grida-canvas/editor.ts index 9bb4eb2409..1627e2312b 100644 --- a/editor/grida-canvas/editor.ts +++ b/editor/grida-canvas/editor.ts @@ -39,6 +39,8 @@ import assert from "assert"; import { describeDocumentTree } from "./utils/cmd-tree"; import { computeRenderPolicyFlagsForOutlineFeature } from "./render-policy-flags"; +const __DEV__ = process.env.NODE_ENV === "development"; + function resolveNumberChangeValue( node: grida.program.nodes.UnknownNode, key: keyof grida.program.nodes.UnknownNode, @@ -192,7 +194,7 @@ export class Camera implements editor.api.ICameraActions { * Transform to fit */ fit( - selector: grida.program.document.Selector, + selector: grida.program.document.Selector | "", options: { margin?: number | [number, number, number, number]; animate?: boolean; @@ -202,17 +204,32 @@ export class Camera implements editor.api.ICameraActions { } ) { const { document_ctx, selection, transform } = this.editor.state; - const ids = dq.querySelector(document_ctx, selection, selector); - const rects = ids - .map((id) => this.editor.geometryProvider.getNodeAbsoluteBoundingRect(id)) - .filter((r) => r) as cmath.Rectangle[]; + let area: cmath.Rectangle | undefined; - if (rects.length === 0) { - return; + if (selector === "") { + // Single WASM call — Rust computes union of scene root children + const rect = + this.editor.geometryProvider.getNodeAbsoluteBoundingRect(""); + if (rect) area = rect; + } else { + const ids = dq.querySelector(document_ctx, selection, selector); + // TODO(perf): batch WASM call — each getNodeAbsoluteBoundingRect crosses + // the WASM boundary individually. Add a bulk API that accepts multiple IDs + // and returns rects + union in a single call. + const rects = ids + .map((id) => + this.editor.geometryProvider.getNodeAbsoluteBoundingRect(id) + ) + .filter((r) => r) as cmath.Rectangle[]; + if (rects.length > 0) { + area = cmath.rect.union(rects); + } } - const area = cmath.rect.union(rects); + if (!area) { + return; + } const { width, height } = this.viewport.size; const view = { x: 0, y: 0, width, height }; @@ -2706,6 +2723,16 @@ export class Editor readonly doc: EditorDocumentStore; private _m_wasm_canvas_scene: Scene | null = null; + private _m_wasm_canvas_resize_observer: ResizeObserver | null = null; + + /** + * Surface init options passed to `createWebGLCanvasSurface()`. + * Set before calling `mount()`. Includes renderer config (e.g. `skip_layout`). + */ + __surfaceOptions: { + use_embedded_fonts?: boolean; + config?: { skip_layout?: boolean }; + } = { use_embedded_fonts: true }; /** * Access the underlying WASM Scene instance for the canvas backend. @@ -3086,10 +3113,10 @@ export class Editor await init({ locateFile: locateFile, }).then((factory) => { - const surface = factory.createWebGLCanvasSurface(el); - surface.runtime_renderer_set_layer_compositing(true); - // surface.setDebug(this.debug); - // surface.setVerbose(this.debug); + const surface = factory.createWebGLCanvasSurface( + el, + this.__surfaceOptions + ); this.__bind_wasm_surface(surface); this.onMount?.(surface); @@ -3147,7 +3174,9 @@ export class Editor } }); - // TODO: cleanup not handled + // Store for cleanup on dispose. + this._m_wasm_canvas_resize_observer = ro; + // Safari doesn't support the "device-pixel-content-box" box option try { ro.observe(el, { box: "device-pixel-content-box" }); @@ -3166,11 +3195,26 @@ export class Editor }); } + // Guards against redundant switchScene calls from the scene_id + // subscriber when syncDocument already activated the same scene. + // + // The problem: syncDocument calls switchScene(X), then camera.fit + // dispatches a transform change. The nested emit re-fires ALL + // subscribers, including scene_id, which sees the new scene_id and + // calls switchScene(X) again — duplicating the expensive layout pass. + // + // The fix: syncDocument increments this counter before switchScene. + // The scene_id subscriber decrements and skips when > 0. The counter + // (not a boolean) is safe against re-entrant calls. + let __switchSceneGuard = 0; + const syncDocument = ( surface: Scene, document: grida.program.document.Document, sceneId?: string ) => { + const t0 = __DEV__ ? performance.now() : 0; + try { const bytes = io.GRID.encode(document); surface.loadSceneGrida(bytes); @@ -3182,24 +3226,27 @@ export class Editor }); surface.loadScene(p); } + // loadSceneGrida only decodes and stores scenes. // switchScene activates the requested scene (or first if unspecified). const targetScene = sceneId ?? document.scenes_ref?.[0] ?? document.entry_scene_id; if (targetScene) { + __switchSceneGuard++; surface.switchScene(targetScene); } + surface.redraw(); - }; - // setup hooks - // - state.document - // - state.scene_id - // - state.debug - // - state.transform - // - [state.hovered_node_id, state.selection] + if (__DEV__) { + console.log( + `[syncDocument] ${Object.keys(document.nodes).length} nodes, ` + + `scene=${targetScene ?? "(none)"} in ${(performance.now() - t0).toFixed(0)}ms` + ); + } + }; - // once + // --- initial mount sync --- syncDocument( this._m_wasm_canvas_scene!, this.doc.state.document, @@ -3211,18 +3258,13 @@ export class Editor el.width, el.height ); + this.camera.fit(""); - // fit the camera - this.camera.fit("*"); + // --- state subscribers --- - // subscribe this.doc.subscribeWithSelector( (state) => state.document, (_, document, _prev, action, patches) => { - // FIXME: Unstable - // the current patch based sync is not stable, it WILL fail to direct sync when deleting a node, etc. - // this is not fully tested, and the direct sync fallback should kept as-is until we fully investicate this. - if (!this._m_wasm_canvas_scene) return; // Full sync on document reset @@ -3232,27 +3274,23 @@ export class Editor document, this.doc.state.scene_id ); - // Perform initial actions after reset - this.camera.fit("*"); + this.camera.fit(""); return; } // Patch-based sync for normal changes + // FIXME: Unstable — patch sync will fail on some operations (e.g. node deletion). + // Direct sync fallback should be kept until this is fully investigated. if (!patches || patches.length === 0) return; const documentPatches = patches.filter( (patch) => patch.path[0] === "document" ); - - if (documentPatches.length === 0) { - return; - } + if (documentPatches.length === 0) return; const operations = editor.api.patch.toJsonPatchOperations(documentPatches); - if (operations.length === 0) { - return; - } + if (operations.length === 0) return; const result = this._m_wasm_canvas_scene.applyTransactions([ operations, @@ -3275,6 +3313,12 @@ export class Editor (state) => state.scene_id, (_, scene_id) => { if (!this._m_wasm_canvas_scene || !scene_id) return; + // syncDocument already called switchScene — skip the redundant call + // which would repeat the expensive layout+geometry pass. + if (__switchSceneGuard > 0) { + __switchSceneGuard--; + return; + } this._m_wasm_canvas_scene.switchScene(scene_id); } ); @@ -3298,7 +3342,6 @@ export class Editor (state) => [state.outline_mode, state.outline_mode_ignores_clips] as const, (_, [outline_mode, outline_mode_ignores_clips]) => { - // Always compute flags from the *new* state values (avoid stale `this.state`). this.__runtime_renderer_set_outline_mode( outline_mode, outline_mode_ignores_clips @@ -4214,6 +4257,10 @@ export class Editor */ dispose() { this._stopImagePoll(); + if (this._m_wasm_canvas_resize_observer) { + this._m_wasm_canvas_resize_observer.disconnect(); + this._m_wasm_canvas_resize_observer = null; + } this.doc.dispose(); } } diff --git a/editor/grida-canvas/embed-bridge.ts b/editor/grida-canvas/embed-bridge.ts index 615e3c4802..b4b914b6ba 100644 --- a/editor/grida-canvas/embed-bridge.ts +++ b/editor/grida-canvas/embed-bridge.ts @@ -20,6 +20,16 @@ function scenesFromEditor(ed: Editor): EmbedSceneInfo[] { export interface EmbedBridgeOptions { /** Handler for `grida:load` command. */ onFile?: (file: File) => void; + /** + * Optional transform applied to every node ID before it is emitted to the + * host via postMessage. Use this to decode synthetic/internal IDs back to + * the original source IDs (e.g. Figma node IDs). + * + * The transform receives a single Grida node ID and must return the ID + * string that should appear in the outgoing event. Return the input + * unchanged for IDs that need no mapping. + */ + __dangerously_transform_node_id?: (id: string) => string; } /** @@ -39,6 +49,7 @@ export interface EmbedBridgeOptions { export class EmbedBridge { private ed: Editor; private onFile?: (file: File) => void; + private transformId: ((id: string) => string) | undefined; private messageHandler: (e: MessageEvent) => void; private unsubscribe: (() => void) | null = null; private readySent = false; @@ -54,6 +65,7 @@ export class EmbedBridge { constructor(ed: Editor, options: EmbedBridgeOptions = {}) { this.ed = ed; this.onFile = options.onFile; + this.transformId = options.__dangerously_transform_node_id; this.prevSelection = ed.state.selection; this.prevSceneId = ed.state.scene_id; @@ -96,6 +108,27 @@ export class EmbedBridge { } } + // --------------------------------------------------------------------------- + // ID mapping helpers + // --------------------------------------------------------------------------- + + /** Map a single node ID through the optional transform. */ + private mapId(id: string): string { + return this.transformId ? this.transformId(id) : id; + } + + /** Map an array of node IDs through the optional transform (deduplicated). */ + private mapIds(ids: string[]): string[] { + if (!this.transformId) return ids; + return [...new Set(ids.map(this.transformId))]; + } + + /** Map scene info IDs through the optional transform. */ + private mapScenes(scenes: EmbedSceneInfo[]): EmbedSceneInfo[] { + if (!this.transformId) return scenes; + return scenes.map((s) => ({ ...s, id: this.transformId!(s.id) })); + } + // --------------------------------------------------------------------------- // Action handler — drives all outgoing events // --------------------------------------------------------------------------- @@ -116,7 +149,7 @@ export class EmbedBridge { this.loading = false; this.post({ type: "grida:document-load", - scenes: scenesFromEditor(this.ed), + scenes: this.mapScenes(scenesFromEditor(this.ed)), }); }); return; @@ -130,7 +163,7 @@ export class EmbedBridge { this.prevSelection = state.selection; this.post({ type: "grida:selection-change", - selection: state.selection, + selection: this.mapIds(state.selection), }); } @@ -140,7 +173,7 @@ export class EmbedBridge { if (state.scene_id) { this.post({ type: "grida:scene-change", - sceneId: state.scene_id, + sceneId: this.mapId(state.scene_id), }); } } @@ -179,9 +212,11 @@ export class EmbedBridge { this.post({ type: "grida:pong", ready: this.readySent, - scenes: scenesFromEditor(this.ed), - sceneId: this.ed.state.scene_id, - selection: this.ed.state.selection, + scenes: this.mapScenes(scenesFromEditor(this.ed)), + sceneId: this.ed.state.scene_id + ? this.mapId(this.ed.state.scene_id) + : undefined, + selection: this.mapIds(this.ed.state.selection), }); break; case "grida:images-resolve": { diff --git a/editor/scaffolds/embed/__tests__/decode-synthetic-figma-id.test.ts b/editor/scaffolds/embed/__tests__/decode-synthetic-figma-id.test.ts new file mode 100644 index 0000000000..2e26dd16d6 --- /dev/null +++ b/editor/scaffolds/embed/__tests__/decode-synthetic-figma-id.test.ts @@ -0,0 +1,54 @@ +import { decodeSyntheticFigmaId } from "../use-refig-editor"; + +describe("decodeSyntheticFigmaId", () => { + test("preserves plain Figma IDs unchanged", () => { + expect(decodeSyntheticFigmaId("1:2")).toBe("1:2"); + expect(decodeSyntheticFigmaId("42:17")).toBe("42:17"); + expect(decodeSyntheticFigmaId("0:0")).toBe("0:0"); + expect(decodeSyntheticFigmaId("1038:24")).toBe("1038:24"); + }); + + test("preserves Figma instance IDs unchanged", () => { + // Instance IDs use I-prefix and semicolons + expect(decodeSyntheticFigmaId("I1620:1441;291:113")).toBe( + "I1620:1441;291:113" + ); + expect(decodeSyntheticFigmaId("I100:200;300:400")).toBe("I100:200;300:400"); + }); + + test("strips _fill_{N} suffix → parent Figma ID", () => { + expect(decodeSyntheticFigmaId("42:17_fill_0")).toBe("42:17"); + expect(decodeSyntheticFigmaId("42:17_fill_1")).toBe("42:17"); + expect(decodeSyntheticFigmaId("1:2_fill_99")).toBe("1:2"); + }); + + test("strips _stroke_{N} suffix → parent Figma ID", () => { + expect(decodeSyntheticFigmaId("42:17_stroke_0")).toBe("42:17"); + expect(decodeSyntheticFigmaId("42:17_stroke_3")).toBe("42:17"); + }); + + test("strips synthetic suffix from instance IDs", () => { + expect(decodeSyntheticFigmaId("I1620:1441;291:113_fill_0")).toBe( + "I1620:1441;291:113" + ); + expect(decodeSyntheticFigmaId("I100:200;300:400_stroke_1")).toBe( + "I100:200;300:400" + ); + }); + + test("decodes instance-clone IDs → original Figma ID", () => { + // Format: {prefix}::{counter}::{originalId} + expect(decodeSyntheticFigmaId("42:17::0::5:3")).toBe("5:3"); + expect(decodeSyntheticFigmaId("42:17::1::10:20")).toBe("10:20"); + }); + + test("decodes instance-clone + synthetic suffix → original Figma ID", () => { + expect(decodeSyntheticFigmaId("42:17::0::5:3_fill_0")).toBe("5:3"); + expect(decodeSyntheticFigmaId("42:17::1::10:20_stroke_1")).toBe("10:20"); + }); + + test("preserves non-Figma IDs unchanged (scene IDs, etc.)", () => { + expect(decodeSyntheticFigmaId("scene-1")).toBe("scene-1"); + expect(decodeSyntheticFigmaId("scene-0")).toBe("scene-0"); + }); +}); diff --git a/editor/scaffolds/embed/use-refig-editor.ts b/editor/scaffolds/embed/use-refig-editor.ts index 62f0bb5451..b889558dce 100644 --- a/editor/scaffolds/embed/use-refig-editor.ts +++ b/editor/scaffolds/embed/use-refig-editor.ts @@ -40,6 +40,37 @@ async function decompressGzip(buf: ArrayBuffer): Promise { return out.buffer; } +/** + * Renderer configuration for the refig embed canvas. + * + * These flags are applied to the WASM renderer after mount, before any + * document is loaded. Values are intentionally narrow literals today — + * widen to `boolean` once the feature graduates from "always-on". + */ +interface RefigRenderConfig { + /** + * Skip the Taffy flexbox layout engine during scene loading. + * + * Figma imports use absolute positioning — running layout on 100k+ + * nodes is the dominant cold-start cost (~25 s in WASM). Setting + * this to `true` derives layout from schema positions instead. + */ + cg_skip_layout: true; + /** + * Bake Figma's absoluteBoundingBox dimensions into TEXT nodes instead + * of relying on layout-time text measurement. + * + * Paired with `cg_skip_layout` — without this, text nodes get 0×0 + * sizes because the layout engine (which would measure them) is skipped. + */ + prefer_fixed_text_sizing: true; +} + +const REFIG_RENDER_CONFIG: RefigRenderConfig = { + cg_skip_layout: true, + prefer_fixed_text_sizing: true, +}; + export function useRefigEditor() { const instance = useEditor( { @@ -73,6 +104,14 @@ export function useRefigEditor() { setCanvasReady(false); const dpr = window.devicePixelRatio || 1; + // Apply renderer config at init time (before mount creates the WASM surface). + instance.__surfaceOptions = { + use_embedded_fonts: true, + config: { + skip_layout: REFIG_RENDER_CONFIG.cg_skip_layout, + }, + }; + instance .mount(canvasElement, dpr) .then(() => { @@ -105,7 +144,7 @@ export function useRefigEditor() { useEffect(() => { if (!documentLoaded || !fileLabel || !canvasReady) return; queueMicrotask(() => { - instance.camera.fit("*"); + instance.camera.fit(""); }); }, [documentLoaded, fileLabel, canvasReady, sceneId, documentKey, instance]); @@ -158,6 +197,9 @@ export function useRefigEditor() { pageNames, } = fig2grida(input, { placeholder_for_missing_images: false, + preserve_figma_ids: true, + prefer_fixed_text_sizing: + REFIG_RENDER_CONFIG.prefer_fixed_text_sizing, }); console.log( `[@grida/refig] fig2grida: ${pageNames.length} page(s), ${nodeCount} nodes in ${(performance.now() - t0).toFixed(0)}ms` @@ -178,12 +220,19 @@ export function useRefigEditor() { logMemory("before reset"); - instance.commands.reset( - editor.state.init({ - editable: false, - document: loaded.document, - }), - file.name + const t2 = performance.now(); + const initState = editor.state.init({ + editable: false, + document: loaded.document, + }); + console.log( + `[@grida/refig] editor.state.init: ${(performance.now() - t2).toFixed(0)}ms` + ); + + const t3 = performance.now(); + instance.commands.reset(initState, file.name); + console.log( + `[@grida/refig] reset (includes syncDocument): ${(performance.now() - t3).toFixed(0)}ms` ); logMemory("after reset"); @@ -222,4 +271,51 @@ export function useRefigEditor() { }; } +/** + * Regex that matches the synthetic suffixes appended by io-figma when + * `prefer_path_for_geometry` is true and fill/stroke geometries are + * decomposed into child nodes. + * + * Patterns: + * - `{figmaId}_fill_{N}` — fill geometry child + * - `{figmaId}_stroke_{N}` — stroke geometry child + */ +const SYNTHETIC_SUFFIX_RE = /_(fill|stroke)_\d+$/; + +/** + * Regex for instance-clone IDs: `{prefix}::{counter}::{originalId}`. + * The `::` separator is unique to clone IDs (never appears in Figma node IDs). + * Captures the trailing original Figma ID after the last `::`. + */ +const INSTANCE_CLONE_RE = /^.+?::\d+::(.+)$/; + +/** + * Decode a Grida node ID that may contain synthetic suffixes back to the + * closest real Figma node ID. This is refig-specific logic and should only + * be used in the embed/refig context where `preserve_figma_ids` is true. + * + * - `"42:17"` → `"42:17"` (real node, unchanged) + * - `"42:17_fill_0"` → `"42:17"` (synthetic fill child → parent) + * - `"42:17_stroke_1"` → `"42:17"` (synthetic stroke child → parent) + * - `"42:17::0::5:3"` → `"5:3"` (instance clone → original) + * - `"42:17::0::5:3_fill_0"` → `"5:3"` (instance clone + synthetic → original) + * - `"scene-1"` → `"scene-1"` (non-Figma ID, unchanged) + */ +export function decodeSyntheticFigmaId(id: string): string { + let decoded = id; + + // Strip instance-clone prefix: `{prefix}::{counter}::{originalId}` → `{originalId}` + // The `::` delimiter never appears in Figma IDs, so its presence is + // unambiguous. We take everything after the last `::`. + const cloneMatch = decoded.match(INSTANCE_CLONE_RE); + if (cloneMatch) { + decoded = cloneMatch[1]; + } + + // Strip synthetic geometry suffix + decoded = decoded.replace(SYNTHETIC_SUFFIX_RE, ""); + + return decoded; +} + export { validateExt }; diff --git a/editor/scaffolds/sidecontrol/controls/ext-zoom.tsx b/editor/scaffolds/sidecontrol/controls/ext-zoom.tsx index 33c034a444..221906b4b0 100644 --- a/editor/scaffolds/sidecontrol/controls/ext-zoom.tsx +++ b/editor/scaffolds/sidecontrol/controls/ext-zoom.tsx @@ -80,7 +80,7 @@ export function ZoomControl({ className }: { className?: string }) { editor.camera.fit("*")} + onSelect={() => editor.camera.fit("")} className="text-xs" > Zoom to fit diff --git a/packages/grida-canvas-io-figma/__bench__/.gitignore b/packages/grida-canvas-io-figma/__bench__/.gitignore new file mode 100644 index 0000000000..d062294cbd --- /dev/null +++ b/packages/grida-canvas-io-figma/__bench__/.gitignore @@ -0,0 +1,6 @@ +# Bench fixtures are large and not tracked in git. +# Only source files and docs are tracked. +* +!.gitignore +!README.md +!*.bench.ts diff --git a/packages/grida-canvas-io-figma/__bench__/README.md b/packages/grida-canvas-io-figma/__bench__/README.md new file mode 100644 index 0000000000..765940d2e9 --- /dev/null +++ b/packages/grida-canvas-io-figma/__bench__/README.md @@ -0,0 +1,12 @@ +# Benchmark Fixtures + +This directory holds large fixture files used by `__bench__/fig2grida.bench.ts`. +Files here are **gitignored** — you must supply them locally. + +Benchmarks that reference missing fixtures are **skipped automatically**. + +## Expected files + +| File | Description | How to obtain | +| -------------------- | --------------------------------------------------------------------------- | -------------------------------------------------------------- | +| `rest-large.json.gz` | Gzipped Figma REST API response (`GET /v1/files/:key`), ideally 100k+ nodes | Export any large Figma file via the REST API and gzip the JSON | diff --git a/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts b/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts new file mode 100644 index 0000000000..5b9a8494a9 --- /dev/null +++ b/packages/grida-canvas-io-figma/__bench__/fig2grida.bench.ts @@ -0,0 +1,186 @@ +/** + * Benchmark for the fig2grida pipeline. + * + * Requires `rest-large.json.gz` in the same directory — a gzipped Figma + * REST API response with 100k+ nodes. See `README.md` for details. + * If the file is missing the entire suite is skipped. + * + * Run: npx vitest bench __bench__/fig2grida.bench.ts + */ +import { existsSync, readFileSync } from "fs"; +import { resolve } from "path"; +import { gunzipSync, zipSync, strToU8 } from "fflate"; +import { bench, describe } from "vitest"; +import { fig2grida, restJsonToGridaDocument } from "../fig2grida-core"; +import { io } from "@grida/io"; +import { format } from "@grida/io/format"; + +// --------------------------------------------------------------------------- +// Fixture contract +// --------------------------------------------------------------------------- + +const FIXTURE_PATH = resolve(__dirname, "rest-large.json.gz"); +const HAS_FIXTURE = existsSync(FIXTURE_PATH); + +if (!HAS_FIXTURE) { + console.warn( + `[bench] skipping — fixture not found: ${FIXTURE_PATH}\n` + + ` See __bench__/README.md for setup instructions.` + ); +} + +// --------------------------------------------------------------------------- +// Fixture loading (outside of benchmarks, only when present) +// --------------------------------------------------------------------------- + +const FIXTURE_GZ = HAS_FIXTURE ? readFileSync(FIXTURE_PATH) : null; + +const decompressedBytes = FIXTURE_GZ + ? gunzipSync(FIXTURE_GZ) + : null; +const jsonString = decompressedBytes + ? new TextDecoder().decode(decompressedBytes) + : null; +const parsedJson = jsonString ? JSON.parse(jsonString) : null; + +const preConverted = parsedJson ? restJsonToGridaDocument(parsedJson) : null; + +if (FIXTURE_GZ && decompressedBytes && jsonString && preConverted) { + const nodeCount = Object.keys(preConverted.document.nodes).length; + console.log( + `[fixture] gz=${(FIXTURE_GZ.byteLength / 1024 / 1024).toFixed(1)}MB, ` + + `decompressed=${(decompressedBytes.byteLength / 1024 / 1024).toFixed(1)}MB, ` + + `jsonString=${(jsonString.length / 1024 / 1024).toFixed(1)}MB, ` + + `nodes=${nodeCount}` + ); +} + +// --------------------------------------------------------------------------- +// Benchmarks — end-to-end pipeline +// --------------------------------------------------------------------------- + +describe.skipIf(!HAS_FIXTURE)("fig2grida pipeline", () => { + bench( + "stage: gzip decompress", + () => { + gunzipSync(FIXTURE_GZ!); + }, + { iterations: 5, warmupIterations: 1 } + ); + + bench( + "stage: JSON.parse", + () => { + JSON.parse(jsonString!); + }, + { iterations: 5, warmupIterations: 1 } + ); + + bench( + "stage: restJsonToGridaDocument (convert + merge)", + () => { + restJsonToGridaDocument(parsedJson); + }, + { iterations: 3, warmupIterations: 1 } + ); + + bench( + "stage: fig2grida full (convert + merge + pack)", + () => { + fig2grida(parsedJson); + }, + { iterations: 3, warmupIterations: 1 } + ); + + bench( + "stage: io.archive.pack (level 6, default)", + () => { + io.archive.pack(preConverted!.document, preConverted!.assets); + }, + { iterations: 3, warmupIterations: 1 } + ); + + bench( + "stage: io.archive.pack (level 0, store)", + () => { + io.archive.pack( + preConverted!.document, + preConverted!.assets, + undefined, + undefined, + { level: 0 } + ); + }, + { iterations: 3, warmupIterations: 1 } + ); +}); + +// --------------------------------------------------------------------------- +// Benchmarks — io.archive.pack sub-stages +// --------------------------------------------------------------------------- + +describe.skipIf(!HAS_FIXTURE)("io.archive.pack sub-stages", () => { + const docForFb = { + ...preConverted!.document, + images: {}, + bitmaps: {}, + }; + + bench( + "sub: toFlatbuffer", + () => { + format.document.encode.toFlatbuffer(docForFb); + }, + { iterations: 3, warmupIterations: 1 } + ); + + const { + images: _images, + bitmaps: _bitmaps, + ...persistedDocument + } = preConverted!.document; + const snapshotPayload = { + version: "1.0", + document: persistedDocument, + }; + + bench( + "sub: JSON.stringify (snapshot)", + () => { + JSON.stringify(snapshotPayload); + }, + { iterations: 5, warmupIterations: 1 } + ); + + const fbBytes = format.document.encode.toFlatbuffer(docForFb); + const snapshotJson = JSON.stringify(snapshotPayload); + + const files: Record = { + "manifest.json": strToU8( + JSON.stringify({ document_file: "document.grida", version: "1.0" }) + ), + "document.grida": fbBytes, + "document.grida1": strToU8(snapshotJson), + }; + + bench( + "sub: zipSync (level 6)", + () => { + zipSync(files); + }, + { iterations: 5, warmupIterations: 1 } + ); + + bench( + "sub: zipSync (level 0)", + () => { + zipSync(files, { level: 0 }); + }, + { iterations: 5, warmupIterations: 1 } + ); + + console.log( + `[pack sizes] flatbuffer=${(fbBytes.byteLength / 1024 / 1024).toFixed(1)}MB, ` + + `snapshot=${(snapshotJson.length / 1024 / 1024).toFixed(1)}MB` + ); +}); diff --git a/packages/grida-canvas-io-figma/__tests__/fig2grida.test.ts b/packages/grida-canvas-io-figma/__tests__/fig2grida.test.ts index 472fca6324..38ca45bd66 100644 --- a/packages/grida-canvas-io-figma/__tests__/fig2grida.test.ts +++ b/packages/grida-canvas-io-figma/__tests__/fig2grida.test.ts @@ -248,6 +248,116 @@ describe("fig2grida", () => { }); }); + describe("preserve_figma_ids", () => { + /** + * Figma IDs have several formats: + * - Simple: `{sessionID}:{localID}` (e.g. "1:2", "42:17") + * - Instance: `I{id};{id}` (e.g. "I1620:1441;291:113") + * + * Synthetic children append: `_fill_{N}` or `_stroke_{N}`. + * Instance clones use: `{prefix}::{counter}::{originalId}`. + * + * This regex matches any ID that is or derives from a Figma-format ID. + */ + const FIGMA_ID_RE = /^(I?\d+:\d+|I\d+:\d+;[\d:;]+)/; + + test("node IDs are Figma-format when preserve_figma_ids is true (REST JSON)", () => { + const { documentJson } = loadFigmaRestArchive( + `${FIGMA_COMMUNITY_REST}/784448220678228461-figma-auto-layout-playground.zip` + ); + const result = fig2grida(documentJson as object, { + preserve_figma_ids: true, + }); + + const unpacked = io.archive.unpack(result.bytes); + const decoded = io.GRID.decode(unpacked.document); + + // Collect all non-scene node IDs + const nodeIds = Object.keys(decoded.nodes).filter( + (id) => decoded.nodes[id].type !== "scene" + ); + + expect(nodeIds.length).toBeGreaterThan(0); + + for (const id of nodeIds) { + expect(id).toMatch(FIGMA_ID_RE); + } + }, 120_000); + + test("node IDs are NOT Figma-format by default (REST JSON)", () => { + const { documentJson } = loadFigmaRestArchive( + `${FIGMA_COMMUNITY_REST}/784448220678228461-figma-auto-layout-playground.zip` + ); + const result = fig2grida(documentJson as object); + + const unpacked = io.archive.unpack(result.bytes); + const decoded = io.GRID.decode(unpacked.document); + + const nodeIds = Object.keys(decoded.nodes).filter( + (id) => decoded.nodes[id].type !== "scene" + ); + + expect(nodeIds.length).toBeGreaterThan(0); + + // Default IDs should use the "rest-import-N" format, not Figma format + for (const id of nodeIds) { + expect(id).toMatch(/^rest-import-/); + } + }, 120_000); + + test("node IDs are Figma-format when preserve_figma_ids is true (.fig)", () => { + const input = new Uint8Array( + readFileSync( + `${FIXTURES_BASE}/community/1510053249065427020-workos-radix-icons.fig` + ) + ); + const result = fig2grida(input, { + preserve_figma_ids: true, + }); + + const unpacked = io.archive.unpack(result.bytes); + const decoded = io.GRID.decode(unpacked.document); + + const nodeIds = Object.keys(decoded.nodes).filter( + (id) => decoded.nodes[id].type !== "scene" + ); + + expect(nodeIds.length).toBeGreaterThan(0); + + for (const id of nodeIds) { + expect(id).toMatch(FIGMA_ID_RE); + } + }); + + test("synthetic fill/stroke children extend parent Figma ID", () => { + const { documentJson } = loadFigmaRestArchive( + `${FIGMA_COMMUNITY_REST}/1510053249065427020-workos-radix-icons.zip` + ); + const result = fig2grida(documentJson as object, { + preserve_figma_ids: true, + }); + + const unpacked = io.archive.unpack(result.bytes); + const decoded = io.GRID.decode(unpacked.document); + + // Find synthetic IDs (fill/stroke children) + const syntheticIds = Object.keys(decoded.nodes).filter( + (id) => id.includes("_fill_") || id.includes("_stroke_") + ); + + // Ensure at least one synthetic child was produced (regression guard) + expect(syntheticIds.length).toBeGreaterThan(0); + + // Synthetic IDs should start with a Figma-format parent ID + for (const id of syntheticIds) { + const parentPart = id.replace(/_(fill|stroke)_\d+$/, ""); + expect(parentPart).toMatch(FIGMA_ID_RE); + // The parent ID should also exist as a node (or be a known ancestor) + // — the parent was converted to a group node + } + }, 120_000); + }); + describe("fig2grida unified input", () => { test("accepts a JSON object directly", () => { const { documentJson } = loadFigmaRestArchive( diff --git a/packages/grida-canvas-io-figma/fig2grida-core.ts b/packages/grida-canvas-io-figma/fig2grida-core.ts index c25a6846b6..cb7137a2bc 100644 --- a/packages/grida-canvas-io-figma/fig2grida-core.ts +++ b/packages/grida-canvas-io-figma/fig2grida-core.ts @@ -34,15 +34,14 @@ import { io } from "@grida/io"; import grida from "@grida/schema"; import kolor from "@grida/color"; -export interface Fig2GridaOptions { +export interface Fig2GridaOptions extends Pick< + iofigma.restful.factory.FactoryContext, + | "placeholder_for_missing_images" + | "preserve_figma_ids" + | "prefer_fixed_text_sizing" +> { /** Convert specific page indices only (only applies to `.fig` input). */ pages?: number[]; - /** - * When true (default), unresolved image refs are replaced with a checker - * pattern placeholder. When false, refs are preserved as `res://images/` - * so the lazy image loading system can request them at render time. - */ - placeholder_for_missing_images?: boolean; } export interface Fig2GridaResult { @@ -225,7 +224,13 @@ function mergePages( } function packMergedDocument(merged: MergedDocument): Fig2GridaResult { - const archiveBytes = io.archive.pack(merged.document, merged.imageRecord); + const archiveBytes = io.archive.pack( + merged.document, + merged.imageRecord, + undefined, + undefined, + { level: 0 } + ); const nodeCount = Object.keys(merged.document.nodes).filter( (id) => merged.document.nodes[id]?.type !== "scene" ).length; @@ -258,9 +263,18 @@ export function fig2grida( const placeholderForMissing = options?.placeholder_for_missing_images !== false; + const preserveFigmaIds = options?.preserve_figma_ids; + const preferFixedTextSizing = options?.prefer_fixed_text_sizing; + // --- Object input: REST JSON directly --- if (!(input instanceof Uint8Array)) { - return fig2gridaFromRestJson(input, undefined, placeholderForMissing); + return fig2gridaFromRestJson( + input, + undefined, + placeholderForMissing, + preserveFigmaIds, + preferFixedTextSizing + ); } // --- Bytes input: detect format --- @@ -271,14 +285,22 @@ export function fig2grida( return fig2gridaFromRestJson( restArchive.json, restArchive.images, - placeholderForMissing + placeholderForMissing, + preserveFigmaIds, + preferFixedTextSizing ); } // Otherwise fall through to .fig parser (handles both ZIP and raw Kiwi) } else if (input.length > 0 && input[0] === 0x7b /* '{' */) { // JSON text — parse and treat as REST API response const json = JSON.parse(new TextDecoder().decode(input)); - return fig2gridaFromRestJson(json, undefined, placeholderForMissing); + return fig2gridaFromRestJson( + json, + undefined, + placeholderForMissing, + preserveFigmaIds, + preferFixedTextSizing + ); } return fig2gridaFromFigBytes(input, options); @@ -315,6 +337,8 @@ function fig2gridaFromFigBytes( gradient_id_generator: makeIdGenerator("grad"), prefer_path_for_geometry: true, placeholder_for_missing_images: placeholderForMissing, + preserve_figma_ids: options?.preserve_figma_ids, + prefer_fixed_text_sizing: options?.prefer_fixed_text_sizing, }); pageResults.push({ name: page.name, result }); } @@ -424,7 +448,9 @@ function extractCanvases(json: unknown): Array<{ function restJsonToMergedDocument( json: unknown, images: Record | undefined, - placeholderForMissing: boolean + placeholderForMissing: boolean, + preserveFigmaIds?: boolean, + preferFixedTextSizing?: boolean ): MergedDocument { const canvases = extractCanvases(json); @@ -434,7 +460,11 @@ function restJsonToMergedDocument( gradient_id_generator: makeIdGenerator("grad"), prefer_path_for_geometry: true, placeholder_for_missing_images: placeholderForMissing, - node_id_generator: makeIdGenerator("rest-import"), + preserve_figma_ids: preserveFigmaIds, + prefer_fixed_text_sizing: preferFixedTextSizing, + node_id_generator: preserveFigmaIds + ? undefined + : makeIdGenerator("rest-import"), ...(images && Object.keys(images).length > 0 && { resolve_image_src: (ref: string) => @@ -458,10 +488,18 @@ function restJsonToMergedDocument( function fig2gridaFromRestJson( json: unknown, images: Record | undefined, - placeholderForMissing: boolean + placeholderForMissing: boolean, + preserveFigmaIds?: boolean, + preferFixedTextSizing?: boolean ): Fig2GridaResult { return packMergedDocument( - restJsonToMergedDocument(json, images, placeholderForMissing) + restJsonToMergedDocument( + json, + images, + placeholderForMissing, + preserveFigmaIds, + preferFixedTextSizing + ) ); } @@ -554,7 +592,10 @@ function emptyPackedScene( // restJsonToGridaDocument — returns in-memory Document (no .grida packing) // --------------------------------------------------------------------------- -export interface RestJsonToGridaOptions { +export interface RestJsonToGridaOptions extends Pick< + iofigma.restful.factory.FactoryContext, + "prefer_fixed_text_sizing" +> { /** * Image hashes from Figma `images` metadata to raw bytes. When provided, * resolves `IMAGE` paint refs to `res://images/`. @@ -583,7 +624,13 @@ export function restJsonToGridaDocument( ): RestJsonToGridaResult { _idCounter = 0; const images = options?.images; - const merged = restJsonToMergedDocument(json, images, true); + const merged = restJsonToMergedDocument( + json, + images, + true, + undefined, + options?.prefer_fixed_text_sizing + ); return { document: merged.document, diff --git a/packages/grida-canvas-io-figma/fig2grida.ts b/packages/grida-canvas-io-figma/fig2grida.ts index 625e87c098..5d08f50197 100644 --- a/packages/grida-canvas-io-figma/fig2grida.ts +++ b/packages/grida-canvas-io-figma/fig2grida.ts @@ -19,6 +19,7 @@ * --out, -o Output path (default: input with .grida extension) * --pages, -p Comma-separated page indices to include (default: all) * --info Print file info (pages, node counts) — .fig only + * --prefer-fixed-text-sizing Use fixed width/height for text nodes * --verbose Print progress details * --help Show help */ @@ -43,6 +44,7 @@ Options: --out, -o Output path (default: input with .grida extension) --pages, -p Comma-separated page indices to include (default: all) --info Print file info (pages, node counts) — .fig only + --prefer-fixed-text-sizing Use fixed width/height for text nodes --verbose Print progress details --help Show help @@ -61,6 +63,7 @@ interface CliArgs { info: boolean; verbose: boolean; help: boolean; + prefer_fixed_text_sizing: boolean; } function parseArgs(argv: string[]): CliArgs { @@ -68,6 +71,7 @@ function parseArgs(argv: string[]): CliArgs { info: false, verbose: false, help: false, + prefer_fixed_text_sizing: false, }; const positional: string[] = []; @@ -93,6 +97,9 @@ function parseArgs(argv: string[]): CliArgs { i++; args.output = argv[i]; break; + case "--prefer-fixed-text-sizing": + args.prefer_fixed_text_sizing = true; + break; case "--pages": case "-p": i++; @@ -232,7 +239,9 @@ function main(): void { } // Build options - const options: Fig2GridaOptions = {}; + const options: Fig2GridaOptions = { + prefer_fixed_text_sizing: args.prefer_fixed_text_sizing || undefined, + }; if (args.pages) { options.pages = args.pages; if (args.verbose) { diff --git a/packages/grida-canvas-io-figma/lib.ts b/packages/grida-canvas-io-figma/lib.ts index 9dbacbaa9b..1fe363226c 100644 --- a/packages/grida-canvas-io-figma/lib.ts +++ b/packages/grida-canvas-io-figma/lib.ts @@ -6,6 +6,29 @@ * * @see https://grida.co/docs/wg/feat-fig/glossary/fig.kiwi — Fig.kiwi format glossary * + * ## TODO — Auto-layout conversion (not implemented) + * + * Currently ALL nodes are emitted with `layout_positioning: "absolute"` and + * `layout_mode: "flow"`. Figma auto-layout properties (`layoutMode`, + * `layoutAlign`, `layoutGrow`, `primaryAxisAlignItems`, + * `counterAxisAlignItems`, `layoutPositioning`, sizing modes, `layoutWrap`) + * are completely dropped during conversion. Positions are always derived + * from `absoluteBoundingBox` / `relativeTransform`. + * + * This means: + * - The output is always safe for `skip_layout` (no flex containers exist) + * - Auto-layout semantics are lost — re-layout in Grida won't match Figma + * - Resizing a container won't reflow children as it would in Figma + * + * To support true auto-layout round-trip: + * - Map `layoutMode` → `layout_mode: "flex"` + * - Map `layoutAlign`, `layoutGrow`, `layoutPositioning` per child + * - Map `primaryAxisAlignItems` / `counterAxisAlignItems` → alignment + * - Map `primaryAxisSizingMode` / `counterAxisSizingMode` → sizing + * - Map `layoutWrap` → `layout_wrap` + * - Only set `layout_positioning: "absolute"` for children with + * `layoutPositioning: "ABSOLUTE"` in Figma + * * ## TODO — Kiwi → REST (not yet fully mapped) * * - **Rich text**: `characterStyleOverrides` and `styleOverrideTable` are always empty. @@ -427,6 +450,29 @@ export namespace iofigma { * @default true */ placeholder_for_missing_images?: boolean; + /** + * When true, TEXT nodes always use concrete width/height from + * absoluteBoundingBox, ignoring Figma's `textAutoResize` ("auto" + * sizing). This produces fixed-size text frames whose dimensions + * match Figma's rendered output exactly. + * + * Use this when the consumer skips layout computation (e.g. + * `skip_layout` mode) and needs pre-resolved text dimensions. + * + * When false (default), TEXT nodes respect `textAutoResize`: + * "WIDTH_AND_HEIGHT" sets both to auto, "HEIGHT" sets height to + * auto. These require layout-time text measurement to resolve. + * + * **Caveat:** The fixed dimensions come from Figma's own renderer + * and font metrics. If the rendering environment uses different + * fonts or a different text shaper, the actual text extent may + * not match the baked-in size — text may overflow or leave extra + * whitespace. Only use this flag when font fidelity cannot be + * guaranteed or when layout computation is explicitly skipped. + * + * @default false + */ + prefer_fixed_text_sizing?: boolean; }; function toGradientPaint(paint: figrest.GradientPaint) { @@ -1570,12 +1616,14 @@ export namespace iofigma { layout_inset_right: constraints.right, layout_inset_bottom: constraints.bottom, layout_target_width: + !context.prefer_fixed_text_sizing && figma_text_resizing_model === "WIDTH_AND_HEIGHT" ? "auto" : fixedwidth, layout_target_height: - figma_text_resizing_model === "WIDTH_AND_HEIGHT" || - figma_text_resizing_model === "HEIGHT" + !context.prefer_fixed_text_sizing && + (figma_text_resizing_model === "WIDTH_AND_HEIGHT" || + figma_text_resizing_model === "HEIGHT") ? "auto" : fixedheight, text_align: node.style.textAlignHorizontal diff --git a/packages/grida-canvas-io-figma/package.json b/packages/grida-canvas-io-figma/package.json index 254253417e..ffc91629d8 100644 --- a/packages/grida-canvas-io-figma/package.json +++ b/packages/grida-canvas-io-figma/package.json @@ -3,9 +3,10 @@ "private": true, "description": "Figma I/O for Grida Canvas", "scripts": { + "bench": "vitest bench", + "fig2grida": "tsx fig2grida.ts", "test": "vitest run", - "typecheck": "tsc --noEmit", - "fig2grida": "tsx fig2grida.ts" + "typecheck": "tsc --noEmit" }, "dependencies": { "@grida/cmath": "workspace:*", diff --git a/packages/grida-canvas-io/__tests__/format-roundtrip.test.ts b/packages/grida-canvas-io/__tests__/format-roundtrip.test.ts index a744193af9..08b8316843 100644 --- a/packages/grida-canvas-io/__tests__/format-roundtrip.test.ts +++ b/packages/grida-canvas-io/__tests__/format-roundtrip.test.ts @@ -2792,4 +2792,119 @@ describe("format roundtrip", () => { ); }); }); + + describe("stroke_dash_array roundtrip", () => { + const dashArray = [10, 5, 3, 5]; + + it("roundtrips stroke_dash_array on VectorNode", () => { + const sceneId = "0-1"; + const nodeId = "0-2"; + const doc = createDocument(sceneId, { + [nodeId]: { + ...baseVector(nodeId), + stroke_dash_array: dashArray, + }, + }); + roundtripTest( + doc, + nodeId, + "vector", + (node) => { + expect(node.stroke_dash_array).toEqual(dashArray); + } + ); + }); + + it("roundtrips stroke_dash_array on LineNode", () => { + const sceneId = "0-1"; + const nodeId = "0-2"; + const doc = createDocument(sceneId, { + [nodeId]: { + ...baseLine(nodeId), + stroke_dash_array: dashArray, + }, + }); + roundtripTest( + doc, + nodeId, + "line", + (node) => { + expect(node.stroke_dash_array).toEqual(dashArray); + } + ); + }); + + it("roundtrips stroke_dash_array on PathNode", () => { + const sceneId = "0-1"; + const nodeId = "0-2"; + const doc = createDocument(sceneId, { + [nodeId]: { + ...basePath(nodeId), + stroke_dash_array: dashArray, + }, + }); + roundtripTest( + doc, + nodeId, + "path", + (node) => { + expect(node.stroke_dash_array).toEqual(dashArray); + } + ); + }); + + it("roundtrips stroke_dash_array on ContainerNode", () => { + const sceneId = "0-1"; + const nodeId = "0-2"; + const doc = createDocument(sceneId, { + [nodeId]: { + ...baseContainer(nodeId), + stroke_dash_array: dashArray, + }, + }); + roundtripTest( + doc, + nodeId, + "container", + (node) => { + expect(node.stroke_dash_array).toEqual(dashArray); + } + ); + }); + + it("roundtrips stroke_dash_array on BooleanPathOperationNode", () => { + const sceneId = "0-1"; + const nodeId = "0-2"; + const doc = createDocument(sceneId, { + [nodeId]: { + ...baseBoolean(nodeId), + stroke_dash_array: dashArray, + }, + }); + roundtripTest( + doc, + nodeId, + "boolean", + (node) => { + expect(node.stroke_dash_array).toEqual(dashArray); + } + ); + }); + + it("roundtrips undefined stroke_dash_array (no dash)", () => { + const sceneId = "0-1"; + const nodeId = "0-2"; + const doc = createDocument(sceneId, { + [nodeId]: baseVector(nodeId), + }); + roundtripTest( + doc, + nodeId, + "vector", + (node) => { + expect(node.stroke_dash_array).toBeUndefined(); + } + ); + }); + }); }); diff --git a/packages/grida-canvas-io/format.ts b/packages/grida-canvas-io/format.ts index d8895ead88..a4b0495cad 100644 --- a/packages/grida-canvas-io/format.ts +++ b/packages/grida-canvas-io/format.ts @@ -1486,6 +1486,7 @@ export namespace format { stroke_cap: containerNode.stroke_cap, stroke_join: containerNode.stroke_join, stroke_width: containerNode.stroke_width, + stroke_dash_array: containerNode.stroke_dash_array, rectangular_stroke_width_top: containerNode.rectangular_stroke_width_top, rectangular_stroke_width_right: @@ -1547,6 +1548,7 @@ export namespace format { stroke_width: lineNode.stroke_width, stroke_cap: lineNode.stroke_cap, stroke_join: lineNode.stroke_join, + stroke_dash_array: lineNode.stroke_dash_array, }); const strokePaintsFiltered = paints(lineNode, "stroke"); const strokePaintsOffset = format.paint.encode.strokePaints( @@ -1600,6 +1602,7 @@ export namespace format { stroke_width: vectorNode.stroke_width, stroke_cap: vectorNode.stroke_cap, stroke_join: vectorNode.stroke_join, + stroke_dash_array: vectorNode.stroke_dash_array, }); const vectorWithSmoothing = vectorNode as grida.program.nodes.VectorNode & @@ -1663,6 +1666,7 @@ export namespace format { stroke_width: pathNode.stroke_width, stroke_cap: pathNode.stroke_cap, stroke_join: pathNode.stroke_join, + stroke_dash_array: pathNode.stroke_dash_array, }); const fillPaintsFiltered = paints(pathNode, "fill"); const fillPaintsOffset = format.paint.encode.fillPaints( @@ -1709,6 +1713,7 @@ export namespace format { stroke_width: booleanNode.stroke_width, stroke_cap: booleanNode.stroke_cap, stroke_join: booleanNode.stroke_join, + stroke_dash_array: booleanNode.stroke_dash_array, }); const booleanWithSmoothing = booleanNode as grida.program.nodes.BooleanPathOperationNode & @@ -2668,6 +2673,7 @@ export namespace format { stroke_cap?: cg.StrokeCap; stroke_join?: cg.StrokeJoin; stroke_width?: number; + stroke_dash_array?: number[]; rectangular_stroke_width_top?: number; rectangular_stroke_width_right?: number; rectangular_stroke_width_bottom?: number; @@ -2677,7 +2683,8 @@ export namespace format { const strokeStyleOffset = createStrokeStyle( builder, node.stroke_cap, - node.stroke_join + node.stroke_join, + node.stroke_dash_array ); // Create VariableWidthProfile (empty for now) @@ -3006,6 +3013,7 @@ export namespace format { rectangular_stroke_width_left: number; stroke_cap: cg.StrokeCap; stroke_join: cg.StrokeJoin; + stroke_dash_array?: number[]; } { if (!trait) { return { @@ -3026,6 +3034,18 @@ export namespace format { ? styling.decode.strokeJoin(strokeStyle.strokeJoin()) : "miter"; + // Decode dash array + let stroke_dash_array: number[] | undefined; + if (strokeStyle) { + const len = strokeStyle.strokeDashArrayLength(); + if (len > 0) { + stroke_dash_array = []; + for (let i = 0; i < len; i++) { + stroke_dash_array.push(strokeStyle.strokeDashArray(i)!); + } + } + } + const strokeWidth = trait.rectangularStrokeWidth(); return { rectangular_stroke_width_top: strokeWidth?.strokeTopWidth() ?? 0, @@ -3035,6 +3055,7 @@ export namespace format { rectangular_stroke_width_left: strokeWidth?.strokeLeftWidth() ?? 0, stroke_cap: cap, stroke_join: join, + ...(stroke_dash_array ? { stroke_dash_array } : {}), }; } @@ -5067,6 +5088,9 @@ export namespace format { format.shape.decode.deriveStrokeWidth(strokeGeometryProps), stroke_cap: strokeGeometryProps.stroke_cap, stroke_join: strokeGeometryProps.stroke_join, + ...(strokeGeometryProps.stroke_dash_array + ? { stroke_dash_array: strokeGeometryProps.stroke_dash_array } + : {}), rectangular_corner_radius_top_left: cornerRadiusProps.rectangular_corner_radius_top_left, rectangular_corner_radius_top_right: @@ -5331,6 +5355,9 @@ export namespace format { marker_end_shape: enums.STROKE_MARKER_PRESET_DECODE.get(n.markerEndShape()) ?? "none", + ...(strokeGeometryProps.stroke_dash_array + ? { stroke_dash_array: strokeGeometryProps.stroke_dash_array } + : {}), ...(effects || {}), } satisfies grida.program.nodes.LineNode; } @@ -5472,6 +5499,9 @@ export namespace format { stroke_width: strokeGeometryProps.stroke_width, stroke_cap: strokeGeometryProps.stroke_cap, stroke_join: strokeGeometryProps.stroke_join, + ...(strokeGeometryProps.stroke_dash_array + ? { stroke_dash_array: strokeGeometryProps.stroke_dash_array } + : {}), data: n.data() ?? "", fill_rule: fillRule, ...(effects || {}), @@ -5531,6 +5561,9 @@ export namespace format { stroke_width: strokeGeometryProps.stroke_width, stroke_cap: strokeGeometryProps.stroke_cap, stroke_join: strokeGeometryProps.stroke_join, + ...(strokeGeometryProps.stroke_dash_array + ? { stroke_dash_array: strokeGeometryProps.stroke_dash_array } + : {}), ...(effects || {}), } satisfies grida.program.nodes.BooleanPathOperationNode; } diff --git a/packages/grida-canvas-io/index.ts b/packages/grida-canvas-io/index.ts index 1a13509f9c..a38545446e 100644 --- a/packages/grida-canvas-io/index.ts +++ b/packages/grida-canvas-io/index.ts @@ -932,11 +932,21 @@ export namespace io { * @param bitmaps - Optional bitmap assets to include in the archive * @returns Uint8Array containing the ZIP archive */ + export interface PackOptions { + /** + * ZIP compression level (0–9). 0 = store-only (fastest), 6 = default, + * 9 = best compression. Use 0 for transient archives that will be + * immediately consumed (e.g. fig2grida → io.load round-trip). + */ + level?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; + } + export function pack( document: grida.program.document.Document, images?: Record, schemaVersion: string = grida.program.document.SCHEMA_VERSION, - bitmaps?: Record + bitmaps?: Record, + options?: PackOptions ): Uint8Array { // Extract bitmaps from document if not provided const inferredBitmaps: Record | undefined = @@ -1018,7 +1028,7 @@ export namespace io { } } - return zipSync(files); + return zipSync(files, { level: options?.level ?? 6 }); } /** @@ -1451,8 +1461,7 @@ export namespace io { */ async quarantine(label?: string): Promise { const dir = await this.getDirectoryHandle(); - const safeLabel = - label ?? new Date().toISOString().replace(/:/g, "-"); + const safeLabel = label ?? new Date().toISOString().replace(/:/g, "-"); const archiveRoot = await dir.getDirectoryHandle("_quarantine", { create: true, });