const t=!1,i=!0,e=new Float32Array(1),n=new Int32Array(e.buffer);class s{endianness=i;position=0;get length(){throw new Error("Deprecated, use 'capacity' instead")}set length(t){throw new Error("Deprecated, use 'capacity' instead")}capacity=1024;data=new ArrayBuffer(1024);dataView=new DataView(this.data);__data_uint8=new Uint8Array(this.data);__growFactor=1.1;get raw_bytes(){return this.__data_uint8}fromArrayBuffer(t){this.data=t,this.dataView=new DataView(t),this.__data_uint8=new Uint8Array(t),this.capacity=t.byteLength,this.position=0}trim(){return this.setCapacity(this.position),this}skip(t){this.position+=t}setCapacity(t){if(t<this.position)throw new Error(`Attempting to set capacity(=${t}) below current position(=${this.position})`);if(this.capacity===t)return;const i=this.__data_uint8,e=new Uint8Array(t);this.position>0&&function(t,i,e,n,s){let r,o;const a=function(t,i,e){const n=0|e;return 3&n?1&n?1:2:4}(0,0,s);4===a?(r=new Uint32Array(t,0,s>>>2),o=new Uint32Array(e,0,s>>>2)):2===a?(r=new Uint16Array(t,0,s>>>1),o=new Uint16Array(e,0,s>>>1)):(r=new Uint8Array(t,0,s),o=new Uint8Array(e,0,s)),o.set(r)}(i.buffer,0,e.buffer,0,Math.min(i.buffer.byteLength,e.buffer.byteLength,this.position)),this.data=e.buffer,this.__data_uint8=e,this.dataView=new DataView(this.data),this.capacity=t}ensureCapacity(t){const i=this.capacity;if(i>=t)return;const e=(n=Math.ceil(Math.max(t,i*this.__growFactor,i+1024)))+(4-n%4)%4;var n;this.setCapacity(e)}readFloat16(){return function(t){let i=0;const e=(31744&t)>>10,n=1023&t;let s=1;return t>>15&&(s=-1),i=0!==e?31===e?0!==n?NaN:Infinity*s:s*Math.pow(2,e-15)*(1+n/1024):s*(n/1024*6103515625e-14),i}(this.readUint16())}readFloat32(){const t=this.dataView.getFloat32(this.position,this.endianness);return this.position+=4,t}readFloat64(){const t=this.dataView.getFloat64(this.position,this.endianness);return this.position+=8,t}readInt8(){const t=this.dataView.getInt8(this.position);return this.position+=1,t}readInt16(){const t=this.dataView.getInt16(this.position,this.endianness);return this.position+=2,t}readInt16LE(){const t=this.dataView.getInt16(this.position,i);return this.position+=2,t}readInt16BE(){const i=this.dataView.getInt16(this.position,t);return this.position+=2,i}readInt32(){const t=this.dataView.getInt32(this.position,this.endianness);return this.position+=4,t}readInt32LE(){const t=this.dataView.getInt32(this.position,i);return this.position+=4,t}readInt32BE(){const i=this.dataView.getInt32(this.position,t);return this.position+=4,i}readInt64(){const t=this.dataView.getBigInt64(this.position,this.endianness);return this.position+=8,t}readUint8(){const t=this.dataView.getUint8(this.position);return this.position+=1,t}readUint16(){const t=this.dataView.getUint16(this.position,this.endianness);return this.position+=2,t}readUint16LE(){const t=this.dataView.getUint16(this.position,i);return this.position+=2,t}readUint16BE(){const i=this.dataView.getUint16(this.position,t);return this.position+=2,i}readUint24(){return this.endianness===t?this.readUint24BE():this.readUint24LE()}readUint24LE(){const t=this.dataView.getUint8(this.position),i=this.dataView.getUint8(this.position+1),e=this.dataView.getUint8(this.position+2);return this.position+=3,t|i<<8|e<<16}readUint24BE(){const t=this.dataView.getUint8(this.position),i=this.dataView.getUint8(this.position+1),e=this.dataView.getUint8(this.position+2);return this.position+=3,e|i<<8|t<<16}readUint32(){const t=this.dataView.getUint32(this.position,this.endianness);return this.position+=4,t}readUint32LE(){const t=this.dataView.getUint32(this.position,i);return this.position+=4,t}readUint32BE(){const i=this.dataView.getUint32(this.position,t);return this.position+=4,i}readUint64(){const t=this.dataView.getBigUint64(this.position,this.endianness);return this.position+=8,t}readUint8Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readUint8()}readUint16Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readUint16()}readUint32Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readUint32()}readInt8Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readInt8()}readInt16Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readInt16()}readInt32Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readInt32()}readFloat32Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readFloat32()}readFloat64Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readFloat64()}writeFloat32Array(t,i,e){for(let n=0;n<e;n++)this.writeFloat32(t[n+i])}writeFloat16Array(t,i,e){for(let n=0;n<e;n++)this.writeFloat16(t[n+i])}readFloat16Array(t,i,e){for(let n=0;n<e;n++)t[n+i]=this.readFloat16()}writeFloat16(t){const i=function(t){let i=t;Number.isFinite(i)&&(i>65504||i<-65504)&&(i=i>0?65504:-65504),e[0]=i;const s=n[0];let r=s>>16&32768,o=s>>12&2047;const a=s>>23&255;return a<103?r:a>142?(r|=31744,255===a&&8388607&s&&(r|=512),r):a<113?(o|=2048,r|=(o>>114-a)+(o>>113-a&1),r):(r|=a-112<<10|o>>1,r+=1&o,r)}(t);this.writeUint16(i)}writeFloat32(t){const i=this.position+4;this.ensureCapacity(i),this.dataView.setFloat32(this.position,t,this.endianness),this.position=i}writeFloat64(t){const i=this.position+8;this.ensureCapacity(i),this.dataView.setFloat64(this.position,t,this.endianness),this.position=i}writeInt8(t){const i=this.position+1;this.ensureCapacity(i),this.dataView.setInt8(this.position,t),this.position=i}writeInt16(t){const i=this.position+2;this.ensureCapacity(i),this.dataView.setInt16(this.position,t,this.endianness),this.position=i}writeInt16BE(i){const e=this.position+2;this.ensureCapacity(e),this.dataView.setInt16(this.position,i,t),this.position=e}writeInt16LE(t){const e=this.position+2;this.ensureCapacity(e),this.dataView.setInt16(this.position,t,i),this.position=e}writeInt32(t){const i=this.position+4;this.ensureCapacity(i),this.dataView.setInt32(this.position,t,this.endianness),this.position=i}writeInt32BE(i){const e=this.position+4;this.ensureCapacity(e),this.dataView.setInt32(this.position,i,t),this.position=e}writeInt32LE(t){const e=this.position+4;this.ensureCapacity(e),this.dataView.setInt32(this.position,t,i),this.position=e}writeInt64(t){const i=this.position+8;this.ensureCapacity(i),this.dataView.setBigInt64(this.position,t,this.endianness),this.position=i}writeInt8Array(t,i,e){this.ensureCapacity(this.position+e);for(let n=0;n<e;n++)this.writeInt8(t[i+n])}writeInt16Array(t,i,e){this.ensureCapacity(this.position+2*e);for(let n=0;n<e;n++)this.writeInt16(t[i+n])}writeInt32Array(t,i,e){this.ensureCapacity(this.position+4*e);for(let n=0;n<e;n++)this.writeInt32(t[i+n])}writeUint8(t){const i=this.position+1;this.ensureCapacity(i),this.dataView.setUint8(this.position,t),this.position=i}writeUint8Array(t,i,e){for(let n=0;n<e;n++)this.writeUint8(t[i+n])}writeUint16(t){const i=this.position+2;this.ensureCapacity(i),this.dataView.setUint16(this.position,t,this.endianness),this.position=i}writeUint16BE(i){const e=this.position+2;this.ensureCapacity(e),this.dataView.setUint16(this.position,i,t),this.position=e}writeUint16LE(t){const e=this.position+2;this.ensureCapacity(e),this.dataView.setUint16(this.position,t,i),this.position=e}writeUint16Array(t,i,e){for(let n=0;n<e;n++)this.writeUint16(t[i+n])}writeUint24(i){this.endianness===t?this.writeUint24BE(i):this.writeUint24LE(i)}writeUint24BE(t){const i=this.position+3;this.ensureCapacity(i);const e=255&t,n=t>>8&255,s=t>>16&255;this.dataView.setUint8(this.position,s),this.dataView.setUint8(this.position+1,n),this.dataView.setUint8(this.position+2,e),this.position=i}writeUint24LE(t){const i=this.position+3;this.ensureCapacity(i);const e=255&t,n=t>>8&255,s=t>>16&255;this.dataView.setUint8(this.position,e),this.dataView.setUint8(this.position+1,n),this.dataView.setUint8(this.position+2,s),this.position=i}writeUintVar(t){let i=!0,e=t;for(;i||0!==e;){i=!1;let t=127&e;e>>=7,e>0&&(t|=128),this.writeUint8(t)}}readUintVar(){let t=!0,i=0,e=0;for(;t;){let n=this.readUint8();t=!!(128&n),i|=(127&n)<<e,e+=7}return i}writeUint32(t){const i=this.position+4;this.ensureCapacity(i),this.dataView.setUint32(this.position,t,this.endianness),this.position=i}writeUint32BE(i){const e=this.position+4;this.ensureCapacity(e),this.dataView.setUint32(this.position,i,t),this.position=e}writeUint32LE(t){const e=this.position+4;this.ensureCapacity(e),this.dataView.setUint32(this.position,t,i),this.position=e}writeUint64(t){const i=this.position+8;this.ensureCapacity(i),this.dataView.setBigUint64(this.position,t,this.endianness),this.position=i}writeUint32Array(t,i,e){this.ensureCapacity(this.position+4*e);for(let n=0;n<e;n++)this.writeUint32(t[i+n])}writeBytes(t,i,e){const n=i+e,s=this.position,r=s+e;if(this.ensureCapacity(r),0===i&&t.length===e)this.__data_uint8.set(t,s);else if("function"==typeof t.subarray)this.__data_uint8.set(t.subarray(i,n),s);else for(let n=0;n<e;n++)this.__data_uint8[s+n]=t[i+n];this.position=r}readBytes(t,i,e){const n=this.position,s=n+e,r=this.__data_uint8;e<128?function(t,i,e,n,s){let r,o,a;for(a=0;a<s;a++)r=i+a,o=n+a,e[o]=t[r]}(r,n,t,i,e):t.set(r.subarray(n,s),i),this.position=s}writeUTF8String(t){if(null===t)return void this.writeUint32(4294967295);if(void 0===t)return void this.writeUint32(4294967294);let i=0;const e=t.length;if(e>=4294967294)throw new Error("String is too long");this.writeUint32(e);let n=this.position;const s=Math.max(32,e+(e>>1)+7);this.ensureCapacity(s+n);let r=this.__data_uint8,o=this.capacity;for(;i<e;){let s=t.charCodeAt(i++);if(s>=55296&&s<=56319){if(i<e){const e=t.charCodeAt(i);56320==(64512&e)&&(++i,s=((1023&s)<<10)+(1023&e)+65536)}s>=55296&&s<=56319&&(s=65533)}if(n+4>o&&(this.position=n,this.ensureCapacity(n+4),o=this.capacity,r=this.__data_uint8),4294967168&s){if(4294965248&s)if(4294901760&s){if(4292870144&s)continue;r[n++]=s>>18&7|240,r[n++]=s>>12&63|128,r[n++]=s>>6&63|128}else r[n++]=s>>12&15|224,r[n++]=s>>6&63|128;else r[n++]=s>>6&31|192;r[n++]=63&s|128}else r[n++]=s}this.position=n}readUTF8String(){const t=this.readUint32();if(4294967295===t)return null;if(4294967294===t)return;const i=this.__data_uint8;let e="",n=this.position,s=0;for(;n<this.capacity&&s<t;){const t=i[n++];let r;if(0===t)break;128&t?192==(224&t)?r=(31&t)<<6|63&i[n++]:224==(240&t)?r=(31&t)<<12|(63&i[n++])<<6|63&i[n++]:240==(248&t)&&(r=(7&t)<<18|(63&i[n++])<<12|(63&i[n++])<<6|63&i[n++],r>65535&&(r-=65536,e+=String.fromCharCode(r>>>10&1023|55296),s++,r=56320|1023&r)):r=t,s++,e+=String.fromCharCode(r)}return this.position=n,e}writeASCIIString(t){const i=t.length,e=this.position,n=e+i;this.ensureCapacity(n);for(let n=0;n<i;n++){const i=t.charCodeAt(n);if(i>127)throw new Error(`Character ${String.fromCharCode(i)} can't be represented by a US-ASCII byte.`);this.__data_uint8[e+n]=i}this.position=n}readASCIICharacters(t,i=!1){let e="";for(let n=0;n<t;n++){const t=this.readUint8();if(i&&0===t)break;e+=String.fromCharCode(t)}return e}toString(){return`BinaryBuffer[position=${this.position}, capacity=${this.capacity}, endianness=${this.endianness}]`}toHexString(){const t=this.__data_uint8,i=Math.min(t.length,this.position);let e="";for(let n=0;n<i;n++)e+=t[n].toString(16).padStart(2,"0").toUpperCase();return e}static fromEndianness(t){const i=new s;return i.endianness=t,i}static fromArrayBuffer(t){const i=new s;return i.fromArrayBuffer(t),i}static copyUTF8String(t,i){const e=t.readUTF8String();return i.writeUTF8String(e),e}static copyUintVar(t,i){const e=t.readUintVar();return i.writeUintVar(e),e}static copyUint8(t,i){const e=t.readUint8();return i.writeUint8(e),e}static copyUint16(t,i){const e=t.readUint16();return i.writeUint16(e),e}static copyUint32(t,i){const e=t.readUint32();return i.writeUint32(e),e}static copyFloat32(t,i){const e=t.readFloat32();return i.writeFloat32(e),e}static copyFloat64(t,i){const e=t.readFloat64();return i.writeFloat64(e),e}static copyBytes(t,i,e){const n=new Uint8Array(e);return t.readBytes(n,0,e),i.writeBytes(n,0,e),n}}s.prototype.isBinaryBuffer=!0;let r=i,o=!1;async function a(t,i="deflate"){let e=0;const n=new ReadableStream({pull(i){e===t.length?i.close():i.enqueue(t[e++])}}).pipeThrough(new DecompressionStream(i));return new Uint8Array(await new Response(n).arrayBuffer())}async function h(t,i=!1){try{return await a(t)}catch(e){if(i){const i=function(t){const i=t.length;if(1===i)return t[0];let e=0;for(let n=0;n<i;n++)e+=t[n].length;const n=new Uint8Array(e);let s=0;for(let e=0;e<i;e++)n.set(t[e],s),s+=t[e].length;return n}(t),e=i[0],n=i[1];if(i.length>=6&&8==(15&e)&&e>>>4<=7&&(e<<8|n)%31==0&&!(32&n))try{return await a([i.subarray(2,i.length-4)],"deflate-raw")}catch{}}throw new Error(`failed to inflate zlib stream: ${e?.message||"invalid or truncated data"}`)}}const c=new Uint32Array(256);for(let t=0;t<256;t++){let i=t;for(let t=0;t<8;t++)1&i?i=3988292384^i>>>1:i>>>=1;c[t]=i}const p=Object.freeze([function(t,i,e,n,s,r,o){for(let n=0;n<o;n++)e[s+n]=t[i+n]},function(t,i,e,n,s,r,o){let a=0;for(;a<n;a++)e[s+a]=t[a+i];for(;a<o;a++){const r=s+a;e[r]=t[a+i]+e[r-n]&255}},function(t,i,e,n,s,r,o){let a,h,c=0;if(r<0)for(;c<o;c++)e[s+c]=t[c+i];else for(;c<o;c++)a=t[c+i],h=e[r+c],e[s+c]=a+h&255},function(t,i,e,n,s,r,o){let a,h,c,p=0;if(r<0){for(;p<n;p++)e[s+p]=t[p+i];for(;p<o;p++){const r=s+p;e[r]=t[p+i]+(e[r-n]>>1)&255}}else{for(;p<n;p++)e[s+p]=t[p+i]+(e[r+p]>>1)&255;for(;p<o;p++)a=t[p+i],h=e[s+p-n],c=e[r+p],e[s+p]=a+(h+c>>1)&255}},function(t,i,e,n,s,r,o){let a,h,c,p,d,f,l,u,w,y=0;if(r<0){for(;y<n;y++)e[s+y]=t[y+i];for(;y<o;y++)e[s+y]=t[y+i]+e[s+y-n]&255}else{for(;y<n;y++)e[s+y]=t[y+i]+e[r+y]&255;for(;y<o;y++)a=t[y+i],p=e[r+y-n],c=e[r+y],h=e[s+y-n],d=h+c-p,f=Math.abs(d-h),l=Math.abs(d-c),u=Math.abs(d-p),w=f<=l&&f<=u?h:l<=u?c:p,e[s+y]=a+w&255}}]);function d(t,i,e,n,s,r,o,a){const h=p[t];if(void 0===h)throw new Error(`unknown filtered scanline type '${t}'`);h(i,e,n,s,r,o,a)}function f(t,i,e,n,s,r){if(1!==e&&2!==e&&4!==e)throw new Error(`only bit depths below 8 are packed, got ${e}`);const o=(1<<e)-1;for(let a=0;a<n;a++){const n=a*e,h=t[i+(n>>3)],c=8-e-(7&n);s[r+a]=h>>>c&o}}class l{width=0;height=0;bitDepth=0;colorType=0;compressionMethod=0;filterMethod=0;interlaceMethod=0;colors=0;alpha=!1;palette=null;pixels=null;transparency_lookup=null;text={};getWidth(){return this.width}setWidth(t){this.width=t}getHeight(){return this.height}setHeight(t){this.height=t}getBitDepth(){return this.bitDepth}setBitDepth(t){if(-1===[1,2,4,8,16].indexOf(t))throw new Error("invalid bith depth "+t);this.bitDepth=t}getColorType(){return this.colorType}setColorType(t){let i=0,e=!1;switch(t){case 0:case 3:i=1;break;case 2:i=3;break;case 4:i=2,e=!0;break;case 6:i=4,e=!0;break;default:throw new Error("invalid color type")}this.colors=i,this.alpha=e,this.colorType=t}setCompressionMethod(t){if(0!==t)throw new Error("invalid compression method "+t);this.compressionMethod=t}setFilterMethod(t){if(0!==t)throw new Error("invalid filter method "+t);this.filterMethod=t}getInterlaceMethod(){return this.interlaceMethod}setInterlaceMethod(t){if(0!==t&&1!==t)throw new Error("invalid interlace method "+t);this.interlaceMethod=t}setPalette(t){if(t.length%3!=0)throw new Error("incorrect PLTE chunk length");if(t.length>3*Math.pow(2,this.bitDepth))throw new Error("palette has more colors than 2^bitdepth");this.palette=t}getUint8Data_case3(){const t=this.width*this.height;let i;const e=this.transparency_lookup;i=null!==e?4:3;const n=new Uint8Array(t*i),s=this.pixels,r=this.palette,o=this.colors*Math.ceil(this.bitDepth/8);for(let e=0;e<t;e++){const t=e*i,a=3*s[e*o];n[t]=r[a],n[t+1]=r[a+1],n[t+2]=r[a+2]}if(null!==e){const i=e.length;for(let r=0;r<t;r++){const t=s[r*o];n[4*r+3]=t>=i?255:e[t]}}return{data:n,itemSize:i}}getUint8Data(){let t,i=0;switch(this.colorType){case 0:t=this.pixels,i=1;break;case 2:t=this.pixels,i=3;break;case 3:const e=this.getUint8Data_case3();t=e.data,i=e.itemSize;break;case 4:t=this.pixels,i=2;break;case 6:t=this.pixels,i=4;break;default:throw new Error("Unsupported color type")}return{data:t,itemSize:i}}}const u=[137,80,78,71,13,10,26,10];function w(t,i){return t[i]<<24|t[i+1]<<16|t[i+2]<<8|t[i+3]}function y(t,i){return t[i]}function U(i){this.png=new l,this.dataChunks=[],this.buffer=new s,this.buffer.endianness=t,this.buffer.fromArrayBuffer(i),this.crc_enabled=!1}U.prototype.readBytes=function(t){const i=this.buffer,e=new Uint8Array(i.data,i.position,t);return i.skip(t),e},U.prototype.decodeHeader=function(){const t=this.buffer,i=new Uint8Array(u.length);if(t.readBytes(i,0,i.length),!function(t,i){const e=t.length;if(e!==i.length)return!1;for(let n=0;n<e;n++)if(t[n]!==i[n])return!1;return!0}(i,u))throw new Error("invalid PNGReader file (bad signature)")},U.prototype.decodeChunk=async function(){const t=this.buffer,i=t.readUint32();if(i<0)throw new Error("Bad chunk length "+(4294967295&i));const e=t.position,n=t.readASCIICharacters(4),r=this.readBytes(i);switch(t.readUint32(),this.crc_enabled&&function(t,i=0,e=t.length){!function(t,i,e,n){let s=4294967295;const r=e+n;for(let t=e;t<r;t++){const e=i[t];s=c[255&(s^e)]^s>>>8}}(0,t,i,e)}(t.raw_bytes,e,i+4),n){case"IHDR":this.decodeIHDR(r);break;case"PLTE":this.decodePLTE(r);break;case"IDAT":this.decodeIDAT(r);break;case"tRNS":this.decodeTRNS(r);break;case"IEND":this.decodeIEND(r);break;case"sRGB":this.decodesRGB(r);break;case"tIME":this.decodetIME(r);break;case"zTXt":await async function(t){const i=t.byteLength,e=t.buffer,n=t.byteOffset,r=s.fromArrayBuffer(e);r.position=n;const o=r.readASCIICharacters(79,!0),a=r.readUint8();let c;if(0!==a)throw new Error(`Unsupported compression method '${a}'`);{const t=r.position-n,s=new Uint8Array(e,r.position,i-t),o=await h([s]);r.fromArrayBuffer(o.buffer),c=r.readASCIICharacters(o.byteLength)}return{keyword:o,text:c}}(r);break;case"iTXt":await async function(t){const i=t.byteLength,e=t.buffer,n=t.byteOffset,r=n+i,o=s.fromArrayBuffer(e);o.position=n;const a=o.readASCIICharacters(79,!0),c=o.readUint8(),p=o.readUint8(),d=o.readASCIICharacters(Number.MAX_SAFE_INTEGER,!0),f=o.readASCIICharacters(Number.MAX_SAFE_INTEGER,!0),l=r-o.position;let u;if(0===c)u=o.readASCIICharacters(l);else{if(1!==c)throw new Error(`Invalid compression flag value '${c}'`);{if(0!==p)throw new Error("only compression_method 0 is supported");const t=await h([new Uint8Array(o.data,o.position,l)]);o.fromArrayBuffer(t.buffer),u=o.readASCIICharacters(t.byteLength)}}return{keyword:a,language_tag:d,translated_keyword:f,text:u}}(r)}return n},U.prototype.decodesRGB=function(t){y(t,0)},U.prototype.decodetIME=function(t){y(t,0),y(t,1),y(t,2),y(t,3),y(t,4),y(t,5),y(t,6)},U.prototype.decodetEXt=function(t){const i=s.fromArrayBuffer(t.buffer),e=i.readASCIICharacters(Number.POSITIVE_INFINITY,!0),n=i.readASCIICharacters(e.length-1,!1);this.png.text[e]=n},U.prototype.decodeiEXt=function(t){const i=s.fromArrayBuffer(t.buffer),e=i.readASCIICharacters(Number.POSITIVE_INFINITY,!0);if(i.readUint8(),i.readUint8(),i.readASCIICharacters(Number.POSITIVE_INFINITY,!0),i.readUTF8String(),0!==i.readUint8())throw new Error("Expected Null Separator after Translated keyword");const n=i.readUTF8String();this.png.text[e]=n},U.prototype.decodeIHDR=function(t){const i=this.png;i.setWidth(w(t,0)),i.setHeight(w(t,4)),i.setBitDepth(y(t,8)),i.setColorType(y(t,9)),i.setCompressionMethod(y(t,10)),i.setFilterMethod(y(t,11)),i.setInterlaceMethod(y(t,12))},U.prototype.decodePLTE=function(t){this.png.setPalette(t)},U.prototype.decodeIDAT=function(t){this.dataChunks.push(t)},U.prototype.decodeTRNS=function(t){this.png.transparency_lookup=t},U.prototype.decodeIEND=function(){},U.prototype.decodePixels=async function(){const e=this.png,n=await h(this.dataChunks,!0);if(0===e.getInterlaceMethod()?this.png.pixels=this.interlaceNone(n):this.png.pixels=this.interlaceAdam7(n),16===e.bitDepth&&function(){if(o)return r;const e=new ArrayBuffer(2),n=new Uint8Array(e),s=new Uint16Array(e);return n[0]=19,r=19==(255&s[0])?i:t,o=!0,r}()===i){const t=this.png.pixels;for(let i=0;i<t.length;i+=2){const e=t[i];t[i]=t[i+1],t[i+1]=e}}},U.prototype.interlaceNone=function(t){const i=this.png,e=i.bitDepth,n=i.colors,s=i.width,r=i.height,o=Math.max(1,Math.ceil(n*e/8)),a=Math.ceil(n*e*s/8);if(e<8)return function(t,i,e,n,s,r){const o=new Uint8Array(e*r),a=new Uint8Array(2*s),h=Math.floor(t.length/(s+1)),c=r<h?r:h;let p=0,l=-1;for(let r=0;r<c;r++){const h=(1&r)*s;d(t[p++],t,p,a,n,h,l,s),p+=s,f(a,h,i,e,o,r*e),l=h}return o}(t,e,n*s,o,a,r);const h=new Uint8Array(a*r);let c=0;const p=t.length;for(let i=0;i<p;i+=a+1){const e=i+1;d(t[i],t,e,h,o,c,c-a,a),c+=a}return h},U.prototype.interlaceAdam7=function(t){const i=this.png,e=i.bitDepth,n=i.colors,s=i.width,r=i.height,o=e<8,a=Math.max(1,Math.ceil(n*e/8)),h=n*(16===e?2:1),c=new Uint8Array(h*s*r),p=[{x:0,y:0,xStep:8,yStep:8},{x:4,y:0,xStep:8,yStep:8},{x:0,y:4,xStep:4,yStep:8},{x:2,y:0,xStep:4,yStep:4},{x:0,y:2,xStep:2,yStep:4},{x:1,y:0,xStep:2,yStep:2},{x:0,y:1,xStep:1,yStep:2}];let l=0;const u=new Uint8Array(2*Math.ceil(n*e*s/8)),w=o?new Uint8Array(n*s):null;for(let i=0;i<7;i++){const y=p[i],U=Math.ceil((s-y.x)/y.xStep),g=Math.ceil((r-y.y)/y.yStep);if(U<=0||g<=0)continue;const I=n*U,b=Math.ceil(I*e/8);let A=-1;for(let i=0;i<g;i++){const n=i%2*b;d(t[l++],t,l,u,a,n,A,b),l+=b,A=n;let p=u,g=n;o&&(f(u,n,e,I,w,0),p=w,g=0);for(let t=0;t<U;t++){const e=y.x+t*y.xStep,n=y.y+i*y.yStep;if(!(e>=s||n>=r))for(let i=0;i<h;i++)c[(n*s+e)*h+i]=p[g+t*h+i]}}}return c},U.prototype.unFilter=function(t,i,e,n,s,r,o,a){d(t,i,e,n,s,r,o,a)},U.prototype.parse=async function(){for(this.decodeHeader();"IEND"!==await this.decodeChunk(););return await this.decodePixels(),this.png},self.Lib={decode:async function(t,i="png"){if(!(t instanceof ArrayBuffer))throw new Error(".data argument must be an ArrayBuffer, instead was something else");if("png"===i.toLowerCase())return await async function(t){const i=await new U(t).parse(),e=i.getUint8Data();return{data:e.data.buffer,width:i.getWidth(),height:i.getHeight(),itemSize:e.itemSize,bitDepth:i.getBitDepth()}}(t);throw new Error("Unsupported type")}};
// Top-down scene example, built on Meep.
//
// 10,000 fantasy props on a ground plane. The camera is top-down at a fixed
// pitch/yaw/distance and is panned by:
// - WASD or arrow keys (held; integrated in postRender)
// - left-mouse drag on the main viewport (drag a world point under the
// cursor as you'd expect from any 2D map UI)
// - the minimap (click or drag in the corner widget)
//
// Engine pieces in use:
// - EngineHarness.bootstrap / .buildBasics
// - GLTFSceneBundleAssetLoader + ImageBitmapAssetLoader (engine.assetManager)
// - SGMesh / MeshSystem (one-line per-instance from URL)
// - ShadedGeometrySystem (the ground plane)
// - Pickable / PickingSystem (click-to-select, off the viz buffer)
// - Highlight / HighlightSystem (the selection outline)
// - TopDownCameraController (we drive its `target`; the
// controller's system re-derives
// the camera transform each tick)
//
// Ambient occlusion used to be a post-process plugin here. Under Shade it is
// GTAO built into the frame and on by default (`renderer.feature_ssao_enabled`),
// so there is nothing left to register.
import { Color } from "@woosh/meep-engine/src/core/color/Color.js";
import {
plane3_compute_ray_intersection
} from "@woosh/meep-engine/src/core/geom/3d/plane/plane3_compute_ray_intersection.js";
import Vector2 from "@woosh/meep-engine/src/core/geom/Vector2.js";
import Vector3 from "@woosh/meep-engine/src/core/geom/Vector3.js";
import { GameAssetType } from "@woosh/meep-engine/src/engine/asset/GameAssetType.js";
import { load_model_scene_bundle } from "@woosh/meep-engine/src/engine/asset/load_model_scene_bundle.js";
import { ArrayBufferLoader } from "@woosh/meep-engine/src/engine/asset/loaders/ArrayBufferLoader.js";
import {
GLTFSceneBundleAssetLoader
} from "@woosh/meep-engine/src/engine/asset/loaders/GLTFSceneBundleAssetLoader.js";
import {
ImageBitmapAssetLoader
} from "@woosh/meep-engine/src/engine/asset/loaders/image/ImageBitmapAssetLoader.js";
import Entity from "@woosh/meep-engine/src/engine/ecs/Entity.js";
import { ParentEntitySystem } from "@woosh/meep-engine/src/engine/ecs/parent/ParentEntitySystem.js";
import {
TransformAttachmentSystem
} from "@woosh/meep-engine/src/engine/ecs/transform-attachment/TransformAttachmentSystem.js";
import { Transform64 } from "@woosh/meep-engine/src/engine/ecs/transform/Transform64.js";
import { EngineHarness } from "@woosh/meep-engine/src/engine/EngineHarness.js";
import { Camera } from "@woosh/meep-engine/src/engine/graphics/ecs/camera/Camera.js";
import TopDownCameraController
from "@woosh/meep-engine/src/engine/graphics/ecs/camera/topdown/TopDownCameraController.js";
import Highlight from "@woosh/meep-engine/src/engine/graphics/ecs/highlight/Highlight.js";
import { SGMesh } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/aggregate/SGMesh.js";
import { ShadedGeometry } from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/ShadedGeometry.js";
import {
HighlightOutlineSystem
} from "@woosh/meep-engine/src/engine/graphics3/HighlightOutlineSystem.js";
import {
SGMeshHighlightSystem
} from "@woosh/meep-engine/src/engine/graphics/ecs/mesh-v2/aggregate/SGMeshHighlightSystem.js";
import { MeshSystem } from "@woosh/meep-engine/src/engine/graphics3/MeshSystem.js";
import { Pickable } from "@woosh/meep-engine/src/engine/graphics3/Pickable.js";
import { PickingQuery } from "@woosh/meep-engine/src/engine/graphics3/PickingQuery.js";
import { PickingSystem } from "@woosh/meep-engine/src/engine/graphics3/PickingSystem.js";
import { ShadedGeometrySystem } from "@woosh/meep-engine/src/engine/graphics3/ShadedGeometrySystem.js";
import {
make_plane_geometry
} from "@woosh/meep-engine/src/shade/renderer/geometry/primitives/make_plane_geometry.js";
import {
meshlet_geometry_build_from_geometry
} from "@woosh/meep-engine/src/shade/renderer/geometry/meshlet_geometry_build_from_geometry.js";
import { StandardAttributes } from "@woosh/meep-engine/src/shade/renderer/geometry/StandardAttributes.js";
import { StandardShadeMaterial } from "@woosh/meep-engine/src/shade/renderer/material/StandardShadeMaterial.js";
import { ColorSpace } from "@woosh/meep-engine/src/shade/renderer/texture/ColorSpace.js";
import { ShadeImage } from "@woosh/meep-engine/src/shade/renderer/texture/source/ShadeImage.js";
import { ShadeTexture } from "@woosh/meep-engine/src/shade/renderer/texture/ShadeTexture.js";
// ─── Scene parameters ───────────────────────────────────────────────────────
const PROP_COUNT = 10_000;
const GROUND_SIZE = 425; // keeps prop density at ~ 5,000 / 300²
const HALF_GROUND = GROUND_SIZE / 2;
const CAMERA_PITCH = 0.9; // rad, ~52° from horizontal
const CAMERA_YAW = Math.PI; // camera sits on +Z and looks toward -Z
const CAMERA_DISTANCE = 45;
const PROP_URL_BASE = "./models/fantasy-props/";
const PROP_URLS = [
"chair.gltf",
"table-1.gltf",
"bed.gltf",
"bookcase.gltf",
"chest-wood.gltf",
"bag-big.gltf",
"bag-01.gltf",
"candles-1.gltf",
"torch-long-1.gltf",
"coins-1.gltf",
"gem-blue.gltf",
"gem-green.gltf",
"gem-red.gltf",
"book-1-low.gltf",
].map((p) => PROP_URL_BASE + p);
// ─── Bootstrap ──────────────────────────────────────────────────────────────
//
// Every system that draws has to be handed the same Shade scene, which
// `EngineHarness.shadeScene(engine)` hands out (one per engine) — two of them
// holding different scenes draws a frame with most of the content missing.
//
// `MeshSystem` is given the loader as a function rather than reaching for the
// asset manager itself: `load_model_scene_bundle` goes through the asset
// framework, so the 14 prop models are fetched once between all 10,000
// placements and each placement gets its own nodes from the shared bundle.
const engine = await EngineHarness.bootstrap({
configuration: (config, engine) => {
const scene = EngineHarness.shadeScene(engine);
// glTF: the loader yields a Shade `SceneBundle`. Its `.bin` buffers and
// its images are requested back through the manager, so both of those
// loaders have to be present too — without ImageBitmapAssetLoader the
// props load as untextured geometry.
const gltfLoader = new GLTFSceneBundleAssetLoader();
config.addLoader(GameAssetType.ModelGLTF, gltfLoader);
config.addLoader(GameAssetType.ModelGLTF_JSON, gltfLoader);
config.addLoader(GameAssetType.ArrayBuffer, new ArrayBufferLoader());
config.addLoader(GameAssetType.ImageBitmap, new ImageBitmapAssetLoader());
const meshSystem = new MeshSystem(
engine.graphics,
scene,
(url) => load_model_scene_bundle(engine.assetManager, url),
);
config.addSystem(meshSystem);
config.addSystem(new ShadedGeometrySystem(engine.graphics, scene));
// A static model is not a thing an entity holds — MeshSystem turns the
// loaded file into a subtree of entities, one ShadedGeometry per
// primitive, parented to the prop by ParentEntity (lifetime) and
// TransformAttachment (pose). Both of those need their systems present:
// without TransformAttachmentSystem every prop's geometry sits at the
// model's own origin instead of where the prop was scattered.
config.addSystem(new ParentEntitySystem());
config.addSystem(new TransformAttachmentSystem());
// Highlighting reads what a model actually looks like, so it is given
// the mesh system rather than the scene. Picking reads the frame the
// renderer drew, so it is given the graphics engine.
config.addSystem(new PickingSystem(engine.graphics));
// The outline pass, plus the source that feeds it the props: a selected
// prop is an SGMesh model, so SGMeshHighlightSystem is the one it needs.
const highlightOutline = new HighlightOutlineSystem(engine.graphics);
config.addSystem(highlightOutline);
config.addSystem(new SGMeshHighlightSystem(highlightOutline, meshSystem));
},
});
await EngineHarness.buildBasics({
engine,
enableTerrain: false,
enableWater: false,
enableLights: true,
enableShadows: true,
focus: new Vector3(0, 0, 0),
distance: CAMERA_DISTANCE,
pitch: CAMERA_PITCH,
yaw: CAMERA_YAW,
shadowmapResolution: 2048,
cameraFieldOfView: 45,
cameraFarDistance: 100,
cameraController: false, // we wire our own input
showFps: false,
});
const ecd = engine.entityManager.dataset;
// ─── Procedural ground texture ──────────────────────────────────────────────
//
// Two-scale grid:
// - Minor lines every GROUND_MINOR_WORLD_UNITS units — fine sense of scale
// next to the props.
// - Major lines every GROUND_MAJOR_WORLD_UNITS units — thicker, brighter,
// act as bigger landmarks.
// Olive base color stays.
//
// Shade has no `CanvasTexture`: a canvas becomes an `ImageBitmap`, the bitmap
// becomes a `ShadeImage`, and the image becomes a `ShadeTexture`. The image's
// `color_space` is what says the bytes are sRGB-encoded — the renderer works in
// linear light and decodes on the way in.
const GROUND_MINOR_WORLD_UNITS = 2; // 1 minor cell = 2 world units
const GROUND_MAJOR_WORLD_UNITS = 8; // major line every 4 minor cells
const GROUND_MINOR_PER_TILE = 8; // texture tile spans 8 minor cells
const GROUND_TILE_WORLD_UNITS = GROUND_MINOR_PER_TILE * GROUND_MINOR_WORLD_UNITS; // 16
const MAJOR_STEP_CELLS = GROUND_MAJOR_WORLD_UNITS / GROUND_MINOR_WORLD_UNITS; // 4
const GROUND_TILES = GROUND_SIZE / GROUND_TILE_WORLD_UNITS;
async function makeGroundTexture() {
const size = 256;
const cellPx = size / GROUND_MINOR_PER_TILE; // 32 px per minor cell
const canvas = document.createElement("canvas");
canvas.width = canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#3c4838";
ctx.fillRect(0, 0, size, size);
function crosshatch(positions, lineWidth, color) {
ctx.strokeStyle = color;
ctx.lineWidth = lineWidth;
for (const p of positions) {
ctx.beginPath();
ctx.moveTo(p, 0);
ctx.lineTo(p, size);
ctx.moveTo(0, p);
ctx.lineTo(size, p);
ctx.stroke();
}
}
// Minor lines — every slot that isn't also a major.
const minorPositions = [];
for (let i = 1; i < GROUND_MINOR_PER_TILE; i++) {
if (i % MAJOR_STEP_CELLS === 0) continue;
minorPositions.push(Math.floor(i * cellPx) + 0.5);
}
crosshatch(minorPositions, 1, "rgba(255, 255, 255, 0.09)");
// Major lines — thicker and brighter.
const majorPositions = [];
for (let i = 0; i < GROUND_MINOR_PER_TILE; i += MAJOR_STEP_CELLS) {
// Inset the tile-boundary line slightly so it doesn't bleed across the wrap seam.
majorPositions.push(i === 0 ? 1.5 : Math.floor(i * cellPx) + 0.5);
}
crosshatch(majorPositions, 1.5, "rgba(255, 255, 255, 0.20)");
const image = ShadeImage.fromImageBitmap(await createImageBitmap(canvas));
image.color_space = ColorSpace.SRGB;
const texture = ShadeTexture.from(image);
// Every filter stays Linear on purpose. `sampler_descriptor_from_shade_texture`
// only asks for 16x anisotropy when min/mag/mip are all linear — a WebGPU
// rule, `maxAnisotropy > 1` is invalid otherwise — so a single
// `magFilter = Nearest` here would quietly drop this texture to isotropic
// sampling. The ground is the most anisotropic surface in the scene (a
// 425-unit plane read at a 52° grazing angle), so it is exactly the surface
// that pays for it: the grid lines break up and crawl into the distance.
// Nearest bought nothing anyway — at this camera a texel is about a pixel,
// so the ground is never really magnified.
// wrapS/wrapT are Repeat by default, which is what tiling needs. There is no
// `texture.repeat` under Shade — the tiling lives in the mesh's UVs instead
// (see below).
return texture;
}
// ─── Ground plane ───────────────────────────────────────────────────────────
//
// `make_plane_geometry` builds the same XY-facing quad three's PlaneGeometry
// did, so the entity is rotated a quarter turn about X to lay it flat rather
// than the geometry being baked pre-rotated.
//
// The subdivision is not cosmetic. Left as the default 1x1 quad this ground is
// two 425-unit triangles, and a flicker map — per-pixel max-minus-min luma over
// a burst of frames at a fixed camera — shows a hard diagonal seam along their
// shared edge, one triangle's half of the ground visibly boiling and the other
// clean. Subdividing removes it: the seam is there at 1x1 and gone at 8x8 and
// 32x32 with nothing else changed.
//
// It really is the triangulation: retriangulating the same quad along its other
// diagonal — same vertices, same UVs, same camera, only the index buffer
// changed — moves the seam onto that diagonal instead. And it is TAA-specific;
// with `feature_taa_enabled = false` a single frame has no spatial seam at all.
// Not SSAO, not shadows, not sharpening, not the sun angle, and the velocity
// buffer is uniformly zero under a static camera.
//
// The mechanism is still not pinned down, but it is not the g-buffer maths,
// which was the obvious suspect. Shade shades from a visibility buffer, so that
// pass reconstructs barycentrics and UV gradients analytically per triangle
// (`barycentric_full`, anchored on `pixel - ndc0`) — and that reconstruction is
// far better conditioned than it looks. Simulated in f32 against an f64
// reference, the two triangles of this quad disagree at the same pixel by
// 5e-4 texels of UV and 2e-4 log2 of mip; the mip swing across a TAA jitter
// sequence is identical for both and for a subdivided cell; the anisotropic
// sample count is stable at 2 for both. It also holds up with a vertex swept to
// within w = 0.004 of the eye plane, where |ndc| is ~1e4 — the 1/w blow-up
// cancels between the anchor and the gradients. Vertex UVs are full-width f32,
// so it is not attribute precision either. What is left is what the rasterizer
// produces per triangle — coverage and depth, each triangle's own plane
// equation extrapolated from vertices far outside the view — feeding TAA's
// depth-based history rejection.
//
// Whatever the cause, it is per triangle and it goes when the triangles are
// kept near the scale of what the camera sees — 425/32 ≈ 13 units a cell,
// against the ~72 units of ground actually in view.
const GROUND_SEGMENTS = 32;
const groundGeometry = make_plane_geometry(
GROUND_SIZE,
GROUND_SIZE,
GROUND_SEGMENTS,
GROUND_SEGMENTS,
);
// The plane's UVs run 0..1; scaling them here is what `texture.repeat` used to do.
const groundUv = groundGeometry.getAttribute(StandardAttributes.TextureCoordinates0);
for (let i = 0; i < groundUv.data.length; i++) {
groundUv.data[i] *= GROUND_TILES;
}
const groundMaterial = new StandardShadeMaterial();
groundMaterial.texture_albedo = await makeGroundTexture();
groundMaterial.roughness_factor = 0.95;
groundMaterial.metallic_factor = 0;
const groundTransform = new Transform64();
// Rx(-90°): the plane's +Z normal becomes +Y. A Transform64 does not maintain
// its matrix, so compose it once after the rotation is written.
groundTransform.setRotation(-Math.SQRT1_2, 0, 0, Math.SQRT1_2);
groundTransform.updateMatrix();
new Entity()
.add(groundTransform)
.add(ShadedGeometry.from(
// Clustering is a once-per-shape precompute; the WASM clusterizer behind
// it is only ready after the engine has bootstrapped, which is why this
// is here and not at module scope.
meshlet_geometry_build_from_geometry(groundGeometry),
groundMaterial,
))
.build(ecd);
// ─── Scatter props ──────────────────────────────────────────────────────────
//
// `Pickable` is what opts an entity into picking. It is per entity on purpose:
// the system keeps a set of opted-in entities and resolves the GPU's drawn row
// to the nearest pickable ancestor. It keeps no CPU-side bounds or index. The
// ground has no `Pickable`, which is what replaces the old raycast filter that
// had to exclude it by hand.
for (let i = 0; i < PROP_COUNT; i++) {
const url = PROP_URLS[(Math.random() * PROP_URLS.length) | 0];
const halfAngle = Math.random() * Math.PI;
new Entity()
.add(Transform64.fromJSON({
translation: {
x: (Math.random() - 0.5) * (GROUND_SIZE - 4),
y: 0,
z: (Math.random() - 0.5) * (GROUND_SIZE - 4),
},
rotation: {
x: 0,
y: Math.sin(halfAngle),
z: 0,
w: Math.cos(halfAngle),
},
}))
.add(SGMesh.fromURL(url))
.add(new Pickable())
.build(ecd);
}
// ─── Get the camera entity and its controller ───────────────────────────────
// getAnyComponent returns a { component, entity } record — unwrap.
const cameraEntry = ecd.getAnyComponent(Camera);
const camera = cameraEntry.component;
const cameraEntity = cameraEntry.entity;
const controller = ecd.getComponent(cameraEntity, TopDownCameraController);
function clampToGround(v) {
if (v < -HALF_GROUND) return -HALF_GROUND;
if (v > HALF_GROUND) return HALF_GROUND;
return v;
}
// All panning — keyboard or mouse — goes through TopDownCameraController.pan,
// which converts a screen-space pixel delta into a target offset in world
// coordinates using the controller's own pitch/yaw/distance. We never have to
// think about the camera's rotation ourselves.
//
// `pan` expects: a Vector2-shaped delta (right & down positive), the controller
// whose `target` is moved, the viewport height in pixels (a perspective camera
// is fitted to height alone, so width plays no part), the vertical FOV in
// degrees, and a per-axis multiplier on the world displacement — zeroing Y is
// what keeps the target on the ground.
const panDelta = new Vector2();
const PAN_ON_GROUND = new Vector3(1, 0, 1);
function applyPan(pixelDx, pixelDy) {
panDelta.set(pixelDx, pixelDy);
TopDownCameraController.pan(
panDelta,
controller,
engine.graphics.viewport.size.y,
camera.fov.getValue(),
PAN_ON_GROUND,
);
controller.target.set(
clampToGround(controller.target.x),
0,
clampToGround(controller.target.z),
);
}
// ─── Keyboard panning ───────────────────────────────────────────────────────
//
// `engine.devices.keyboard.keys.{name}.is_down` is the engine's polled state
// for each key; the engine handles focus loss, repeat suppression, and
// everything else. We just check the relevant keys each engine tick.
const KEYBOARD_PIXELS_PER_SEC = 600;
function applyKeyboard(dt) {
const k = engine.devices.keyboard.keys;
let sx = 0, sy = 0;
if (k.a.is_down || k.left_arrow.is_down) sx -= 1;
if (k.d.is_down || k.right_arrow.is_down) sx += 1;
if (k.w.is_down || k.up_arrow.is_down) sy -= 1;
if (k.s.is_down || k.down_arrow.is_down) sy += 1;
if (sx === 0 && sy === 0) return;
const len = Math.hypot(sx, sy);
const step = KEYBOARD_PIXELS_PER_SEC * dt / len;
applyPan(sx * step, sy * step);
}
// ─── Mouse drag on the main viewport ────────────────────────────────────────
//
// The engine's PointerDevice is bound to `viewStack.el`; HUD panels are
// siblings outside that scope, so we don't have to filter them out.
// `on.drag` fires with (position, origin, lastPosition, event); we just take
// the per-step delta and feed it to pan().
engine.devices.pointer.on.drag.add((position, _origin, lastPosition) => {
const dx = position.x - lastPosition.x;
const dy = position.y - lastPosition.y;
// Negate so the world drags with the cursor (map-drag style).
applyPan(-dx, -dy);
});
// ─── Click to select ────────────────────────────────────────────────────────
//
// PointerDevice's `tap` signal fires only on a quick press-release that
// doesn't drift more than ~10px — drags don't trigger it, so this is a clean
// signal for "the user actually clicked here".
//
// The pick itself goes through PickingSystem, and it is **asynchronous and
// batched by design**: the answer is a promise for an array of results, one per
// query, in the order the queries were given. A pick reads the GPU's visibility
// buffer — the `meshes` row the rasterizer wrote at that pixel, and the depth it
// wrote with it — so the answer is a frame or two late and arrives for the whole
// batch at once. What was drawn is what is picked, at triangle precision, under
// skinning, with nothing kept on the CPU for it.
//
// A query names a **point of the view**, not a ray: `(0, 0)` is the top-left
// corner and `(1, 1)` the bottom-right, so a caller holding a pointer position in
// viewport pixels needs to know nothing about the resolution the frame was drawn
// at.
//
// The pixel names the primitive's own entity — one of the children MeshSystem
// spawned — and the system walks up ParentEntity to the nearest ancestor that
// opted in, which is the prop carrying SGMesh and Pickable. Highlight goes
// straight onto what comes back, which is also what HighlightSystem wants: it
// outlines entities with both Highlight and SGMesh.
const picking = engine.entityManager.getSystem(PickingSystem);
// Register the Highlight component type once so add/remove via the dataset works.
ecd.registerComponentType(Highlight);
const pickNdc = new Vector2();
let selectedEntity = -1;
// A tap that lands while an earlier pick is still in flight has to win; the
// stale answer is dropped rather than allowed to overwrite it.
let pickGeneration = 0;
function setSelection(entity) {
if (entity === selectedEntity) return;
if (selectedEntity !== -1 && ecd.entityExists(selectedEntity)) {
ecd.removeComponentFromEntity(selectedEntity, Highlight);
}
selectedEntity = entity;
const plaque = document.getElementById("selection-plaque");
if (selectedEntity === -1) {
plaque.hidden = true;
} else {
ecd.addComponentToEntity(selectedEntity, Highlight.fromOne(0.31, 0.94, 0.66, 1));
plaque.hidden = false;
document.getElementById("selection-id").textContent = "#" + selectedEntity;
}
}
engine.devices.pointer.on.tap.add(async (position) => {
// Pointer position (viewport pixels) → clip space → the view fraction a
// PickingQuery names. normalizeViewportPoint answers in clip space (−1…1,
// +Y up); the query wants 0…1 with v measured downwards from the top edge.
engine.graphics.normalizeViewportPoint(position, pickNdc);
const generation = ++pickGeneration;
const [result] = await picking.pick([
PickingQuery.from_viewport((pickNdc.x + 1) / 2, (1 - pickNdc.y) / 2),
]);
if (generation !== pickGeneration) {
// a later tap has already been asked; this answer is stale
return;
}
// A miss still produces a result — `found` is how you ask, rather than
// comparing against a bare -1.
setSelection(result.found ? result.entity : -1);
});
// ─── Minimap (world-axis-aligned satellite view) ────────────────────────────
const map = document.getElementById("minimap");
const marker = document.getElementById("minimap-marker");
const frustumPoly = document.getElementById("minimap-frustum");
function mapRect() {
return map.getBoundingClientRect();
}
function mapPxToWorld(px, py) {
const rect = mapRect();
return {
x: (px / rect.width - 0.5) * GROUND_SIZE,
z: (py / rect.height - 0.5) * GROUND_SIZE,
};
}
function worldToMapPx(worldX, worldZ) {
const rect = mapRect();
return {
x: (worldX / GROUND_SIZE + 0.5) * rect.width,
y: (worldZ / GROUND_SIZE + 0.5) * rect.height,
};
}
let mapDragging = false;
function minimapApplyAt(clientX, clientY) {
const rect = mapRect();
const px = Math.max(0, Math.min(rect.width, clientX - rect.left));
const py = Math.max(0, Math.min(rect.height, clientY - rect.top));
const w = mapPxToWorld(px, py);
controller.target.set(clampToGround(w.x), 0, clampToGround(w.z));
}
map.addEventListener("pointerdown", (e) => {
e.stopPropagation();
mapDragging = true;
map.setPointerCapture(e.pointerId);
minimapApplyAt(e.clientX, e.clientY);
});
map.addEventListener("pointermove", (e) => {
if (!mapDragging) return;
minimapApplyAt(e.clientX, e.clientY);
});
map.addEventListener("pointerup", (e) => {
mapDragging = false;
map.releasePointerCapture(e.pointerId);
});
// Refresh the minimap each engine frame — handles all input sources uniformly.
// Frustum projection uses meep's own facilities: the graphics facade's
// viewportProjectionRay turns a clip-space coord into a world-space ray, and
// plane3_compute_ray_intersection does the ground-plane intersection.
const NDC_CORNERS = [[-1, -1], [1, -1], [1, 1], [-1, 1]];
const rayOrigin = new Vector3();
const rayDirection = new Vector3();
const rayHit = new Vector3();
function refreshMinimap() {
const p = worldToMapPx(controller.target.x, controller.target.z);
marker.style.left = `${p.x}px`;
marker.style.top = `${p.y}px`;
const points = [];
for (const [nx, ny] of NDC_CORNERS) {
engine.graphics.viewportProjectionRay(nx, ny, rayOrigin, rayDirection);
plane3_compute_ray_intersection(
rayHit,
rayOrigin.x, rayOrigin.y, rayOrigin.z,
rayDirection.x, rayDirection.y, rayDirection.z,
0, 1, 0, 0, // ground plane: normal +Y, offset 0
);
const mp = worldToMapPx(rayHit.x, rayHit.z);
points.push(`${mp.x.toFixed(1)},${mp.y.toFixed(1)}`);
}
frustumPoly.setAttribute("points", points.join(" "));
}
// ─── HUD + frame loop ───────────────────────────────────────────────────────
const fpsEl = document.getElementById("fps");
const countEl = document.getElementById("count");
let fpsWindow = 0;
let fpsFrames = 0;
let lastFrame = performance.now();
engine.graphics.on.postRender.add(() => {
const now = performance.now();
const dt = (now - lastFrame) / 1000;
lastFrame = now;
applyKeyboard(dt);
refreshMinimap();
fpsWindow += dt;
fpsFrames++;
if (fpsWindow >= 0.5) {
fpsEl.textContent = (fpsFrames / fpsWindow).toFixed(0);
countEl.textContent = ecd.entityCount.toLocaleString();
fpsWindow = 0;
fpsFrames = 0;
}
});
refreshMinimap();