@selvajs/visualization 1.6.0-beta.1 → 1.6.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/gpu-dispose-BKp7iijv.js +355 -0
  2. package/dist/gpu-dispose-BKp7iijv.js.map +1 -0
  3. package/dist/gpu-dispose-Cxu3dFmA.cjs +469 -0
  4. package/dist/gpu-dispose-Cxu3dFmA.cjs.map +1 -0
  5. package/dist/gpu-ownership-ADRZBxxs.cjs +102 -0
  6. package/dist/gpu-ownership-ADRZBxxs.cjs.map +1 -0
  7. package/dist/gpu-ownership-Bs9l7hrU.js +40 -0
  8. package/dist/gpu-ownership-Bs9l7hrU.js.map +1 -0
  9. package/dist/index.js +1 -1
  10. package/dist/merged-draw-CO4rr2Co.js +129 -0
  11. package/dist/merged-draw-CO4rr2Co.js.map +1 -0
  12. package/dist/merged-draw-ma_2r4N8.cjs +171 -0
  13. package/dist/merged-draw-ma_2r4N8.cjs.map +1 -0
  14. package/dist/parse.cjs +2789 -11
  15. package/dist/parse.cjs.map +1 -1
  16. package/dist/parse.d.cts +15 -18
  17. package/dist/parse.d.ts +15 -18
  18. package/dist/parse.js +2780 -11
  19. package/dist/parse.js.map +1 -1
  20. package/dist/render.cjs +4302 -6
  21. package/dist/render.cjs.map +1 -1
  22. package/dist/render.d.cts +62 -43
  23. package/dist/render.d.ts +62 -43
  24. package/dist/render.js +4281 -6
  25. package/dist/render.js.map +1 -1
  26. package/dist/scene.cjs +343 -1
  27. package/dist/scene.cjs.map +1 -1
  28. package/dist/scene.d.cts +0 -12
  29. package/dist/scene.d.ts +0 -12
  30. package/dist/scene.js +336 -1
  31. package/dist/scene.js.map +1 -1
  32. package/dist/{types-DjPelPRM.d.ts → types-DW_qU4-Y.d.cts} +22 -5
  33. package/dist/{types-DjPelPRM.d.cts → types-DW_qU4-Y.d.ts} +22 -5
  34. package/package.json +18 -11
  35. package/dist/gpu-dispose-Dich0oW7.js +0 -2
  36. package/dist/gpu-dispose-Dich0oW7.js.map +0 -1
  37. package/dist/gpu-dispose-GZmNW-gK.cjs +0 -2
  38. package/dist/gpu-dispose-GZmNW-gK.cjs.map +0 -1
  39. package/dist/merged-draw-B75jHIzV.cjs +0 -2
  40. package/dist/merged-draw-B75jHIzV.cjs.map +0 -1
  41. package/dist/merged-draw-BPhBYH-v.js +0 -2
  42. package/dist/merged-draw-BPhBYH-v.js.map +0 -1
  43. package/dist/rolldown-runtime-BocRIvOZ.cjs +0 -1
package/dist/parse.cjs CHANGED
@@ -1,5 +1,1835 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./rolldown-runtime-BocRIvOZ.cjs"),t=require("./gpu-dispose-GZmNW-gK.cjs");let n=require("three");n=e.t(n,1);let r=require("three/addons/lines/Line2.js"),i=require("three/addons/lines/LineGeometry.js"),a=require("three/addons/lines/LineMaterial.js"),o=require("fflate");function s(){let e=globalThis.Buffer;return typeof e==`function`?e:void 0}function c(e){let t=e.replace(/[\t\n\f\r ]/g,``);return t.length%4==0&&(t=t.replace(/={1,2}$/,``)),t.length%4==1?null:t}function l(e){let n=c(e),r=n===null?null:u(n);if(r===null)throw new t.g(`Invalid base64 input.`,t.h.ENCODING_ERROR,{context:{inputLength:e.length}});return r}function u(e){let n=s();if(n){if(n.byteLength(e,`utf8`)!==e.length||e.includes(`-`)||e.includes(`_`))return null;let t=n.from(e,`base64`);return t.length===Math.floor(e.length*3/4)?t.byteOffset===0&&t.buffer.byteLength===t.length?new Uint8Array(t.buffer,0,t.length):new Uint8Array(t):null}if(typeof globalThis.atob==`function`){let t;try{t=globalThis.atob(e)}catch{return null}let n=new Uint8Array(t.length);for(let e=0;e<t.length;e++)n[e]=t.charCodeAt(e)&255;return n}throw new t.g(`Base64 decoding not supported in this environment.`,t.h.INVALID_STATE,{context:{environmentInfo:`atob or Buffer not available`}})}function d(e){return e.map(e=>{let t=e.clone(!0),n=[];e.traverse(e=>n.push(e));let r=0;return t.traverse(e=>{let t=n[r++],i=e;t.geometry&&(i.geometry=t.geometry.clone())}),t})}function f(e){e.forEach(e=>t.t(e,{materials:!1}))}const p={clone:d,release:f};function m(e,t){let r=t??1;return{color:new n.Color(e??`#ffffff`),transparent:r<1,opacity:r}}function h(e){let t=g(e);if(!t)return null;let n=new i.LineGeometry;n.setPositions(t);let o=m(e.color,e.opacity),s=new a.LineMaterial({color:o.color}),c=s;c.linewidth=e.width??2,c.transparent=o.transparent,c.opacity=o.opacity;let l=new r.Line2(n,s);return l.computeLineDistances(),l.name=e.name,l.userData={source:`compute`,id:e.id,layer:e.layer,kind:`curve`,metadata:e.metadata},l}function g(e){if(!e.points)throw new t.g(`Curve display item '${e.id}' has no tessellated points. It was produced by an outdated Display component — upgrade it in Grasshopper (Solution → Upgrade obsolete components) and re-save the definition.`,t.h.INVALID_CONFIG,{context:{itemId:e.id,kind:e.kind}});return e.points.length>=6?e.points:null}function _(e){let{position:r}=e;if(!r||typeof r.X!=`number`||!Number.isFinite(r.X)||typeof r.Y!=`number`||!Number.isFinite(r.Y)||typeof r.Z!=`number`||!Number.isFinite(r.Z))return t.p().warn(`Skipping point display item with missing or non-finite position (id: ${String(e.id)}).`),null;let i=new n.BufferGeometry;i.setAttribute(`position`,new n.Float32BufferAttribute([r.X,r.Y,r.Z],3));let a=new n.PointsMaterial({...m(e.color,e.opacity),size:6,sizeAttenuation:!1}),o=new n.Points(i,a);return o.name=e.name,o.userData={source:`compute`,id:e.id,layer:e.layer,kind:`point`,metadata:e.metadata},o}function v(e){if(!e||e.length===0)return[];let n=[];for(let r of e)switch(r.kind){case`curve`:{let e=h(r);e&&n.push(e);break}case`point`:{let e=_(r);e&&n.push(e);break}default:{let e=r;t.p().warn(`Skipping unknown display item kind: ${String(e.kind)}`);break}}return n}const y=new Uint16Array(new Uint8Array([1,0]).buffer)[0]===1;function b(e){return typeof e==`string`?l(e):e instanceof Uint8Array?e:new Uint8Array(e)}function x(e){if(e.byteLength<8)return e;let t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(t.getUint32(0,!0)!==1515605075)return e;let n=t.getUint32(4,!0),r=e.subarray(8),i=Math.max(r.byteLength*1032+1024,1<<20);if(n>i)throw F(`SLVZ header declares an implausible uncompressed length`,{uncompressedLen:n,deflatedBytes:r.byteLength,maxPlausibleLen:i});let a;try{a=(0,o.inflateSync)(r,{out:new Uint8Array(n+1)})}catch(e){throw F(`Failed to inflate SLVZ blob: ${e instanceof Error?e.message:String(e)}`,{uncompressedLen:n,deflatedBytes:r.byteLength})}if(a.byteLength!==n)throw F(`SLVZ payload inflated to a different size than the header declares.`,{declaredLen:n,actualLen:a.byteLength,deflatedBytes:r.byteLength});return a}function S(e){if(typeof TextDecoder<`u`)return new TextDecoder(`utf-8`).decode(e);if(globalThis.Buffer!==void 0)return globalThis.Buffer.from(e).toString(`utf-8`);throw new t.g(`No UTF-8 decoder available in this environment.`,t.h.INVALID_STATE)}function C(e,t,n){if(n===0)return new Int16Array;if(t%2==0)return new Int16Array(e,t,n);let r=new Uint8Array(n*2);return r.set(new Uint8Array(e,t,n*2)),new Int16Array(r.buffer)}function w(e,t,n){if(n===0)return new Float32Array;if(t%4==0)return new Float32Array(e,t,n);let r=new Uint8Array(n*4);return r.set(new Uint8Array(e,t,n*4)),new Float32Array(r.buffer)}function T(e,t,n){if(n===0)return new Uint16Array;if(t%2==0)return new Uint16Array(e,t,n);let r=new Uint8Array(n*2);return r.set(new Uint8Array(e,t,n*2)),new Uint16Array(r.buffer)}function E(e,t,n){if(n===0)return new Uint32Array;if(t%4==0)return new Uint32Array(e,t,n);let r=new Uint8Array(n*4);return r.set(new Uint8Array(e,t,n*4)),new Uint32Array(r.buffer)}function D(e,t){if(e.length!==0&&!(e instanceof Uint16Array&&t>65535)){for(let n=0;n<e.length;n++)if(e[n]>=t)throw F(`Index out of range of vertexCount.`,{indexPosition:n,indexValue:e[n],vertexCount:t})}}function O(e){return e>>>1^-(e&1)}function k(e){let t=new Int16Array(e.length),n=0,r=0,i=0;for(let a=0;a<e.length;a+=3)n=n+O(e[a])<<16>>16,r=r+O(e[a+1])<<16>>16,i=i+O(e[a+2])<<16>>16,t[a]=n,t[a+1]=r,t[a+2]=i;return t}function A(e,t){let n=new Int16Array(t*3),r=t,i=0,a=0,o=0;for(let t=0;t<r;t++)i=i+O(e[t]|e[r*3+t]<<8)<<16>>16,a=a+O(e[r+t]|e[r*4+t]<<8)<<16>>16,o=o+O(e[r*2+t]|e[r*5+t]<<8)<<16>>16,n[t*3]=i,n[t*3+1]=a,n[t*3+2]=o;return n}function j(e,t){let n=new Uint16Array(t),r=0;for(let i=0;i<t;i++)r=r+O(e[i]|e[t+i]<<8)&65535,n[i]=r;return n}function M(e,t){let n=new Uint32Array(t),r=0;for(let i=0;i<t;i++){let a=(e[i]|e[t+i]<<8|e[t*2+i]<<16|e[t*3+i]<<24)>>>0;r=r+O(a)>>>0,n[i]=r}return n}function N(e){let t=new Uint16Array(e.length),n=0;for(let r=0;r<e.length;r++)n=n+O(e[r])&65535,t[r]=n;return t}function P(e){let t=new Uint32Array(e.length),n=0;for(let r=0;r<e.length;r++)n=n+O(e[r])>>>0,t[r]=n;return t}function F(e,n){return new t.g(e,t.h.VALIDATION_ERROR,{context:n})}function ee(e,t,n,r,i,a=!1){if(n+36>e.byteLength)throw F(`Insufficient data to read UV chunk header.`,{expectedBytes:36,availableBytes:e.byteLength-n,offset:n});let o=t.getUint32(n,!0);n+=4;let s=t.getFloat64(n,!0);n+=8;let c=t.getFloat64(n,!0);n+=8;let l=t.getFloat64(n,!0);n+=8;let u=t.getFloat64(n,!0);n+=8;let d=r*2,f=o===1,p=d*(f?4:2);if(n+p>e.byteLength)throw F(`Insufficient data to read UV chunk.`,{expectedBytes:p,availableBytes:e.byteLength-n,offset:n,uvFormat:o,vertexCount:r});let m=e.byteOffset+n,h;if(f)h=w(e.buffer,m,d).slice();else if(a){let t=e.subarray(n,n+p),i=r;h=new Float32Array(d);let a=0,o=0;for(let e=0;e<i;e++)a=a+O(t[e]|t[i*2+e]<<8)&65535,o=o+O(t[i+e]|t[i*3+e]<<8)&65535,h[e*2]=s+a*l,h[e*2+1]=c+o*u}else{let t=T(e.buffer,m,d);h=new Float32Array(d);let n=0,r=0;for(let e=0;e<d;e+=2)i?(n=n+O(t[e])&65535,r=r+O(t[e+1])&65535):(n=t[e],r=t[e+1]),h[e]=s+n*l,h[e+1]=c+r*u}return{uvs:h,offset:n+p}}function te(e,t,n,r){let i=n*3;if(t+i>e.byteLength)throw F(`Insufficient data to read vertex-color chunk.`,{expectedBytes:i,availableBytes:e.byteLength-t,offset:t,vertexCount:n});let a=e.subarray(t,t+i);if(!r)return a.slice();let o=new Uint8Array(i),s=0,c=0,l=0;for(let e=0;e<i;e+=3)s=s+O(a[e])&255,c=c+O(a[e+1])&255,l=l+O(a[e+2])&255,o[e]=s,o[e+1]=c,o[e+2]=l;return o}function ne(e){return e.byteLength>=4&&new DataView(e.buffer,e.byteOffset,4).getUint32(0,!0)===1297501267}function re(e){let t=I(e),n=null,r=null,i=null,a=[];for(let{type:e,payload:o}of t)switch(e){case 1297040711:n=o;break;case 1279410516:r=x(o);break;case 1280590157:i=S(o);break;case 1381516628:a.push(o)}if(n===null)throw F(`SLVM container has no GEOM chunk.`,{chunkCount:t.length});if(r===null)throw F(`SLVM container has no TABL chunk.`,{chunkCount:t.length});let{groups:o}=z(r),s=B(i,a);return{geometryBlob:n,metadata:{materials:s,groups:o}}}function I(e){let t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(e.byteLength<12||t.getUint32(0,!0)!==1297501267)throw F(`Not an SLVM container (bad magic).`,{byteLength:e.byteLength});let n=t.getUint32(4,!0);if(n!==3)throw F(`Unsupported SLVM version: ${n}`,{expectedVersion:3});let r=t.getUint32(8,!0),i=[],a=12;for(let n=0;n<r;n++){if(a+8>e.byteLength)throw F(`Truncated SLVM chunk header.`,{offset:a,chunkIndex:n});let r=t.getUint32(a,!0),o=t.getUint32(a+4,!0);if(a+=8,a+o>e.byteLength)throw F(`Truncated SLVM chunk payload.`,{offset:a,chunkIndex:n,byteLen:o});i.push({type:r,payload:e.subarray(a,a+o)}),a+=o+(4-o%4)%4}return i}function L(e){let t=0,n=0;for(;;){if(e.pos>=e.bytes.byteLength||n>28)throw F(`Malformed varint in SLVM table.`,{pos:e.pos});let r=e.bytes[e.pos++];if(t|=(r&127)<<n,!(r&128))return t>>>0;n+=7}}function R(e,t,n){let r=e.bytes[e.pos++],i=Array(t);switch(r){case 0:i.fill(``);break;case 1:for(let e=0;e<t;e++)i[e]=String(e+1);break;case 2:for(let r=0;r<t;r++)i[r]=n[L(e)]??``;break;default:throw F(`Unknown SLVM string column mode: ${r}`,{pos:e.pos})}return i}function z(e){let t={bytes:e,pos:0},n=L(t),r=L(t),i=L(t),a=n+r+i,o=L(t),s=Array(o);for(let n=0;n<o;n++){let r=L(t);s[n]=S(e.subarray(t.pos,t.pos+r)),t.pos+=r}let c=Array(n),l=Array(n);for(let e=0;e<n;e++)c[e]=L(t),l[e]=L(t);for(let e=0;e<r;e++)L(t);let u=L(t),d=[];for(let e=0;e<u;e++)d.push({materialId:L(t),meshCount:L(t)});let f=R(t,a,s),p=R(t,a,s),m=Array(a),h=L(t);for(let e=0;e<h;e++){let e=s[L(t)]??``,n=L(t),r=Array(n),i=0;for(let e=0;e<n;e++)i+=L(t),r[e]=i;for(let i=0;i<n;i++){let n=s[L(t)]??``,a=r[i];(m[a]??={})[e]=n}}let g=d.length>0||n===0?d:[{materialId:0,meshCount:n}],_=[],v=0,y=0,b=0;for(let e of g){let t=[];for(let r=0;r<e.meshCount&&v<n;r++,v++){let e=m[v],n=e?.id;e!==void 0&&delete e.id,t.push({id:n,name:f[v],layer:p[v],vertexCount:c[v],indexCount:l[v]*3,vertexStart:y,indexStart:b,metadata:e??{}}),y+=c[v],b+=l[v]*3}_.push({materialId:e.materialId,meshes:t})}return{groups:_}}function B(e,t){if(e===null)return[];let n;try{n=JSON.parse(e).materials??[]}catch(t){throw F(`Failed to parse SLVM materials JSON: ${t instanceof Error?t.message:String(t)}`,{byteLength:e.length})}for(let e of n)for(let n of ie){let r=e[n];if(r?.startsWith(`slvm:tex:`)){let i=t[Number(r.slice(9))];i!==void 0&&(e[n]=ae(i))}}return n}const ie=[`map`,`roughnessMap`,`normalMap`];function ae(e){let t={bytes:e,pos:0},n=L(t),r=S(e.subarray(t.pos,t.pos+n)),i=e.subarray(t.pos+n),a=``,o=32768;for(let e=0;e<i.byteLength;e+=o)a+=String.fromCharCode(...i.subarray(e,e+o));return`data:${r};base64,${btoa(a)}`}function oe(e){let t=V(e),n;n=t.isFloat32?t.vertexData:t.planarByteSplit?A(t.vertexData,t.vertexCount):t.deltaEncoded?k(t.vertexData):t.vertexData;let r;if(t.planarByteSplit){let e=t.indexData;r=t.uint16Indices?j(e,e.length/2):M(e,e.length/4)}else r=t.deltaEncoded?t.indexData instanceof Uint16Array?N(t.indexData):P(t.indexData):t.indexData;return D(r,t.vertexCount),{metadata:t.metadata,flags:t.flags,vertices:n,indices:r,origin:t.origin,scale:t.scale,uvs:t.uvs,colors:t.colors}}function V(e){if(!y)throw new t.g(`SLVA parsing requires a little-endian host: the zero-copy geometry readers view the wire bytes in host byte order.`,t.h.ENVIRONMENT_ERROR);let n=b(e);if(ne(n)){let e=re(n),t=V(e.geometryBlob);return t.metadata=e.metadata,t}let r=x(n),i=new DataView(r.buffer,r.byteOffset,r.byteLength);if(r.byteLength<12)throw F(`Blob too small to contain SLVA header.`,{expectedBytes:12,availableBytes:r.byteLength});let a=0,o=i.getUint32(a,!0);if(a+=4,o!==1096174675)throw F(`Invalid SLVA magic: 0x${o.toString(16)}`,{expectedMagic:`0x41564c53`,actualMagic:`0x${o.toString(16)}`});let s=i.getUint32(a,!0);if(a+=4,s<1||s>4)throw F(`Unsupported SLVA version: ${s}`,{minSupportedVersion:1,maxSupportedVersion:4,actualVersion:s});let c=i.getUint32(a,!0);if(a+=4,a+c>r.byteLength)throw F(`Insufficient data to read metadata JSON.`,{expectedBytes:c,availableBytes:r.byteLength-a,offset:a});let l=r.subarray(a,a+c);a+=c;let u;try{u=c===0?{}:JSON.parse(S(l))}catch(e){throw F(`Failed to parse metadata JSON: ${e instanceof Error?e.message:String(e)}`,{metadataLen:c})}if(a+56>r.byteLength)throw F(`Insufficient data to read geometry header.`,{expectedBytes:56,availableBytes:r.byteLength-a,offset:a});let d=i.getUint32(a,!0);a+=4;let f=i.getFloat64(a,!0);a+=8;let p=i.getFloat64(a,!0);a+=8;let m=i.getFloat64(a,!0);a+=8;let h=i.getFloat64(a,!0);a+=8;let g=i.getFloat64(a,!0);a+=8;let _=i.getFloat64(a,!0);a+=8;let v=i.getUint32(a,!0);a+=4;let D=!!(d&1),O=!!(d&4),k=!!(d&32),A=v*3,j=A*(D?4:2);if(a+j>r.byteLength)throw F(`Insufficient data to read vertices.`,{expectedBytes:j,availableBytes:r.byteLength-a,offset:a,useFloat32:D,vertexCount:v});let M=r.byteOffset+a,N;if(N=D?w(r.buffer,M,A):k?r.subarray(a,a+j):O?T(r.buffer,M,A):C(r.buffer,M,A),a+=j,a+4>r.byteLength)throw F(`Insufficient data to read index count.`,{expectedBytes:4,availableBytes:r.byteLength-a,offset:a});let P=i.getUint32(a,!0);a+=4;let I=!!(d&2),L=P*(I?2:4);if(a+L>r.byteLength)throw F(`Insufficient data to read indices.`,{expectedBytes:L,availableBytes:r.byteLength-a,offset:a,indexCount:P,useUint16Indices:I});let R=k?r.subarray(a,a+L):I?T(r.buffer,r.byteOffset+a,P):E(r.buffer,r.byteOffset+a,P);a+=L;let z=null;if(d&8){let e=ee(r,i,a,v,O,k);z=e.uvs,a=e.offset}let B=null;return d&16&&(B=te(r,a,v,O)),{metadata:u,flags:d,vertexData:N,indexData:R,isFloat32:D,deltaEncoded:O,planarByteSplit:k,uint16Indices:I,vertexCount:v,origin:[f,p,m],scale:[h,g,_],uvs:z,colors:B}}function se(e,t,n){if(n.length===0)return!1;let r=e=>{let t=16;for(;t<e*2;)t*=2;return t},i=(n,i)=>{if(i<12||i%3!=0)return!1;let a=0;for(let r=0;r<i;r++){let i=t[n+r]*3;a=Math.max(a,Math.abs(e[i]),Math.abs(e[i+1]),Math.abs(e[i+2]))}if(!(a>0)||!Number.isFinite(a))return!1;let o=1/(2*2**(Math.floor(Math.log2(a))-23)),s=r(i),c=s-1,l=new Int32Array(s).fill(-1),u=new Int32Array(s),d=new Int32Array(s),f=new Int32Array(s),p=(e,t,n)=>{let r=Math.imul(e,2654435761)^Math.imul(t,2246822519)^Math.imul(n,3266489917);for(r=(r^r>>>15)&c;l[r]!==-1&&(u[r]!==e||d[r]!==t||f[r]!==n);)r=r+1&c;return r},m=(e,t)=>{let n=e-t;return n>.25?t+1:n<-.25?t-1:t},h=new Int32Array(i);for(let r=0;r<i;r++){let i=t[n+r],a=e[i*3]*o,s=e[i*3+1]*o,c=e[i*3+2]*o,g=Math.round(a),_=Math.round(s),v=Math.round(c),y=m(a,g),b=m(s,_),x=m(c,v),S=-1;for(let e=0;e<8&&S===-1;e++){if(e&1&&y===g||e&2&&b===_||e&4&&x===v)continue;let t=p(e&1?y:g,e&2?b:_,e&4?x:v);l[t]!==-1&&(S=l[t])}if(S===-1){let e=p(g,_,v);l[e]=i,u[e]=g,d[e]=_,f[e]=v,S=i}h[r]=S}let g=r(i),_=g-1,v=new Int32Array(g).fill(-1),y=new Int32Array(g),b=(e,t)=>{let n=(Math.imul(e,2654435761)^Math.imul(t,2246822519))&_;for(;v[n]!==-1&&(v[n]!==e||y[n]!==t);)n=n+1&_;return n},x=(e,t)=>{let n=b(e,t);return v[n]===-1&&(v[n]=e,y[n]=t,!0)},S=(e,t)=>v[b(e,t)]!==-1,C=0;for(let e=0;e<i;e+=3){let t=h[e],n=h[e+1],r=h[e+2];if(t!==n&&n!==r&&r!==t){if(!x(t,n)||!x(n,r)||!x(r,t))return!1;C++}}if(C<4)return!1;for(let e=0;e<i;e+=3){let t=h[e],n=h[e+1],r=h[e+2];if(t!==n&&n!==r&&r!==t&&(!S(n,t)||!S(r,n)||!S(t,r)))return!1}let w=h[0]*3,T=e[w],E=e[w+1],D=e[w+2],O=0;for(let t=0;t<i;t+=3){let n=h[t]*3,r=h[t+1]*3,i=h[t+2]*3,a=e[n]-T,o=e[n+1]-E,s=e[n+2]-D,c=e[r]-T,l=e[r+1]-E,u=e[r+2]-D,d=e[i]-T,f=e[i+1]-E,p=e[i+2]-D;O+=a*(l*p-u*f)+o*(u*d-c*p)+s*(c*f-l*d)}return O>0},a=0;for(let e of n){if(!i(a,e))return!1;a+=e}return!0}function ce(e,t,n,r){let i=Math.cos(30*Math.PI/180),a=e.length/3,o=t.length,s=o/3;{let i=new Uint32Array(e.buffer,e.byteOffset,e.length),s=n?new Uint32Array(n.buffer,n.byteOffset,n.length):null,c=(e,t)=>e[t]===2147483648?0:e[t],l=(e,t)=>{for(let n=0;n<3;n++)if(c(i,e*3+n)!==c(i,t*3+n)||r&&r[e*3+n]!==r[t*3+n])return!1;return!s||s[e*2]===s[t*2]&&s[e*2+1]===s[t*2+1]},u=16;for(;u<a*2;)u*=2;let d=u-1,f=new Int32Array(u).fill(-1),p=new Uint32Array(a);for(let e=0;e<a;e++){let t=Math.imul(c(i,e*3),2654435761)^Math.imul(c(i,e*3+1),2246822519)^Math.imul(c(i,e*3+2),3266489917);for(t=(t^t>>>15)&d;f[t]!==-1&&!l(f[t],e);)t=t+1&d;f[t]===-1&&(f[t]=e),p[e]=f[t]}for(let e=0;e<o;e++)t[e]=p[t[e]]}let c=new Float32Array(s*3),l=new Float32Array(s);for(let n=0;n<s;n++){let r=t[n*3]*3,i=t[n*3+1]*3,a=t[n*3+2]*3,o=e[i]-e[r],s=e[i+1]-e[r+1],u=e[i+2]-e[r+2],d=e[a]-e[r],f=e[a+1]-e[r+1],p=e[a+2]-e[r+2],m=s*p-u*f,h=u*d-o*p,g=o*f-s*d,_=Math.sqrt(m*m+h*h+g*g);_!==0&&(c[n*3]=m/_,c[n*3+1]=h/_,c[n*3+2]=g/_,l[n]=_)}let u=new Uint32Array(a+1);for(let e=0;e<o;e++)u[t[e]+1]+=1;for(let e=0;e<a;e++)u[e+1]+=u[e];let d=new Uint32Array(o),f=u.slice(0,a);for(let e=0;e<o;e++){let n=t[e];d[f[n]]=e,f[n]+=1}let p=0;for(let e=0;e<a;e++)p=Math.max(p,u[e+1]-u[e]);let m=new Uint32Array(o),h=new Uint32Array(a),g=new Float32Array(Math.max(1,p)*3);for(let e=0;e<a;e++){let t=0;for(let n=u[e];n<u[e+1];n++){let e=d[n],r=Math.floor(e/3)*3,a=c[r],o=c[r+1],s=c[r+2],l=-1;if(a===0&&o===0&&s===0)l=0;else for(let e=0;e<t;e++)if(g[e*3]*a+g[e*3+1]*o+g[e*3+2]*s>=i){l=e;break}l===-1&&(l=t,g[t*3]=a,g[t*3+1]=o,g[t*3+2]=s,t++),m[e]=l}h[e]=Math.max(1,t)}let _=new Uint32Array(a+1);for(let e=0;e<a;e++)_[e+1]=_[e]+h[e]-1;let v=_[a],y=a+v,b=e,x=n,S=r;if(v>0){b=new Float32Array(y*3),b.set(e),x=n?new Float32Array(y*2):null,x&&n&&x.set(n),S=r?new Uint8Array(y*3):null,S&&r&&S.set(r);for(let t=0;t<a;t++)for(let i=1;i<h[t];i++){let o=a+_[t]+i-1;b[o*3]=e[t*3],b[o*3+1]=e[t*3+1],b[o*3+2]=e[t*3+2],x&&n&&(x[o*2]=n[t*2],x[o*2+1]=n[t*2+1]),S&&r&&(S[o*3]=r[t*3],S[o*3+1]=r[t*3+1],S[o*3+2]=r[t*3+2])}}let C=new Float32Array(y*3);for(let e=0;e<o;e++){let n=t[e],r=m[e],i=r===0?n:a+_[n]+r-1;t[e]=i;let o=Math.floor(e/3)*3,s=l[o/3];C[i*3]+=c[o]*s,C[i*3+1]+=c[o+1]*s,C[i*3+2]+=c[o+2]*s}for(let e=0;e<C.length;e+=3){let t=C[e],n=C[e+1],r=C[e+2],i=Math.sqrt(t*t+n*n+r*r)||1;C[e]=t/i,C[e+1]=n/i,C[e+2]=r/i}return{positions:b,normals:C,indices:t,uvs:x,colors:S}}function H(e,t,n,r){let i=e.length/3,a=new Float32Array(i*3),o=new Float32Array(i*3);for(let t=0;t<n.length;t+=3){let i=n[t],s=n[t+1],c=n[t+2],l=e[s*3]-e[i*3],u=e[s*3+1]-e[i*3+1],d=e[s*3+2]-e[i*3+2],f=e[c*3]-e[i*3],p=e[c*3+1]-e[i*3+1],m=e[c*3+2]-e[i*3+2],h=u*m-d*p,g=d*f-l*m,_=l*p-u*f,v=Math.sqrt(h*h+g*g+_*_);if(v===0)continue;let y=(p*_-m*g)/v,b=(m*h-f*_)/v,x=(f*g-p*h)/v,S=(g*d-_*u)/v,C=(_*l-h*d)/v,w=(h*u-g*l)/v,T=r[s*2]-r[i*2],E=r[c*2]-r[i*2],D=r[s*2+1]-r[i*2+1],O=r[c*2+1]-r[i*2+1],k=T*y+E*S,A=T*b+E*C,j=T*x+E*w,M=D*y+O*S,N=D*b+O*C,P=D*x+O*w;for(let e=0;e<3;e++){let r=n[t+e];a[r*3]+=k,a[r*3+1]+=A,a[r*3+2]+=j,o[r*3]+=M,o[r*3+1]+=N,o[r*3+2]+=P}}let s=new Float32Array(i*4);for(let e=0;e<i;e++){let n=t[e*3],r=t[e*3+1],i=t[e*3+2],c=a[e*3]*n+a[e*3+1]*r+a[e*3+2]*i,l=a[e*3]-c*n,u=a[e*3+1]-c*r,d=a[e*3+2]-c*i,f=Math.sqrt(l*l+u*u+d*d);if(!(f>1e-12)){let e=+(Math.abs(n)<.9),t=1-e;l=t*i,u=-e*i,d=e*r-t*n,f=Math.sqrt(l*l+u*u+d*d)||1}l/=f,u/=f,d/=f;let p=r*d-i*u,m=i*l-n*d,h=n*u-r*l,g=p*o[e*3]+m*o[e*3+1]+h*o[e*3+2];s[e*4]=l,s[e*4+1]=u,s[e*4+2]=d,s[e*4+3]=g<0?-1:1}return s}function U(e,t){let n=se(e.positions,e.indices,t),r=ce(e.positions,e.indices,e.uvs,e.colors),i=r.uvs?H(r.positions,r.normals,r.indices,r.uvs):null;return{...r,closed:n,tangents:i}}function le(e){let{isFloat32:t,deltaEncoded:n,planarByteSplit:r,uint16Indices:i,origin:a,scale:o,uvs:s,colors:c,jobs:l}=e,u=e=>e>>>1^-(e&1),d;if(t)d=e.vertexData;else{let t;if(r){let n=e.vertexData,r=n.length/6;t=new Int16Array(r*3);let i=0,a=0,o=0;for(let e=0;e<r;e++)i=i+u(n[e]|n[r*3+e]<<8)<<16>>16,a=a+u(n[r+e]|n[r*4+e]<<8)<<16>>16,o=o+u(n[r*2+e]|n[r*5+e]<<8)<<16>>16,t[e*3]=i,t[e*3+1]=a,t[e*3+2]=o}else if(n){let n=e.vertexData;t=new Int16Array(n.length);let r=0,i=0,a=0;for(let e=0;e<n.length;e+=3)r=r+u(n[e])<<16>>16,i=i+u(n[e+1])<<16>>16,a=a+u(n[e+2])<<16>>16,t[e]=r,t[e+1]=i,t[e+2]=a}else t=e.vertexData;d=new Float32Array(t.length);let i=a[0],s=a[1],c=a[2],l=o[0],f=o[1],p=o[2];for(let e=0;e<t.length;e+=3)d[e]=i+(t[e]+32767)*l,d[e+1]=s+(t[e+1]+32767)*f,d[e+2]=c+(t[e+2]+32767)*p}let f;if(r){let t=e.indexData;if(i){let e=t.length/2,n=new Uint16Array(e),r=0;for(let i=0;i<e;i++)r=r+u(t[i]|t[e+i]<<8)&65535,n[i]=r;f=n}else{let e=t.length/4,n=new Uint32Array(e),r=0;for(let i=0;i<e;i++){let a=(t[i]|t[e+i]<<8|t[e*2+i]<<16|t[e*3+i]<<24)>>>0;r=r+u(a)>>>0,n[i]=r}f=n}}else if(n){let t=e.indexData;if(t instanceof Uint16Array){let e=new Uint16Array(t.length),n=0;for(let r=0;r<t.length;r++)n=n+u(t[r])&65535,e[r]=n;f=e}else{let e=new Uint32Array(t.length),n=0;for(let r=0;r<t.length;r++)n=n+u(t[r])>>>0,e[r]=n;f=e}}else f=e.indexData;let p=d.length/3;for(let e=0;e<f.length;e++)if(f[e]>=p)throw Error(`Index ${f[e]} out of range of vertexCount ${p}`);let m=[];for(let e of l){let t=0,n=0;for(let r of e.windows)t+=r.vertexCount,n+=r.indexCount;let r=new Float32Array(t*3),i=new Uint32Array(n),a=s?new Float32Array(t*2):null,o=c?new Uint8Array(t*3):null,l=0,u=0;for(let t of e.windows){let e=t.vertexStart*3;r.set(d.subarray(e,e+t.vertexCount*3),l*3),a&&s&&a.set(s.subarray(t.vertexStart*2,(t.vertexStart+t.vertexCount)*2),l*2),o&&c&&o.set(c.subarray(e,e+t.vertexCount*3),l*3);let n=t.vertexStart,p=t.vertexStart+t.vertexCount,m=l-t.vertexStart;for(let e=0;e<t.indexCount;e++){let r=f[t.indexStart+e];if(r<n||r>=p)throw Error(`Index ${r} outside vertex window [${n}, ${p})`);i[u+e]=r+m}l+=t.vertexCount,u+=t.indexCount}m.push({positions:r,indices:i,uvs:a,colors:o})}return m}function ue(){return[`const assemble = ${le.toString()};`,`const isClosedSolid = ${se.toString()};`,`const splitCreases = ${ce.toString()};`,`const grainTangents = ${H.toString()};`,`self.onmessage = (event) => {`,` const { id, input } = event.data;`,` try {`,` const geometries = assemble(input).map((g, i) => {`,` const counts = input.jobs[i].windows.map((w) => w.indexCount);`,` const closed = isClosedSolid(g.positions, g.indices, counts);`,` const c = splitCreases(g.positions, g.indices, g.uvs, g.colors);`,` const tangents = c.uvs ? grainTangents(c.positions, c.normals, c.indices, c.uvs) : null;`,` return { ...c, closed, tangents };`,` });`,` const transfer = [];`,` for (const g of geometries) {`,` transfer.push(g.positions.buffer, g.normals.buffer, g.indices.buffer);`,` if (g.uvs) transfer.push(g.uvs.buffer);`,` if (g.tangents) transfer.push(g.tangents.buffer);`,` if (g.colors) transfer.push(g.colors.buffer);`,` }`,` self.postMessage({ id, geometries }, transfer);`,` } catch (error) {`,` self.postMessage({ id, error: String((error && error.message) || error) });`,` }`,`};`].join(`
2
- `)}let W;const G=new Map;let de=1;function fe(){if(W!==void 0)return W;if(typeof Worker>`u`||typeof Blob>`u`||typeof URL>`u`||typeof URL.createObjectURL!=`function`)return W=null,null;try{let e=URL.createObjectURL(new Blob([ue()],{type:`text/javascript`})),t=new Worker(e);t.onmessage=e=>{let{id:t,geometries:n,error:r}=e.data,i=G.get(t);i&&(G.delete(t),n?i.resolve(n):i.reject(Error(r??`mesh assembly failed in worker`)))},t.onerror=()=>{for(let e of G.values())e.reject(Error(`mesh assembly worker crashed`));G.clear(),t.terminate(),W=null},W=t}catch{W=null}return W}function pe(e,t,n){return new Promise((r,i)=>{let a=de++;G.set(a,{resolve:r,reject:i}),e.postMessage({id:a,input:t},n)})}let K=1;function q(e){K=Math.max(1,e)}t.n(q);function J(e,r,i,a){typeof document>`u`||new n.TextureLoader().load(r,t=>{if(t.colorSpace=i===`map`?n.SRGBColorSpace:n.NoColorSpace,t.anisotropy=K,a){let e=t.image,r=e?.width&&e.height?e.height/e.width:1;t.wrapS=n.RepeatWrapping,t.wrapT=n.RepeatWrapping,t.repeat.set(1/a,1/(a*r))}e[i]=t,e.needsUpdate=!0},void 0,e=>{t.p().warn(`Failed to load material ${i} ${r}:`,e)})}const me={brushed:{fibres:[{pitch:.04,length:[2,12],depth:.3},{pitch:.25,length:[5,40],depth:.18}],bands:[]},rolled:{fibres:[{pitch:.3,length:[80,600],depth:.07}],bands:[{width:20,length:1500,depth:.05}]}},he=.35,Y=e=>e.toFixed(4);function ge(e,t){let n=me[e];if(!n||!(t>0))return null;let r=n.fibres.map((e,t)=>`selvaFibres( vUv, footprint, ${Y(e.pitch)}, vec2( ${Y(e.length[0])}, ${Y(e.length[1])} ), ${Y(e.depth)}, ${Y(t*31+3)} )`).join(` + `)||`0.0`,i=n.bands.map(e=>`selvaBand( vUv, footprint, vec2( ${Y(e.length)}, ${Y(e.width)} ), ${Y(e.depth)} )`).join(` + `)||`0.0`;return{key:`finish:${e}:${Y(t)}`,patch:e=>{e.fragmentShader=e.fragmentShader.replace(`#include <common>`,`#include <common>
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ const require_gpu_ownership = require("./gpu-ownership-ADRZBxxs.cjs");
3
+ const require_gpu_dispose = require("./gpu-dispose-Cxu3dFmA.cjs");
4
+ let three = require("three");
5
+ three = require_gpu_ownership.__toESM(three, 1);
6
+ let three_addons_lines_Line2_js = require("three/addons/lines/Line2.js");
7
+ let three_addons_lines_LineGeometry_js = require("three/addons/lines/LineGeometry.js");
8
+ let three_addons_lines_LineMaterial_js = require("three/addons/lines/LineMaterial.js");
9
+ let fflate = require("fflate");
10
+ // src/shared/encoding.ts
11
+ function getNodeBuffer() {
12
+ const buf = globalThis.Buffer;
13
+ return typeof buf === "function" ? buf : void 0;
14
+ }
15
+ /**
16
+ * WHATWG forgiving-base64 normalization without the alphabet check: strip ASCII whitespace, then
17
+ * drop trailing padding where the spec allows it (total length a multiple of 4). `null` when the
18
+ * length can't be base64. The decoders check the alphabet themselves, faster than a regex.
19
+ */
20
+ function stripBase64(str) {
21
+ let data = str.replace(/[\t\n\f\r ]/g, "");
22
+ if (data.length % 4 === 0) data = data.replace(/={1,2}$/, "");
23
+ return data.length % 4 === 1 ? null : data;
24
+ }
25
+ /**
26
+ * @throws {VisualizationError} `ENCODING_ERROR` if invalid, or `INVALID_STATE` if no decoder is
27
+ * available in this environment.
28
+ */
29
+ function decodeBase64ToBinary(base64File) {
30
+ const data = stripBase64(base64File);
31
+ const bytes = data === null ? null : decodeStripped(data);
32
+ if (bytes === null) throw new require_gpu_dispose.VisualizationError("Invalid base64 input.", require_gpu_dispose.ErrorCodes.ENCODING_ERROR, { context: { inputLength: base64File.length } });
33
+ return bytes;
34
+ }
35
+ /** Decode {@link stripBase64} output; `null` when a character is outside the alphabet. */
36
+ function decodeStripped(data) {
37
+ const Buffer = getNodeBuffer();
38
+ if (Buffer) {
39
+ if (Buffer.byteLength(data, "utf8") !== data.length) return null;
40
+ if (data.includes("-") || data.includes("_")) return null;
41
+ const decoded = Buffer.from(data, "base64");
42
+ if (decoded.length !== Math.floor(data.length * 3 / 4)) return null;
43
+ return decoded.byteOffset === 0 && decoded.buffer.byteLength === decoded.length ? new Uint8Array(decoded.buffer, 0, decoded.length) : new Uint8Array(decoded);
44
+ }
45
+ if (typeof globalThis.atob === "function") {
46
+ let binary;
47
+ try {
48
+ binary = globalThis.atob(data);
49
+ } catch {
50
+ return null;
51
+ }
52
+ const bytes = new Uint8Array(binary.length);
53
+ for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i) & 255;
54
+ return bytes;
55
+ }
56
+ throw new require_gpu_dispose.VisualizationError("Base64 decoding not supported in this environment.", require_gpu_dispose.ErrorCodes.INVALID_STATE, { context: { environmentInfo: "atob or Buffer not available" } });
57
+ }
58
+ // src/parse/mesh-policy.ts
59
+ function cloneSceneObjects(meshes) {
60
+ return meshes.map((root) => {
61
+ const copy = root.clone(true);
62
+ const sources = [];
63
+ root.traverse((child) => sources.push(child));
64
+ let i = 0;
65
+ copy.traverse((child) => {
66
+ const source = sources[i++];
67
+ const target = child;
68
+ if (!source.geometry) return;
69
+ target.geometry = source.geometry.clone();
70
+ });
71
+ return copy;
72
+ });
73
+ }
74
+ /** Skips materials — the memo never owns those; see the file header. */
75
+ function releaseSceneObjects(meshes) {
76
+ meshes.forEach((root) => require_gpu_dispose.disposeObjectTree(root, { materials: false }));
77
+ }
78
+ /** Structurally, not nominally, typed as `@selvajs/solve/client`'s `MeshPolicy<THREE.Object3D>` — avoids a dependency on solve. */
79
+ const meshPolicy = {
80
+ clone: cloneSceneObjects,
81
+ release: releaseSceneObjects
82
+ };
83
+ /** Opacity < 1 flips `transparent` on. */
84
+ function materialParams(color, opacity) {
85
+ const resolved = opacity ?? 1;
86
+ return {
87
+ color: new three.Color(color ?? "#ffffff"),
88
+ transparent: resolved < 1,
89
+ opacity: resolved
90
+ };
91
+ }
92
+ // src/parse/display-items/items/curves.ts
93
+ const DEFAULT_LINE_WIDTH = 2;
94
+ /** Two vertices — the shortest renderable polyline. */
95
+ const MIN_POSITIONS = 6;
96
+ /**
97
+ * Curves arrive tessellated: the backend sends `points`, this builds the line. Nothing decodes
98
+ * geometry in the browser.
99
+ *
100
+ * Uses `Line2`/`LineMaterial` instead of `THREE.Line`: plain `THREE.Line` is hard-capped at 1px on
101
+ * every major GPU backend, so `item.width` would go unhonoured. `Line2.onBeforeRender` sets
102
+ * `LineMaterial`'s required `resolution`, so no renderer reference is needed here.
103
+ *
104
+ * @throws VisualizationError when the item has no `points` — see {@link curvePositions}.
105
+ */
106
+ function buildCurveLine(item) {
107
+ const positions = curvePositions(item);
108
+ if (!positions) return null;
109
+ const geometry = new three_addons_lines_LineGeometry_js.LineGeometry();
110
+ geometry.setPositions(positions);
111
+ const params = materialParams(item.color, item.opacity);
112
+ const material = new three_addons_lines_LineMaterial_js.LineMaterial({ color: params.color });
113
+ const styled = material;
114
+ styled.linewidth = item.width ?? DEFAULT_LINE_WIDTH;
115
+ styled.transparent = params.transparent;
116
+ styled.opacity = params.opacity;
117
+ const line = new three_addons_lines_Line2_js.Line2(geometry, material);
118
+ line.computeLineDistances();
119
+ line.name = item.name;
120
+ line.userData = {
121
+ source: "compute",
122
+ id: item.id,
123
+ layer: item.layer,
124
+ kind: "curve",
125
+ metadata: item.metadata
126
+ };
127
+ return line;
128
+ }
129
+ /**
130
+ * Flat `[x,y,z, …]`, or null for a degenerate curve — one of those can't abort the batch.
131
+ *
132
+ * A curve with no `points` **throws** instead. It means the definition was solved by a Display
133
+ * component predating backend tessellation, which is a stale definition rather than one bad curve:
134
+ * skipping would render a scene silently missing geometry, indistinguishable from a definition that
135
+ * has no curves, with the fix nowhere in sight.
136
+ */
137
+ function curvePositions(item) {
138
+ if (!item.points) throw new require_gpu_dispose.VisualizationError(`Curve display item '${item.id}' has no tessellated points. It was produced by an outdated Display component — upgrade it in Grasshopper (Solution → Upgrade obsolete components) and re-save the definition.`, require_gpu_dispose.ErrorCodes.INVALID_CONFIG, { context: {
139
+ itemId: item.id,
140
+ kind: item.kind
141
+ } });
142
+ return item.points.length >= MIN_POSITIONS ? item.points : null;
143
+ }
144
+ // src/parse/display-items/items/points.ts
145
+ function buildPoint(item) {
146
+ const { position } = item;
147
+ if (!position || typeof position.X !== "number" || !Number.isFinite(position.X) || typeof position.Y !== "number" || !Number.isFinite(position.Y) || typeof position.Z !== "number" || !Number.isFinite(position.Z)) {
148
+ require_gpu_dispose.getLogger().warn(`Skipping point display item with missing or non-finite position (id: ${String(item.id)}).`);
149
+ return null;
150
+ }
151
+ const geometry = new three.BufferGeometry();
152
+ geometry.setAttribute("position", new three.Float32BufferAttribute([
153
+ position.X,
154
+ position.Y,
155
+ position.Z
156
+ ], 3));
157
+ const material = new three.PointsMaterial({
158
+ ...materialParams(item.color, item.opacity),
159
+ size: 6,
160
+ sizeAttenuation: false
161
+ });
162
+ const points = new three.Points(geometry, material);
163
+ points.name = item.name;
164
+ points.userData = {
165
+ source: "compute",
166
+ id: item.id,
167
+ layer: item.layer,
168
+ kind: "point",
169
+ metadata: item.metadata
170
+ };
171
+ return points;
172
+ }
173
+ // src/parse/display-items/display-items-parser.ts
174
+ /**
175
+ * Builds THREE objects for the batch's non-mesh items.
176
+ *
177
+ * @throws VisualizationError when a curve predates backend tessellation, so a stale definition
178
+ * surfaces as an actionable error instead of a scene quietly missing its curves. Every other
179
+ * unrenderable item is logged and skipped.
180
+ */
181
+ function parseDisplayItems(items) {
182
+ if (!items || items.length === 0) return [];
183
+ const objects = [];
184
+ for (const item of items) switch (item.kind) {
185
+ case "curve": {
186
+ const line = buildCurveLine(item);
187
+ if (line) objects.push(line);
188
+ break;
189
+ }
190
+ case "point": {
191
+ const point = buildPoint(item);
192
+ if (point) objects.push(point);
193
+ break;
194
+ }
195
+ default: {
196
+ const unknown = item;
197
+ require_gpu_dispose.getLogger().warn(`Skipping unknown display item kind: ${String(unknown.kind)}`);
198
+ break;
199
+ }
200
+ }
201
+ return objects;
202
+ }
203
+ // src/parse/webdisplay/binary/header.ts
204
+ /** "SLVA" little-endian — an uncompressed mesh blob. */
205
+ const BINARY_MESH_MAGIC = 1096174675;
206
+ /**
207
+ * Header fields use explicit-LE `DataView` reads, but the zero-copy geometry readers build
208
+ * typed-array views in *host* byte order (every mainstream JS target is little-endian, and
209
+ * per-element DataView reads would be far costlier on the hot geometry paths). This check makes
210
+ * the assumption explicit: on a big-endian host the parser refuses to decode rather than return
211
+ * byte-swapped garbage.
212
+ */
213
+ const HOST_IS_LITTLE_ENDIAN = new Uint16Array(new Uint8Array([1, 0]).buffer)[0] === 1;
214
+ // src/parse/webdisplay/binary/geometry.ts
215
+ function toUint8Array(input) {
216
+ if (typeof input === "string") return decodeBase64ToBinary(input);
217
+ if (input instanceof Uint8Array) return input;
218
+ return new Uint8Array(input);
219
+ }
220
+ function isSlvz(bytes) {
221
+ return bytes.byteLength >= 8 && new DataView(bytes.buffer, bytes.byteOffset, 8).getUint32(0, true) === 1515605075;
222
+ }
223
+ /** Size a SLVZ blob inflates to, as its header declares; 0 for anything else. Unvalidated. */
224
+ function slvzInflatedBytes(bytes) {
225
+ return isSlvz(bytes) ? new DataView(bytes.buffer, bytes.byteOffset, 8).getUint32(4, true) : 0;
226
+ }
227
+ function slvzPayload(bytes) {
228
+ const uncompressedLen = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength).getUint32(4, true);
229
+ const deflated = bytes.subarray(8);
230
+ const maxPlausibleLen = Math.max(deflated.byteLength * 1032 + 1024, 1 << 20);
231
+ if (uncompressedLen > maxPlausibleLen) throw fail("SLVZ header declares an implausible uncompressed length", {
232
+ uncompressedLen,
233
+ deflatedBytes: deflated.byteLength,
234
+ maxPlausibleLen
235
+ });
236
+ return {
237
+ deflated,
238
+ uncompressedLen
239
+ };
240
+ }
241
+ function inflateFailure(error, payload) {
242
+ return fail(`Failed to inflate SLVZ blob: ${error instanceof Error ? error.message : String(error)}`, {
243
+ uncompressedLen: payload.uncompressedLen,
244
+ deflatedBytes: payload.deflated.byteLength
245
+ });
246
+ }
247
+ function checkInflatedLength(out, payload) {
248
+ if (out.byteLength !== payload.uncompressedLen) throw fail("SLVZ payload inflated to a different size than the header declares.", {
249
+ declaredLen: payload.uncompressedLen,
250
+ actualLen: out.byteLength,
251
+ deflatedBytes: payload.deflated.byteLength
252
+ });
253
+ return out;
254
+ }
255
+ /**
256
+ * If the blob is a SLVZ compressed container, inflate it back to the raw SLVA bytes; otherwise
257
+ * return the input untouched. Detection is by the leading 4-byte magic, so an uncompressed SLVA
258
+ * blob (or any pre-v3 payload) flows through unchanged.
259
+ */
260
+ function maybeDecompress(bytes) {
261
+ if (!isSlvz(bytes)) return bytes;
262
+ const payload = slvzPayload(bytes);
263
+ let out;
264
+ try {
265
+ out = (0, fflate.inflateSync)(payload.deflated, { out: new Uint8Array(payload.uncompressedLen + 1) });
266
+ } catch (error) {
267
+ throw inflateFailure(error, payload);
268
+ }
269
+ return checkInflatedLength(out, payload);
270
+ }
271
+ /**
272
+ * {@link maybeDecompress} for a SLVZ blob, inflated in fflate's own worker so the calling thread
273
+ * stays free. The result owns its buffer. Rejects on any failure, including a worker that could not
274
+ * start: the caller falls back to {@link maybeDecompress}, which reports a corrupt blob properly.
275
+ */
276
+ function inflateSlvzAsync(bytes) {
277
+ return new Promise((resolve, reject) => {
278
+ const payload = slvzPayload(bytes);
279
+ try {
280
+ (0, fflate.inflate)(payload.deflated, { size: payload.uncompressedLen + 1 }, (error, out) => {
281
+ if (error) {
282
+ reject(inflateFailure(error, payload));
283
+ return;
284
+ }
285
+ try {
286
+ resolve(checkInflatedLength(out, payload));
287
+ } catch (mismatch) {
288
+ reject(mismatch);
289
+ }
290
+ });
291
+ } catch (error) {
292
+ reject(inflateFailure(error, payload));
293
+ }
294
+ });
295
+ }
296
+ function decodeUtf8(bytes) {
297
+ if (typeof TextDecoder !== "undefined") return new TextDecoder("utf-8").decode(bytes);
298
+ if (typeof globalThis.Buffer !== "undefined") return globalThis.Buffer.from(bytes).toString("utf-8");
299
+ throw new require_gpu_dispose.VisualizationError("No UTF-8 decoder available in this environment.", require_gpu_dispose.ErrorCodes.INVALID_STATE);
300
+ }
301
+ function readInt16Vertices(buffer, byteOffset, count) {
302
+ if (count === 0) return /* @__PURE__ */ new Int16Array(0);
303
+ if (byteOffset % 2 === 0) return new Int16Array(buffer, byteOffset, count);
304
+ const copy = new Uint8Array(count * 2);
305
+ copy.set(new Uint8Array(buffer, byteOffset, count * 2));
306
+ return new Int16Array(copy.buffer);
307
+ }
308
+ function readFloat32Vertices(buffer, byteOffset, count) {
309
+ if (count === 0) return /* @__PURE__ */ new Float32Array(0);
310
+ if (byteOffset % 4 === 0) return new Float32Array(buffer, byteOffset, count);
311
+ const copy = new Uint8Array(count * 4);
312
+ copy.set(new Uint8Array(buffer, byteOffset, count * 4));
313
+ return new Float32Array(copy.buffer);
314
+ }
315
+ function readUint16Array(buffer, byteOffset, count) {
316
+ if (count === 0) return /* @__PURE__ */ new Uint16Array(0);
317
+ if (byteOffset % 2 === 0) return new Uint16Array(buffer, byteOffset, count);
318
+ const copy = new Uint8Array(count * 2);
319
+ copy.set(new Uint8Array(buffer, byteOffset, count * 2));
320
+ return new Uint16Array(copy.buffer);
321
+ }
322
+ function readUint32Array(buffer, byteOffset, count) {
323
+ if (count === 0) return /* @__PURE__ */ new Uint32Array(0);
324
+ if (byteOffset % 4 === 0) return new Uint32Array(buffer, byteOffset, count);
325
+ const copy = new Uint8Array(count * 4);
326
+ copy.set(new Uint8Array(buffer, byteOffset, count * 4));
327
+ return new Uint32Array(copy.buffer);
328
+ }
329
+ /**
330
+ * Rejects blobs whose index stream references vertices past `vertexCount`. Downstream mesh
331
+ * assembly trusts indices arithmetically (rebasing, `subarray` slicing), so an out-of-range index
332
+ * would otherwise corrupt geometry silently instead of failing the parse. A uint16 index stream
333
+ * can't exceed a vertex count above 65535, so that case skips the scan.
334
+ */
335
+ function validateIndicesInRange(indices, vertexCount) {
336
+ if (indices.length === 0) return;
337
+ if (indices instanceof Uint16Array && vertexCount > 65535) return;
338
+ for (let i = 0; i < indices.length; i++) if (indices[i] >= vertexCount) throw fail("Index out of range of vertexCount.", {
339
+ indexPosition: i,
340
+ indexValue: indices[i],
341
+ vertexCount
342
+ });
343
+ }
344
+ /** Inverse of the writer's zigzag map: 0,1,2,3 → 0,-1,1,-2. */
345
+ function unzigzag(zz) {
346
+ return zz >>> 1 ^ -(zz & 1);
347
+ }
348
+ /**
349
+ * Undoes the v3 delta filter on the quantized vertex stream: each component is a zigzag-mapped,
350
+ * wrapped 16-bit difference from the previous vertex's same component (independent x/y/z running
351
+ * sums). `(x << 16) >> 16` reproduces the writer's int16 wrapping.
352
+ */
353
+ function decodeDeltaVertices(zigzagged) {
354
+ const out = new Int16Array(zigzagged.length);
355
+ let px = 0;
356
+ let py = 0;
357
+ let pz = 0;
358
+ for (let i = 0; i < zigzagged.length; i += 3) {
359
+ px = px + unzigzag(zigzagged[i]) << 16 >> 16;
360
+ py = py + unzigzag(zigzagged[i + 1]) << 16 >> 16;
361
+ pz = pz + unzigzag(zigzagged[i + 2]) << 16 >> 16;
362
+ out[i] = px;
363
+ out[i + 1] = py;
364
+ out[i + 2] = pz;
365
+ }
366
+ return out;
367
+ }
368
+ /**
369
+ * Undoes the v4 planar byte-split vertex layout: six byte planes
370
+ * `[Xlo][Ylo][Zlo][Xhi][Yhi][Zhi]` (each `vertexCount` bytes) of the zigzag deltas — merge the
371
+ * planes, unzigzag, prefix-sum per component. Same delta semantics as {@link decodeDeltaVertices}.
372
+ */
373
+ function decodePlanarVertices(planes, vertexCount) {
374
+ const out = new Int16Array(vertexCount * 3);
375
+ const n = vertexCount;
376
+ let px = 0;
377
+ let py = 0;
378
+ let pz = 0;
379
+ for (let i = 0; i < n; i++) {
380
+ px = px + unzigzag(planes[i] | planes[n * 3 + i] << 8) << 16 >> 16;
381
+ py = py + unzigzag(planes[n + i] | planes[n * 4 + i] << 8) << 16 >> 16;
382
+ pz = pz + unzigzag(planes[n * 2 + i] | planes[n * 5 + i] << 8) << 16 >> 16;
383
+ out[i * 3] = px;
384
+ out[i * 3 + 1] = py;
385
+ out[i * 3 + 2] = pz;
386
+ }
387
+ return out;
388
+ }
389
+ /** Undoes the v4 planar byte-split uint16 index layout `[lo × N][hi × N]`. */
390
+ function decodePlanarIndices16(planes, count) {
391
+ const out = new Uint16Array(count);
392
+ let prev = 0;
393
+ for (let i = 0; i < count; i++) {
394
+ prev = prev + unzigzag(planes[i] | planes[count + i] << 8) & 65535;
395
+ out[i] = prev;
396
+ }
397
+ return out;
398
+ }
399
+ /** Undoes the v4 planar byte-split uint32 index layout `[b0 × N][b1 × N][b2 × N][b3 × N]`. */
400
+ function decodePlanarIndices32(planes, count) {
401
+ const out = new Uint32Array(count);
402
+ let prev = 0;
403
+ for (let i = 0; i < count; i++) {
404
+ const zz = (planes[i] | planes[count + i] << 8 | planes[count * 2 + i] << 16 | planes[count * 3 + i] << 24) >>> 0;
405
+ prev = prev + unzigzag(zz) >>> 0;
406
+ out[i] = prev;
407
+ }
408
+ return out;
409
+ }
410
+ function decodeDeltaIndices16(zigzagged) {
411
+ const out = new Uint16Array(zigzagged.length);
412
+ let prev = 0;
413
+ for (let i = 0; i < zigzagged.length; i++) {
414
+ prev = prev + unzigzag(zigzagged[i]) & 65535;
415
+ out[i] = prev;
416
+ }
417
+ return out;
418
+ }
419
+ function decodeDeltaIndices32(zigzagged) {
420
+ const out = new Uint32Array(zigzagged.length);
421
+ let prev = 0;
422
+ for (let i = 0; i < zigzagged.length; i++) {
423
+ prev = prev + unzigzag(zigzagged[i]) >>> 0;
424
+ out[i] = prev;
425
+ }
426
+ return out;
427
+ }
428
+ function fail(message, context) {
429
+ return new require_gpu_dispose.VisualizationError(message, require_gpu_dispose.ErrorCodes.VALIDATION_ERROR, { context });
430
+ }
431
+ // src/parse/webdisplay/binary/textures.ts
432
+ /** Byte size of the UV chunk header: uvFormat(u32) + uvOrigin(2×f64) + uvScale(2×f64). */
433
+ const UV_CHUNK_HEADER_BYTES = 36;
434
+ /**
435
+ * Parses the trailing UV chunk into absolute Float32 u,v pairs. Quantized UVs reconstruct as
436
+ * `origin + q * scale` (unsigned q), undoing the per-component delta+zigzag filter when set;
437
+ * float32 UVs are copied out as-is (never filtered).
438
+ */
439
+ function parseUvChunk(bytes, view, offset, vertexCount, deltaEncoded, planarByteSplit = false) {
440
+ if (offset + UV_CHUNK_HEADER_BYTES > bytes.byteLength) throw fail("Insufficient data to read UV chunk header.", {
441
+ expectedBytes: UV_CHUNK_HEADER_BYTES,
442
+ availableBytes: bytes.byteLength - offset,
443
+ offset
444
+ });
445
+ const uvFormat = view.getUint32(offset, true);
446
+ offset += 4;
447
+ const originU = view.getFloat64(offset, true);
448
+ offset += 8;
449
+ const originV = view.getFloat64(offset, true);
450
+ offset += 8;
451
+ const scaleU = view.getFloat64(offset, true);
452
+ offset += 8;
453
+ const scaleV = view.getFloat64(offset, true);
454
+ offset += 8;
455
+ const componentCount = vertexCount * 2;
456
+ const useFloat32 = uvFormat === 1;
457
+ const dataByteLength = componentCount * (useFloat32 ? 4 : 2);
458
+ if (offset + dataByteLength > bytes.byteLength) throw fail("Insufficient data to read UV chunk.", {
459
+ expectedBytes: dataByteLength,
460
+ availableBytes: bytes.byteLength - offset,
461
+ offset,
462
+ uvFormat,
463
+ vertexCount
464
+ });
465
+ const absoluteOffset = bytes.byteOffset + offset;
466
+ let uvs;
467
+ if (useFloat32) uvs = readFloat32Vertices(bytes.buffer, absoluteOffset, componentCount).slice();
468
+ else if (planarByteSplit) {
469
+ const planes = bytes.subarray(offset, offset + dataByteLength);
470
+ const n = vertexCount;
471
+ uvs = new Float32Array(componentCount);
472
+ let qu = 0;
473
+ let qv = 0;
474
+ for (let i = 0; i < n; i++) {
475
+ qu = qu + unzigzag(planes[i] | planes[n * 2 + i] << 8) & 65535;
476
+ qv = qv + unzigzag(planes[n + i] | planes[n * 3 + i] << 8) & 65535;
477
+ uvs[i * 2] = originU + qu * scaleU;
478
+ uvs[i * 2 + 1] = originV + qv * scaleV;
479
+ }
480
+ } else {
481
+ const raw = readUint16Array(bytes.buffer, absoluteOffset, componentCount);
482
+ uvs = new Float32Array(componentCount);
483
+ let qu = 0;
484
+ let qv = 0;
485
+ for (let i = 0; i < componentCount; i += 2) {
486
+ if (deltaEncoded) {
487
+ qu = qu + unzigzag(raw[i]) & 65535;
488
+ qv = qv + unzigzag(raw[i + 1]) & 65535;
489
+ } else {
490
+ qu = raw[i];
491
+ qv = raw[i + 1];
492
+ }
493
+ uvs[i] = originU + qu * scaleU;
494
+ uvs[i + 1] = originV + qv * scaleV;
495
+ }
496
+ }
497
+ return {
498
+ uvs,
499
+ offset: offset + dataByteLength
500
+ };
501
+ }
502
+ /**
503
+ * Parses the trailing vertex-color chunk into raw r,g,b bytes, undoing the per-channel wrapped
504
+ * 8-bit delta+zigzag filter when the blob-wide delta flag is set.
505
+ */
506
+ function parseColorChunk(bytes, offset, vertexCount, deltaEncoded) {
507
+ const byteLength = vertexCount * 3;
508
+ if (offset + byteLength > bytes.byteLength) throw fail("Insufficient data to read vertex-color chunk.", {
509
+ expectedBytes: byteLength,
510
+ availableBytes: bytes.byteLength - offset,
511
+ offset,
512
+ vertexCount
513
+ });
514
+ const raw = bytes.subarray(offset, offset + byteLength);
515
+ if (!deltaEncoded) return raw.slice();
516
+ const colors = new Uint8Array(byteLength);
517
+ let r = 0;
518
+ let g = 0;
519
+ let b = 0;
520
+ for (let i = 0; i < byteLength; i += 3) {
521
+ r = r + unzigzag(raw[i]) & 255;
522
+ g = g + unzigzag(raw[i + 1]) & 255;
523
+ b = b + unzigzag(raw[i + 2]) & 255;
524
+ colors[i] = r;
525
+ colors[i + 1] = g;
526
+ colors[i + 2] = b;
527
+ }
528
+ return colors;
529
+ }
530
+ const CHUNK_GEOM = 1297040711;
531
+ const CHUNK_TABL = 1279410516;
532
+ const CHUNK_MATL = 1280590157;
533
+ const CHUNK_TEXR = 1381516628;
534
+ /** One byte per mesh row: 0 = unknown, 1 = open, 2 = closed solid. */
535
+ const CHUNK_CLSD = 1146309699;
536
+ /** Reserved attr key carrying each object's identity — split out of the attr dict on read. */
537
+ const ID_ATTR = "id";
538
+ /** Material `map` prefix that references a TEXR chunk by index. */
539
+ const TEX_REF_PREFIX = "slvm:tex:";
540
+ const NAMES_NONE = 0;
541
+ const NAMES_SEQUENTIAL = 1;
542
+ const NAMES_POOL = 2;
543
+ function isSlvmContainer(bytes) {
544
+ return bytes.byteLength >= 4 && new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0, true) === 1297501267;
545
+ }
546
+ /**
547
+ * Parses an SLVM v3 container down to its mesh geometry blob and the metadata the rest of the
548
+ * pipeline expects. Group vertex/index windows are rebuilt as prefix sums over the table — the
549
+ * format mandates geometry is concatenated in table order, so starts are never stored.
550
+ *
551
+ * @throws {VisualizationError} On bad magic, unknown version, or a truncated/malformed container.
552
+ */
553
+ function parseSlvmContainer(bytes) {
554
+ const chunks = readChunks(bytes);
555
+ let geometryBlob = null;
556
+ let tableBytes = null;
557
+ let materialsJson = null;
558
+ let closedFlags = null;
559
+ const textures = [];
560
+ for (const { type, payload } of chunks) switch (type) {
561
+ case CHUNK_GEOM:
562
+ geometryBlob = payload;
563
+ break;
564
+ case CHUNK_TABL:
565
+ tableBytes = maybeDecompress(payload);
566
+ break;
567
+ case CHUNK_MATL:
568
+ materialsJson = decodeUtf8(payload);
569
+ break;
570
+ case CHUNK_TEXR:
571
+ textures.push(payload);
572
+ break;
573
+ case CHUNK_CLSD: closedFlags = maybeDecompress(payload);
574
+ }
575
+ if (geometryBlob === null) throw fail("SLVM container has no GEOM chunk.", { chunkCount: chunks.length });
576
+ if (tableBytes === null) throw fail("SLVM container has no TABL chunk.", { chunkCount: chunks.length });
577
+ const { groups } = parseTable(tableBytes, closedFlags);
578
+ const materials = parseMaterials(materialsJson, textures);
579
+ return {
580
+ geometryBlob,
581
+ metadata: {
582
+ materials,
583
+ groups
584
+ }
585
+ };
586
+ }
587
+ function readChunks(bytes) {
588
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
589
+ if (bytes.byteLength < 12 || view.getUint32(0, true) !== 1297501267) throw fail("Not an SLVM container (bad magic).", { byteLength: bytes.byteLength });
590
+ const version = view.getUint32(4, true);
591
+ if (version !== 3) throw fail(`Unsupported SLVM version: ${version}`, { expectedVersion: 3 });
592
+ const count = view.getUint32(8, true);
593
+ const chunks = [];
594
+ let offset = 12;
595
+ for (let i = 0; i < count; i++) {
596
+ if (offset + 8 > bytes.byteLength) throw fail("Truncated SLVM chunk header.", {
597
+ offset,
598
+ chunkIndex: i
599
+ });
600
+ const type = view.getUint32(offset, true);
601
+ const len = view.getUint32(offset + 4, true);
602
+ offset += 8;
603
+ if (offset + len > bytes.byteLength) throw fail("Truncated SLVM chunk payload.", {
604
+ offset,
605
+ chunkIndex: i,
606
+ byteLen: len
607
+ });
608
+ chunks.push({
609
+ type,
610
+ payload: bytes.subarray(offset, offset + len)
611
+ });
612
+ offset += len + (4 - len % 4) % 4;
613
+ }
614
+ return chunks;
615
+ }
616
+ function readVarint(r) {
617
+ let value = 0;
618
+ let shift = 0;
619
+ for (;;) {
620
+ if (r.pos >= r.bytes.byteLength || shift > 28) throw fail("Malformed varint in SLVM table.", { pos: r.pos });
621
+ const b = r.bytes[r.pos++];
622
+ value |= (b & 127) << shift;
623
+ if ((b & 128) === 0) return value >>> 0;
624
+ shift += 7;
625
+ }
626
+ }
627
+ function readStringColumn(r, objectCount, pool) {
628
+ const mode = r.bytes[r.pos++];
629
+ const result = new Array(objectCount);
630
+ switch (mode) {
631
+ case NAMES_NONE:
632
+ result.fill("");
633
+ break;
634
+ case NAMES_SEQUENTIAL:
635
+ for (let i = 0; i < objectCount; i++) result[i] = String(i + 1);
636
+ break;
637
+ case NAMES_POOL:
638
+ for (let i = 0; i < objectCount; i++) result[i] = pool[readVarint(r)] ?? "";
639
+ break;
640
+ default: throw fail(`Unknown SLVM string column mode: ${mode}`, { pos: r.pos });
641
+ }
642
+ return result;
643
+ }
644
+ function parseTable(bytes, closedFlags) {
645
+ const r = {
646
+ bytes,
647
+ pos: 0
648
+ };
649
+ const meshCount = readVarint(r);
650
+ const curveCount = readVarint(r);
651
+ const pointCount = readVarint(r);
652
+ const objectCount = meshCount + curveCount + pointCount;
653
+ const poolCount = readVarint(r);
654
+ const pool = new Array(poolCount);
655
+ for (let i = 0; i < poolCount; i++) {
656
+ const len = readVarint(r);
657
+ pool[i] = decodeUtf8(bytes.subarray(r.pos, r.pos + len));
658
+ r.pos += len;
659
+ }
660
+ const vertexCounts = new Array(meshCount);
661
+ const triCounts = new Array(meshCount);
662
+ for (let i = 0; i < meshCount; i++) {
663
+ vertexCounts[i] = readVarint(r);
664
+ triCounts[i] = readVarint(r);
665
+ }
666
+ for (let i = 0; i < curveCount; i++) readVarint(r);
667
+ const runCount = readVarint(r);
668
+ const runs = [];
669
+ for (let i = 0; i < runCount; i++) runs.push({
670
+ materialId: readVarint(r),
671
+ meshCount: readVarint(r)
672
+ });
673
+ const names = readStringColumn(r, objectCount, pool);
674
+ const layers = readStringColumn(r, objectCount, pool);
675
+ const attrs = new Array(objectCount);
676
+ const attrCount = readVarint(r);
677
+ for (let a = 0; a < attrCount; a++) {
678
+ const key = pool[readVarint(r)] ?? "";
679
+ const n = readVarint(r);
680
+ const indices = new Array(n);
681
+ let idx = 0;
682
+ for (let i = 0; i < n; i++) {
683
+ idx += readVarint(r);
684
+ indices[i] = idx;
685
+ }
686
+ for (let i = 0; i < n; i++) {
687
+ const value = pool[readVarint(r)] ?? "";
688
+ const objIndex = indices[i];
689
+ (attrs[objIndex] ??= {})[key] = value;
690
+ }
691
+ }
692
+ const effectiveRuns = runs.length > 0 || meshCount === 0 ? runs : [{
693
+ materialId: 0,
694
+ meshCount
695
+ }];
696
+ const groups = [];
697
+ let meshIndex = 0;
698
+ let vertexStart = 0;
699
+ let indexStart = 0;
700
+ for (const run of effectiveRuns) {
701
+ const meshes = [];
702
+ for (let i = 0; i < run.meshCount && meshIndex < meshCount; i++, meshIndex++) {
703
+ const meshAttrs = attrs[meshIndex];
704
+ const id = meshAttrs?.[ID_ATTR];
705
+ if (meshAttrs !== void 0) delete meshAttrs[ID_ATTR];
706
+ const mesh = {
707
+ id,
708
+ name: names[meshIndex],
709
+ layer: layers[meshIndex],
710
+ vertexCount: vertexCounts[meshIndex],
711
+ indexCount: triCounts[meshIndex] * 3,
712
+ vertexStart,
713
+ indexStart,
714
+ metadata: meshAttrs ?? {}
715
+ };
716
+ const verdict = closedFlags?.[meshIndex];
717
+ if (verdict === 1 || verdict === 2) mesh.closed = verdict === 2;
718
+ meshes.push(mesh);
719
+ vertexStart += vertexCounts[meshIndex];
720
+ indexStart += triCounts[meshIndex] * 3;
721
+ }
722
+ groups.push({
723
+ materialId: run.materialId,
724
+ meshes
725
+ });
726
+ }
727
+ return { groups };
728
+ }
729
+ function parseMaterials(json, textures) {
730
+ if (json === null) return [];
731
+ let materials;
732
+ try {
733
+ materials = JSON.parse(json).materials ?? [];
734
+ } catch (error) {
735
+ throw fail(`Failed to parse SLVM materials JSON: ${error instanceof Error ? error.message : String(error)}`, { byteLength: json.length });
736
+ }
737
+ for (const material of materials) for (const slot of TEXTURE_SLOTS) {
738
+ const reference = material[slot];
739
+ if (reference?.startsWith(TEX_REF_PREFIX)) {
740
+ const tex = textures[Number(reference.slice(9))];
741
+ if (tex !== void 0) material[slot] = textureToDataUri(tex);
742
+ }
743
+ }
744
+ return materials;
745
+ }
746
+ const TEXTURE_SLOTS = [
747
+ "map",
748
+ "roughnessMap",
749
+ "normalMap"
750
+ ];
751
+ /** TEXR payload is [varint mimeLen][mime utf8][image bytes]; downstream wants a data URI. */
752
+ function textureToDataUri(payload) {
753
+ const r = {
754
+ bytes: payload,
755
+ pos: 0
756
+ };
757
+ const mimeLen = readVarint(r);
758
+ const mime = decodeUtf8(payload.subarray(r.pos, r.pos + mimeLen));
759
+ const data = payload.subarray(r.pos + mimeLen);
760
+ let binary = "";
761
+ const step = 32768;
762
+ for (let i = 0; i < data.byteLength; i += step) binary += String.fromCharCode(...data.subarray(i, i + step));
763
+ return `data:${mime};base64,${btoa(binary)}`;
764
+ }
765
+ // src/parse/webdisplay/binary-parser.ts
766
+ /** Second half of {@link parseBinaryMeshBatch}, for a caller that already holds the raw view. */
767
+ function decodeRawMeshBatch(raw) {
768
+ let vertices;
769
+ if (raw.isFloat32) vertices = raw.vertexData;
770
+ else if (raw.planarByteSplit) vertices = decodePlanarVertices(raw.vertexData, raw.vertexCount);
771
+ else if (raw.deltaEncoded) vertices = decodeDeltaVertices(raw.vertexData);
772
+ else vertices = raw.vertexData;
773
+ let indices;
774
+ if (raw.planarByteSplit) {
775
+ const planes = raw.indexData;
776
+ indices = raw.uint16Indices ? decodePlanarIndices16(planes, planes.length / 2) : decodePlanarIndices32(planes, planes.length / 4);
777
+ } else if (raw.deltaEncoded) indices = raw.indexData instanceof Uint16Array ? decodeDeltaIndices16(raw.indexData) : decodeDeltaIndices32(raw.indexData);
778
+ else indices = raw.indexData;
779
+ validateIndicesInRange(indices, raw.vertexCount);
780
+ return {
781
+ metadata: raw.metadata,
782
+ flags: raw.flags,
783
+ vertices,
784
+ indices,
785
+ origin: raw.origin,
786
+ scale: raw.scale,
787
+ uvs: raw.uvs,
788
+ colors: raw.colors
789
+ };
790
+ }
791
+ /** See {@link RawBinaryMeshBatch}. Same validation/throw behavior as the decoding parser. */
792
+ function parseBinaryMeshBatchRaw(input) {
793
+ if (!HOST_IS_LITTLE_ENDIAN) throw new require_gpu_dispose.VisualizationError("SLVA parsing requires a little-endian host: the zero-copy geometry readers view the wire bytes in host byte order.", require_gpu_dispose.ErrorCodes.ENVIRONMENT_ERROR);
794
+ const rawInput = toUint8Array(input);
795
+ if (isSlvmContainer(rawInput)) {
796
+ const container = parseSlvmContainer(rawInput);
797
+ const raw = parseBinaryMeshBatchRaw(container.geometryBlob);
798
+ raw.metadata = container.metadata;
799
+ return raw;
800
+ }
801
+ const bytes = maybeDecompress(rawInput);
802
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
803
+ if (bytes.byteLength < 12) throw fail("Blob too small to contain SLVA header.", {
804
+ expectedBytes: 12,
805
+ availableBytes: bytes.byteLength
806
+ });
807
+ let offset = 0;
808
+ const magic = view.getUint32(offset, true);
809
+ offset += 4;
810
+ if (magic !== 1096174675) throw fail(`Invalid SLVA magic: 0x${magic.toString(16)}`, {
811
+ expectedMagic: `0x${BINARY_MESH_MAGIC.toString(16)}`,
812
+ actualMagic: `0x${magic.toString(16)}`
813
+ });
814
+ const version = view.getUint32(offset, true);
815
+ offset += 4;
816
+ if (version < 1 || version > 4) throw fail(`Unsupported SLVA version: ${version}`, {
817
+ minSupportedVersion: 1,
818
+ maxSupportedVersion: 4,
819
+ actualVersion: version
820
+ });
821
+ const metadataLen = view.getUint32(offset, true);
822
+ offset += 4;
823
+ if (offset + metadataLen > bytes.byteLength) throw fail("Insufficient data to read metadata JSON.", {
824
+ expectedBytes: metadataLen,
825
+ availableBytes: bytes.byteLength - offset,
826
+ offset
827
+ });
828
+ const metadataBytes = bytes.subarray(offset, offset + metadataLen);
829
+ offset += metadataLen;
830
+ let metadata;
831
+ try {
832
+ metadata = metadataLen === 0 ? {} : JSON.parse(decodeUtf8(metadataBytes));
833
+ } catch (error) {
834
+ throw fail(`Failed to parse metadata JSON: ${error instanceof Error ? error.message : String(error)}`, { metadataLen });
835
+ }
836
+ if (offset + 56 > bytes.byteLength) throw fail("Insufficient data to read geometry header.", {
837
+ expectedBytes: 56,
838
+ availableBytes: bytes.byteLength - offset,
839
+ offset
840
+ });
841
+ const flags = view.getUint32(offset, true);
842
+ offset += 4;
843
+ const originX = view.getFloat64(offset, true);
844
+ offset += 8;
845
+ const originY = view.getFloat64(offset, true);
846
+ offset += 8;
847
+ const originZ = view.getFloat64(offset, true);
848
+ offset += 8;
849
+ const scaleX = view.getFloat64(offset, true);
850
+ offset += 8;
851
+ const scaleY = view.getFloat64(offset, true);
852
+ offset += 8;
853
+ const scaleZ = view.getFloat64(offset, true);
854
+ offset += 8;
855
+ const vertexCount = view.getUint32(offset, true);
856
+ offset += 4;
857
+ const useFloat32 = (flags & 1) !== 0;
858
+ const deltaEncoded = (flags & 4) !== 0;
859
+ const planarByteSplit = (flags & 32) !== 0;
860
+ const componentCount = vertexCount * 3;
861
+ const verticesByteLength = componentCount * (useFloat32 ? 4 : 2);
862
+ if (offset + verticesByteLength > bytes.byteLength) throw fail("Insufficient data to read vertices.", {
863
+ expectedBytes: verticesByteLength,
864
+ availableBytes: bytes.byteLength - offset,
865
+ offset,
866
+ useFloat32,
867
+ vertexCount
868
+ });
869
+ const absoluteOffset = bytes.byteOffset + offset;
870
+ let vertexData;
871
+ if (useFloat32) vertexData = readFloat32Vertices(bytes.buffer, absoluteOffset, componentCount);
872
+ else if (planarByteSplit) vertexData = bytes.subarray(offset, offset + verticesByteLength);
873
+ else if (deltaEncoded) vertexData = readUint16Array(bytes.buffer, absoluteOffset, componentCount);
874
+ else vertexData = readInt16Vertices(bytes.buffer, absoluteOffset, componentCount);
875
+ offset += verticesByteLength;
876
+ if (offset + 4 > bytes.byteLength) throw fail("Insufficient data to read index count.", {
877
+ expectedBytes: 4,
878
+ availableBytes: bytes.byteLength - offset,
879
+ offset
880
+ });
881
+ const indexCount = view.getUint32(offset, true);
882
+ offset += 4;
883
+ const useUint16Indices = (flags & 2) !== 0;
884
+ const indicesByteLength = indexCount * (useUint16Indices ? 2 : 4);
885
+ if (offset + indicesByteLength > bytes.byteLength) throw fail("Insufficient data to read indices.", {
886
+ expectedBytes: indicesByteLength,
887
+ availableBytes: bytes.byteLength - offset,
888
+ offset,
889
+ indexCount,
890
+ useUint16Indices
891
+ });
892
+ const indexData = planarByteSplit ? bytes.subarray(offset, offset + indicesByteLength) : useUint16Indices ? readUint16Array(bytes.buffer, bytes.byteOffset + offset, indexCount) : readUint32Array(bytes.buffer, bytes.byteOffset + offset, indexCount);
893
+ offset += indicesByteLength;
894
+ let uvs = null;
895
+ if ((flags & 8) !== 0) {
896
+ const parsed = parseUvChunk(bytes, view, offset, vertexCount, deltaEncoded, planarByteSplit);
897
+ uvs = parsed.uvs;
898
+ offset = parsed.offset;
899
+ }
900
+ let colors = null;
901
+ if ((flags & 16) !== 0) colors = parseColorChunk(bytes, offset, vertexCount, deltaEncoded);
902
+ return {
903
+ metadata,
904
+ flags,
905
+ vertexData,
906
+ indexData,
907
+ isFloat32: useFloat32,
908
+ deltaEncoded,
909
+ planarByteSplit,
910
+ uint16Indices: useUint16Indices,
911
+ vertexCount,
912
+ origin: [
913
+ originX,
914
+ originY,
915
+ originZ
916
+ ],
917
+ scale: [
918
+ scaleX,
919
+ scaleY,
920
+ scaleZ
921
+ ],
922
+ uvs,
923
+ colors
924
+ };
925
+ }
926
+ // src/parse/webdisplay/closed-solid.ts
927
+ /**
928
+ * True when every member of a geometry is a closed, consistently wound, outward-facing triangle
929
+ * surface, so its back faces can never be seen and the material can cull them.
930
+ *
931
+ * Drawing them anyway is not just overdraw. A thin closed part (a 0.5 mm sheet) puts its inside
932
+ * faces within one depth-buffer step of its outside ones, and once precision drops below the
933
+ * thickness they punch through as dark jagged triangles. Culled, they never compete.
934
+ *
935
+ * Members are tested apart, because two closed parts touching along an edge share it four ways and
936
+ * would read as open together. Positions are welded first: Rhino meshes each Brep face separately,
937
+ * so a solid's seams carry duplicate vertices that only position identifies. Any
938
+ * doubt (an unwelded seam, a non-manifold edge, inward winding) answers false, which keeps the
939
+ * double-sided default.
940
+ *
941
+ * Self-contained (no outer captures besides `Math`) so the mesh-assembly worker can embed it via
942
+ * `Function.prototype.toString`.
943
+ *
944
+ * @param memberIndexCounts Index count of each member, in order; members are concatenated in
945
+ * `indices`.
946
+ */
947
+ function isClosedSolid(positions, indices, memberIndexCounts) {
948
+ if (memberIndexCounts.length === 0) return false;
949
+ const tableSizeFor = (count) => {
950
+ let size = 16;
951
+ while (size < count * 2) size *= 2;
952
+ return size;
953
+ };
954
+ const vertexCount = positions.length / 3;
955
+ const tag = new Int32Array(vertexCount);
956
+ const canonicalOf = new Int32Array(vertexCount);
957
+ let weldVertex = /* @__PURE__ */ new Int32Array(0);
958
+ let cellX = /* @__PURE__ */ new Int32Array(0);
959
+ let cellY = /* @__PURE__ */ new Int32Array(0);
960
+ let cellZ = /* @__PURE__ */ new Int32Array(0);
961
+ let welded = /* @__PURE__ */ new Int32Array(0);
962
+ let vertexOf = /* @__PURE__ */ new Int32Array(0);
963
+ let outStart = /* @__PURE__ */ new Int32Array(0);
964
+ let outTo = /* @__PURE__ */ new Int32Array(0);
965
+ let cursor = /* @__PURE__ */ new Int32Array(0);
966
+ const grown = (array, length) => array.length >= length ? array : new Int32Array(length);
967
+ const memberIsClosed = (member, start, count) => {
968
+ if (count < 12 || count % 3 !== 0) return false;
969
+ const seen = 2 * member + 1;
970
+ const done = seen + 1;
971
+ let magnitude = 0;
972
+ let uniqueVertices = 0;
973
+ for (let i = 0; i < count; i++) {
974
+ const v = indices[start + i];
975
+ if (tag[v] === seen) continue;
976
+ tag[v] = seen;
977
+ uniqueVertices++;
978
+ magnitude = Math.max(magnitude, Math.abs(positions[v * 3]), Math.abs(positions[v * 3 + 1]), Math.abs(positions[v * 3 + 2]));
979
+ }
980
+ if (!(magnitude > 0) || !Number.isFinite(magnitude)) return false;
981
+ const inverseCell = 1 / (2 * Math.pow(2, Math.floor(Math.log2(magnitude)) - 23));
982
+ const weldSize = tableSizeFor(uniqueVertices);
983
+ const weldMask = weldSize - 1;
984
+ weldVertex = grown(weldVertex, weldSize);
985
+ weldVertex.fill(-1, 0, weldSize);
986
+ cellX = grown(cellX, weldSize);
987
+ cellY = grown(cellY, weldSize);
988
+ cellZ = grown(cellZ, weldSize);
989
+ const cellSlot = (x, y, z) => {
990
+ let h = Math.imul(x, 2654435761) ^ Math.imul(y, 2246822519) ^ Math.imul(z, 3266489917);
991
+ h = (h ^ h >>> 15) & weldMask;
992
+ while (weldVertex[h] !== -1 && (cellX[h] !== x || cellY[h] !== y || cellZ[h] !== z)) h = h + 1 & weldMask;
993
+ return h;
994
+ };
995
+ const neighbour = (scaled, cell) => {
996
+ const offset = scaled - cell;
997
+ return offset > .25 ? cell + 1 : offset < -.25 ? cell - 1 : cell;
998
+ };
999
+ welded = grown(welded, count);
1000
+ vertexOf = grown(vertexOf, uniqueVertices);
1001
+ let canonicals = 0;
1002
+ for (let i = 0; i < count; i++) {
1003
+ const v = indices[start + i];
1004
+ if (tag[v] === done) {
1005
+ welded[i] = canonicalOf[v];
1006
+ continue;
1007
+ }
1008
+ const sx = positions[v * 3] * inverseCell;
1009
+ const sy = positions[v * 3 + 1] * inverseCell;
1010
+ const sz = positions[v * 3 + 2] * inverseCell;
1011
+ const x = Math.round(sx);
1012
+ const y = Math.round(sy);
1013
+ const z = Math.round(sz);
1014
+ const nx = neighbour(sx, x);
1015
+ const ny = neighbour(sy, y);
1016
+ const nz = neighbour(sz, z);
1017
+ let canonical = -1;
1018
+ let ownSlot = -1;
1019
+ for (let combo = 0; combo < 8 && canonical === -1; combo++) {
1020
+ if (combo & 1 && nx === x || combo & 2 && ny === y || combo & 4 && nz === z) continue;
1021
+ const h = cellSlot(combo & 1 ? nx : x, combo & 2 ? ny : y, combo & 4 ? nz : z);
1022
+ if (combo === 0) ownSlot = h;
1023
+ if (weldVertex[h] !== -1) canonical = weldVertex[h];
1024
+ }
1025
+ if (canonical === -1) {
1026
+ const h = ownSlot;
1027
+ canonical = canonicals++;
1028
+ weldVertex[h] = canonical;
1029
+ cellX[h] = x;
1030
+ cellY[h] = y;
1031
+ cellZ[h] = z;
1032
+ vertexOf[canonical] = v;
1033
+ }
1034
+ tag[v] = done;
1035
+ canonicalOf[v] = canonical;
1036
+ welded[i] = canonical;
1037
+ }
1038
+ outStart = grown(outStart, canonicals + 1);
1039
+ outStart.fill(0, 0, canonicals + 1);
1040
+ let triangles = 0;
1041
+ for (let i = 0; i < count; i += 3) {
1042
+ const a = welded[i];
1043
+ const b = welded[i + 1];
1044
+ const c = welded[i + 2];
1045
+ if (a === b || b === c || c === a) continue;
1046
+ outStart[a + 1] += 1;
1047
+ outStart[b + 1] += 1;
1048
+ outStart[c + 1] += 1;
1049
+ triangles++;
1050
+ }
1051
+ if (triangles < 4) return false;
1052
+ for (let k = 0; k < canonicals; k++) outStart[k + 1] += outStart[k];
1053
+ outTo = grown(outTo, outStart[canonicals]);
1054
+ cursor = grown(cursor, canonicals);
1055
+ cursor.set(outStart.subarray(0, canonicals));
1056
+ for (let i = 0; i < count; i += 3) {
1057
+ const a = welded[i];
1058
+ const b = welded[i + 1];
1059
+ const c = welded[i + 2];
1060
+ if (a === b || b === c || c === a) continue;
1061
+ outTo[cursor[a]] = b;
1062
+ cursor[a] += 1;
1063
+ outTo[cursor[b]] = c;
1064
+ cursor[b] += 1;
1065
+ outTo[cursor[c]] = a;
1066
+ cursor[c] += 1;
1067
+ }
1068
+ for (let a = 0; a < canonicals; a++) {
1069
+ const first = outStart[a];
1070
+ const end = outStart[a + 1];
1071
+ for (let k = first; k < end; k++) {
1072
+ const b = outTo[k];
1073
+ for (let j = first; j < k; j++) if (outTo[j] === b) return false;
1074
+ let reversed = false;
1075
+ for (let j = outStart[b], bEnd = outStart[b + 1]; j < bEnd && !reversed; j++) reversed = outTo[j] === a;
1076
+ if (!reversed) return false;
1077
+ }
1078
+ }
1079
+ const o = vertexOf[welded[0]] * 3;
1080
+ const ox = positions[o];
1081
+ const oy = positions[o + 1];
1082
+ const oz = positions[o + 2];
1083
+ let volume = 0;
1084
+ for (let i = 0; i < count; i += 3) {
1085
+ const a = vertexOf[welded[i]] * 3;
1086
+ const b = vertexOf[welded[i + 1]] * 3;
1087
+ const c = vertexOf[welded[i + 2]] * 3;
1088
+ const ax = positions[a] - ox;
1089
+ const ay = positions[a + 1] - oy;
1090
+ const az = positions[a + 2] - oz;
1091
+ const bx = positions[b] - ox;
1092
+ const by = positions[b + 1] - oy;
1093
+ const bz = positions[b + 2] - oz;
1094
+ const cx = positions[c] - ox;
1095
+ const cy = positions[c + 1] - oy;
1096
+ const cz = positions[c + 2] - oz;
1097
+ volume += ax * (by * cz - bz * cy) + ay * (bz * cx - bx * cz) + az * (bx * cy - by * cx);
1098
+ }
1099
+ return volume > 0;
1100
+ };
1101
+ let start = 0;
1102
+ for (let member = 0; member < memberIndexCounts.length; member++) {
1103
+ const count = memberIndexCounts[member];
1104
+ if (!memberIsClosed(member, start, count)) return false;
1105
+ start += count;
1106
+ }
1107
+ return true;
1108
+ }
1109
+ // src/parse/webdisplay/crease-normals.ts
1110
+ /**
1111
+ * Vertex normals that stay smooth across gentle curvature and break at creases, splitting a vertex
1112
+ * wherever the faces sharing it disagree by more than the crease angle.
1113
+ *
1114
+ * The writer welds coincident vertices, and seams between Brep faces often come out welded
1115
+ * across a 90° fold. A plain average there tilts every normal along the fold halfway round it,
1116
+ * and the long thin triangles of a sheet-metal strip carry that tilt metres across a flat face:
1117
+ * streaks, an X around every hole, a whole strip shaded darker than its neighbour.
1118
+ *
1119
+ * Face normals are area-weighted: across a tangent bend the bend's facets are narrow, so they barely
1120
+ * tilt the wide flat face beside them. Weighting by corner angle instead let a bend facet's corner
1121
+ * tilt a flat band by up to 23°, enough to flip a metal's reflection from sky to ground.
1122
+ *
1123
+ * `indices` is rewritten in place to point split corners at their new vertices, which take the
1124
+ * slots of welded-away duplicates before any are appended; the index count never changes, so index
1125
+ * windows stay valid.
1126
+ *
1127
+ * Self-contained (no outer captures besides `Math`) so the mesh-assembly worker can embed it via
1128
+ * `Function.prototype.toString`.
1129
+ */
1130
+ function splitCreases(positions, indices, uvs, colors) {
1131
+ const CREASE_COS = Math.cos(30 * Math.PI / 180);
1132
+ const vertexCount = positions.length / 3;
1133
+ const cornerCount = indices.length;
1134
+ const triangleCount = cornerCount / 3;
1135
+ let orphans = /* @__PURE__ */ new Uint32Array(0);
1136
+ {
1137
+ const posBits = new Uint32Array(positions.buffer, positions.byteOffset, positions.length);
1138
+ const uvBits = uvs ? new Uint32Array(uvs.buffer, uvs.byteOffset, uvs.length) : null;
1139
+ const word = (bits, i) => bits[i] === 2147483648 ? 0 : bits[i];
1140
+ let size = 16;
1141
+ while (size < vertexCount * 2) size *= 2;
1142
+ const mask = size - 1;
1143
+ const slots = new Int32Array(size).fill(-1);
1144
+ const canonical = new Uint32Array(vertexCount);
1145
+ let merged = false;
1146
+ for (let v = 0; v < vertexCount; v++) {
1147
+ const x = word(posBits, v * 3);
1148
+ const y = word(posBits, v * 3 + 1);
1149
+ const z = word(posBits, v * 3 + 2);
1150
+ let h = Math.imul(x, 2654435761) ^ Math.imul(y, 2246822519) ^ Math.imul(z, 3266489917);
1151
+ h = (h ^ h >>> 15) & mask;
1152
+ for (;;) {
1153
+ const other = slots[h];
1154
+ if (other === -1) {
1155
+ slots[h] = v;
1156
+ canonical[v] = v;
1157
+ break;
1158
+ }
1159
+ if (word(posBits, other * 3) === x && word(posBits, other * 3 + 1) === y && word(posBits, other * 3 + 2) === z && (!colors || colors[other * 3] === colors[v * 3] && colors[other * 3 + 1] === colors[v * 3 + 1] && colors[other * 3 + 2] === colors[v * 3 + 2]) && (!uvBits || uvBits[other * 2] === uvBits[v * 2] && uvBits[other * 2 + 1] === uvBits[v * 2 + 1])) {
1160
+ canonical[v] = other;
1161
+ merged = true;
1162
+ break;
1163
+ }
1164
+ h = h + 1 & mask;
1165
+ }
1166
+ }
1167
+ if (merged) {
1168
+ for (let i = 0; i < cornerCount; i++) indices[i] = canonical[indices[i]];
1169
+ let orphanCount = 0;
1170
+ for (let v = 0; v < vertexCount; v++) if (canonical[v] !== v) orphanCount++;
1171
+ orphans = new Uint32Array(orphanCount);
1172
+ for (let v = 0, k = 0; v < vertexCount; v++) if (canonical[v] !== v) orphans[k++] = v;
1173
+ }
1174
+ }
1175
+ const faceNormals = new Float32Array(triangleCount * 3);
1176
+ const faceAreas = new Float32Array(triangleCount);
1177
+ for (let t = 0; t < triangleCount; t++) {
1178
+ const a = indices[t * 3] * 3;
1179
+ const b = indices[t * 3 + 1] * 3;
1180
+ const c = indices[t * 3 + 2] * 3;
1181
+ const abx = positions[b] - positions[a];
1182
+ const aby = positions[b + 1] - positions[a + 1];
1183
+ const abz = positions[b + 2] - positions[a + 2];
1184
+ const acx = positions[c] - positions[a];
1185
+ const acy = positions[c + 1] - positions[a + 1];
1186
+ const acz = positions[c + 2] - positions[a + 2];
1187
+ const nx = aby * acz - abz * acy;
1188
+ const ny = abz * acx - abx * acz;
1189
+ const nz = abx * acy - aby * acx;
1190
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
1191
+ if (length === 0) continue;
1192
+ faceNormals[t * 3] = nx / length;
1193
+ faceNormals[t * 3 + 1] = ny / length;
1194
+ faceNormals[t * 3 + 2] = nz / length;
1195
+ faceAreas[t] = length;
1196
+ }
1197
+ const cornerStart = new Uint32Array(vertexCount + 1);
1198
+ for (let i = 0; i < cornerCount; i++) cornerStart[indices[i] + 1] += 1;
1199
+ for (let v = 0; v < vertexCount; v++) cornerStart[v + 1] += cornerStart[v];
1200
+ const cornersOf = new Uint32Array(cornerCount);
1201
+ const fill = cornerStart.slice(0, vertexCount);
1202
+ for (let i = 0; i < cornerCount; i++) {
1203
+ const v = indices[i];
1204
+ cornersOf[fill[v]] = i;
1205
+ fill[v] += 1;
1206
+ }
1207
+ let maxDegree = 0;
1208
+ for (let v = 0; v < vertexCount; v++) maxDegree = Math.max(maxDegree, cornerStart[v + 1] - cornerStart[v]);
1209
+ const groupOf = new Uint32Array(cornerCount);
1210
+ const groupCount = new Uint32Array(vertexCount);
1211
+ const founders = new Float32Array(Math.max(1, maxDegree) * 3);
1212
+ for (let v = 0; v < vertexCount; v++) {
1213
+ let groups = 0;
1214
+ for (let k = cornerStart[v]; k < cornerStart[v + 1]; k++) {
1215
+ const corner = cornersOf[k];
1216
+ const f = Math.floor(corner / 3) * 3;
1217
+ const nx = faceNormals[f];
1218
+ const ny = faceNormals[f + 1];
1219
+ const nz = faceNormals[f + 2];
1220
+ let group = -1;
1221
+ if (nx === 0 && ny === 0 && nz === 0) group = 0;
1222
+ else for (let g = 0; g < groups; g++) if (founders[g * 3] * nx + founders[g * 3 + 1] * ny + founders[g * 3 + 2] * nz >= CREASE_COS) {
1223
+ group = g;
1224
+ break;
1225
+ }
1226
+ if (group === -1) {
1227
+ group = groups;
1228
+ founders[groups * 3] = nx;
1229
+ founders[groups * 3 + 1] = ny;
1230
+ founders[groups * 3 + 2] = nz;
1231
+ groups++;
1232
+ }
1233
+ groupOf[corner] = group;
1234
+ }
1235
+ groupCount[v] = Math.max(1, groups);
1236
+ }
1237
+ const extraStart = new Uint32Array(vertexCount + 1);
1238
+ for (let v = 0; v < vertexCount; v++) extraStart[v + 1] = extraStart[v] + groupCount[v] - 1;
1239
+ const extra = extraStart[vertexCount];
1240
+ const total = vertexCount + Math.max(0, extra - orphans.length);
1241
+ const copySlot = new Uint32Array(extra);
1242
+ for (let k = 0; k < extra; k++) copySlot[k] = k < orphans.length ? orphans[k] : vertexCount + k - orphans.length;
1243
+ let outPositions = positions;
1244
+ let outUvs = uvs;
1245
+ let outColors = colors;
1246
+ if (extra > 0) {
1247
+ outPositions = new Float32Array(total * 3);
1248
+ outPositions.set(positions);
1249
+ outUvs = uvs ? new Float32Array(total * 2) : null;
1250
+ if (outUvs && uvs) outUvs.set(uvs);
1251
+ outColors = colors ? new Uint8Array(total * 3) : null;
1252
+ if (outColors && colors) outColors.set(colors);
1253
+ for (let v = 0; v < vertexCount; v++) for (let g = 1; g < groupCount[v]; g++) {
1254
+ const copy = copySlot[extraStart[v] + g - 1];
1255
+ outPositions[copy * 3] = positions[v * 3];
1256
+ outPositions[copy * 3 + 1] = positions[v * 3 + 1];
1257
+ outPositions[copy * 3 + 2] = positions[v * 3 + 2];
1258
+ if (outUvs && uvs) {
1259
+ outUvs[copy * 2] = uvs[v * 2];
1260
+ outUvs[copy * 2 + 1] = uvs[v * 2 + 1];
1261
+ }
1262
+ if (outColors && colors) {
1263
+ outColors[copy * 3] = colors[v * 3];
1264
+ outColors[copy * 3 + 1] = colors[v * 3 + 1];
1265
+ outColors[copy * 3 + 2] = colors[v * 3 + 2];
1266
+ }
1267
+ }
1268
+ }
1269
+ const normals = new Float32Array(total * 3);
1270
+ for (let t = 0; t < triangleCount; t++) {
1271
+ const w = faceAreas[t];
1272
+ const nx = faceNormals[t * 3] * w;
1273
+ const ny = faceNormals[t * 3 + 1] * w;
1274
+ const nz = faceNormals[t * 3 + 2] * w;
1275
+ for (let corner = t * 3; corner < t * 3 + 3; corner++) {
1276
+ let target = indices[corner];
1277
+ const g = groupOf[corner];
1278
+ if (g !== 0) {
1279
+ target = copySlot[extraStart[target] + g - 1];
1280
+ indices[corner] = target;
1281
+ }
1282
+ normals[target * 3] += nx;
1283
+ normals[target * 3 + 1] += ny;
1284
+ normals[target * 3 + 2] += nz;
1285
+ }
1286
+ }
1287
+ for (let i = 0; i < normals.length; i += 3) {
1288
+ const x = normals[i];
1289
+ const y = normals[i + 1];
1290
+ const z = normals[i + 2];
1291
+ const length = Math.sqrt(x * x + y * y + z * z) || 1;
1292
+ normals[i] = x / length;
1293
+ normals[i + 1] = y / length;
1294
+ normals[i + 2] = z / length;
1295
+ }
1296
+ return {
1297
+ positions: outPositions,
1298
+ normals,
1299
+ indices,
1300
+ uvs: outUvs,
1301
+ colors: outColors
1302
+ };
1303
+ }
1304
+ // src/parse/webdisplay/grain-tangents.ts
1305
+ /**
1306
+ * Per-vertex tangents for three's `tangent` attribute: xyz along increasing U, w the handedness that
1307
+ * points `cross(normal, tangent)` along increasing V.
1308
+ *
1309
+ * Without the attribute three rebuilds the frame per pixel from screen-space UV derivatives, which
1310
+ * is noisy on long thin triangles. three's own `computeTangents()` needs V to vary on every
1311
+ * triangle and leaves a zero tangent where it doesn't (a tube centred on its grain axis has constant
1312
+ * V), and a zero tangent renders NaN-black. Only U's gradient is needed here, so V can be anything.
1313
+ *
1314
+ * Self-contained (no outer captures besides `Math`) so the mesh-assembly worker can embed it via
1315
+ * `Function.prototype.toString`.
1316
+ */
1317
+ function grainTangents(positions, normals, indices, uvs) {
1318
+ const vertexCount = positions.length / 3;
1319
+ const gradU = new Float32Array(vertexCount * 3);
1320
+ const gradV = new Float32Array(vertexCount * 3);
1321
+ for (let t = 0; t < indices.length; t += 3) {
1322
+ const a = indices[t];
1323
+ const b = indices[t + 1];
1324
+ const c = indices[t + 2];
1325
+ const e1x = positions[b * 3] - positions[a * 3];
1326
+ const e1y = positions[b * 3 + 1] - positions[a * 3 + 1];
1327
+ const e1z = positions[b * 3 + 2] - positions[a * 3 + 2];
1328
+ const e2x = positions[c * 3] - positions[a * 3];
1329
+ const e2y = positions[c * 3 + 1] - positions[a * 3 + 1];
1330
+ const e2z = positions[c * 3 + 2] - positions[a * 3 + 2];
1331
+ const nx = e1y * e2z - e1z * e2y;
1332
+ const ny = e1z * e2x - e1x * e2z;
1333
+ const nz = e1x * e2y - e1y * e2x;
1334
+ const length = Math.sqrt(nx * nx + ny * ny + nz * nz);
1335
+ if (length === 0) continue;
1336
+ const px = (e2y * nz - e2z * ny) / length;
1337
+ const py = (e2z * nx - e2x * nz) / length;
1338
+ const pz = (e2x * ny - e2y * nx) / length;
1339
+ const qx = (ny * e1z - nz * e1y) / length;
1340
+ const qy = (nz * e1x - nx * e1z) / length;
1341
+ const qz = (nx * e1y - ny * e1x) / length;
1342
+ const du1 = uvs[b * 2] - uvs[a * 2];
1343
+ const du2 = uvs[c * 2] - uvs[a * 2];
1344
+ const dv1 = uvs[b * 2 + 1] - uvs[a * 2 + 1];
1345
+ const dv2 = uvs[c * 2 + 1] - uvs[a * 2 + 1];
1346
+ const ux = du1 * px + du2 * qx;
1347
+ const uy = du1 * py + du2 * qy;
1348
+ const uz = du1 * pz + du2 * qz;
1349
+ const vx = dv1 * px + dv2 * qx;
1350
+ const vy = dv1 * py + dv2 * qy;
1351
+ const vz = dv1 * pz + dv2 * qz;
1352
+ for (let k = 0; k < 3; k++) {
1353
+ const v = indices[t + k];
1354
+ gradU[v * 3] += ux;
1355
+ gradU[v * 3 + 1] += uy;
1356
+ gradU[v * 3 + 2] += uz;
1357
+ gradV[v * 3] += vx;
1358
+ gradV[v * 3 + 1] += vy;
1359
+ gradV[v * 3 + 2] += vz;
1360
+ }
1361
+ }
1362
+ const tangents = new Float32Array(vertexCount * 4);
1363
+ for (let v = 0; v < vertexCount; v++) {
1364
+ const nx = normals[v * 3];
1365
+ const ny = normals[v * 3 + 1];
1366
+ const nz = normals[v * 3 + 2];
1367
+ const along = gradU[v * 3] * nx + gradU[v * 3 + 1] * ny + gradU[v * 3 + 2] * nz;
1368
+ let tx = gradU[v * 3] - along * nx;
1369
+ let ty = gradU[v * 3 + 1] - along * ny;
1370
+ let tz = gradU[v * 3 + 2] - along * nz;
1371
+ let length = Math.sqrt(tx * tx + ty * ty + tz * tz);
1372
+ if (!(length > 1e-12)) {
1373
+ const ax = Math.abs(nx) < .9 ? 1 : 0;
1374
+ const ay = 1 - ax;
1375
+ tx = ay * nz;
1376
+ ty = -ax * nz;
1377
+ tz = ax * ny - ay * nx;
1378
+ length = Math.sqrt(tx * tx + ty * ty + tz * tz) || 1;
1379
+ }
1380
+ tx /= length;
1381
+ ty /= length;
1382
+ tz /= length;
1383
+ const bx = ny * tz - nz * ty;
1384
+ const by = nz * tx - nx * tz;
1385
+ const bz = nx * ty - ny * tx;
1386
+ const handedness = bx * gradV[v * 3] + by * gradV[v * 3 + 1] + bz * gradV[v * 3 + 2];
1387
+ tangents[v * 4] = tx;
1388
+ tangents[v * 4 + 1] = ty;
1389
+ tangents[v * 4 + 2] = tz;
1390
+ tangents[v * 4 + 3] = handedness < 0 ? -1 : 1;
1391
+ }
1392
+ return tangents;
1393
+ }
1394
+ // src/parse/webdisplay/mesh-assembly.ts
1395
+ /**
1396
+ * {@link assembleGeometries} is the hot, pure part of batch parsing: undoes the delta filter on
1397
+ * the raw wire arrays, dequantizes int16 positions to world floats, and slices/rebases per-geometry
1398
+ * windows. {@link finishGeometry} then adds normals and the closed-solid flag.
1399
+ * Everything it needs travels as typed arrays, so the whole stage runs in a Worker and the main
1400
+ * thread only wraps the returned buffers into `BufferGeometry` objects.
1401
+ *
1402
+ * Like `edge-extract.ts`, it's a single self-contained function with zero outer captures (only
1403
+ * `Math` and its arguments) so `Function.prototype.toString` yields code that runs unchanged
1404
+ * inside a blob-URL Worker ({@link meshAssemblyWorkerSource}) — bundler-agnostic by construction.
1405
+ * That forces duplicating small helpers from `binary-parser.ts` (unzigzag/delta decode);
1406
+ * equivalence with the synchronous path is pinned by tests.
1407
+ */
1408
+ /**
1409
+ * The shared tail of both parse paths, so the worker and the main-thread fallback can't drift.
1410
+ * Closedness is judged before the crease split, which rewires corners to new vertex slots.
1411
+ * Tangents come after it, so each split copy takes only its own side's faces.
1412
+ */
1413
+ function finishGeometry(geometry, members) {
1414
+ const closed = members.every((m) => typeof m.closed === "boolean") ? members.length > 0 && members.every((m) => m.closed) : isClosedSolid(geometry.positions, geometry.indices, members.map((m) => m.indexCount));
1415
+ const creased = splitCreases(geometry.positions, geometry.indices, geometry.uvs, geometry.colors);
1416
+ const tangents = creased.uvs ? grainTangents(creased.positions, creased.normals, creased.indices, creased.uvs) : null;
1417
+ return {
1418
+ ...creased,
1419
+ closed,
1420
+ tangents
1421
+ };
1422
+ }
1423
+ function assembleGeometries(input) {
1424
+ const { isFloat32, deltaEncoded, planarByteSplit, uint16Indices, origin, scale, uvs, colors, jobs } = input;
1425
+ const unzigzag = (zz) => zz >>> 1 ^ -(zz & 1);
1426
+ let worldVertices;
1427
+ if (isFloat32) worldVertices = input.vertexData;
1428
+ else {
1429
+ let quantized;
1430
+ if (planarByteSplit) {
1431
+ const planes = input.vertexData;
1432
+ const n = planes.length / 6;
1433
+ quantized = new Int16Array(n * 3);
1434
+ let px = 0;
1435
+ let py = 0;
1436
+ let pz = 0;
1437
+ for (let i = 0; i < n; i++) {
1438
+ px = px + unzigzag(planes[i] | planes[n * 3 + i] << 8) << 16 >> 16;
1439
+ py = py + unzigzag(planes[n + i] | planes[n * 4 + i] << 8) << 16 >> 16;
1440
+ pz = pz + unzigzag(planes[n * 2 + i] | planes[n * 5 + i] << 8) << 16 >> 16;
1441
+ quantized[i * 3] = px;
1442
+ quantized[i * 3 + 1] = py;
1443
+ quantized[i * 3 + 2] = pz;
1444
+ }
1445
+ } else if (deltaEncoded) {
1446
+ const zigzagged = input.vertexData;
1447
+ quantized = new Int16Array(zigzagged.length);
1448
+ let px = 0;
1449
+ let py = 0;
1450
+ let pz = 0;
1451
+ for (let i = 0; i < zigzagged.length; i += 3) {
1452
+ px = px + unzigzag(zigzagged[i]) << 16 >> 16;
1453
+ py = py + unzigzag(zigzagged[i + 1]) << 16 >> 16;
1454
+ pz = pz + unzigzag(zigzagged[i + 2]) << 16 >> 16;
1455
+ quantized[i] = px;
1456
+ quantized[i + 1] = py;
1457
+ quantized[i + 2] = pz;
1458
+ }
1459
+ } else quantized = input.vertexData;
1460
+ worldVertices = new Float32Array(quantized.length);
1461
+ const ox = origin[0];
1462
+ const oy = origin[1];
1463
+ const oz = origin[2];
1464
+ const sx = scale[0];
1465
+ const sy = scale[1];
1466
+ const sz = scale[2];
1467
+ for (let i = 0; i < quantized.length; i += 3) {
1468
+ worldVertices[i] = ox + (quantized[i] + 32767) * sx;
1469
+ worldVertices[i + 1] = oy + (quantized[i + 1] + 32767) * sy;
1470
+ worldVertices[i + 2] = oz + (quantized[i + 2] + 32767) * sz;
1471
+ }
1472
+ }
1473
+ let indices;
1474
+ if (planarByteSplit) {
1475
+ const planes = input.indexData;
1476
+ if (uint16Indices) {
1477
+ const count = planes.length / 2;
1478
+ const out = new Uint16Array(count);
1479
+ let prev = 0;
1480
+ for (let i = 0; i < count; i++) {
1481
+ prev = prev + unzigzag(planes[i] | planes[count + i] << 8) & 65535;
1482
+ out[i] = prev;
1483
+ }
1484
+ indices = out;
1485
+ } else {
1486
+ const count = planes.length / 4;
1487
+ const out = new Uint32Array(count);
1488
+ let prev = 0;
1489
+ for (let i = 0; i < count; i++) {
1490
+ const zz = (planes[i] | planes[count + i] << 8 | planes[count * 2 + i] << 16 | planes[count * 3 + i] << 24) >>> 0;
1491
+ prev = prev + unzigzag(zz) >>> 0;
1492
+ out[i] = prev;
1493
+ }
1494
+ indices = out;
1495
+ }
1496
+ } else if (deltaEncoded) {
1497
+ const zigzagged = input.indexData;
1498
+ if (zigzagged instanceof Uint16Array) {
1499
+ const out = new Uint16Array(zigzagged.length);
1500
+ let prev = 0;
1501
+ for (let i = 0; i < zigzagged.length; i++) {
1502
+ prev = prev + unzigzag(zigzagged[i]) & 65535;
1503
+ out[i] = prev;
1504
+ }
1505
+ indices = out;
1506
+ } else {
1507
+ const out = new Uint32Array(zigzagged.length);
1508
+ let prev = 0;
1509
+ for (let i = 0; i < zigzagged.length; i++) {
1510
+ prev = prev + unzigzag(zigzagged[i]) >>> 0;
1511
+ out[i] = prev;
1512
+ }
1513
+ indices = out;
1514
+ }
1515
+ } else indices = input.indexData;
1516
+ const totalVertexCount = worldVertices.length / 3;
1517
+ for (let i = 0; i < indices.length; i++) if (indices[i] >= totalVertexCount) throw new Error(`Index ${indices[i]} out of range of vertexCount ${totalVertexCount}`);
1518
+ const results = [];
1519
+ for (const job of jobs) {
1520
+ let vertexTotal = 0;
1521
+ let indexTotal = 0;
1522
+ for (const window of job.windows) {
1523
+ vertexTotal += window.vertexCount;
1524
+ indexTotal += window.indexCount;
1525
+ }
1526
+ const positions = new Float32Array(vertexTotal * 3);
1527
+ const outIndices = new Uint32Array(indexTotal);
1528
+ const outUvs = uvs ? new Float32Array(vertexTotal * 2) : null;
1529
+ const outColors = colors ? new Uint8Array(vertexTotal * 3) : null;
1530
+ let vertexCursor = 0;
1531
+ let indexCursor = 0;
1532
+ for (const window of job.windows) {
1533
+ const componentStart = window.vertexStart * 3;
1534
+ positions.set(worldVertices.subarray(componentStart, componentStart + window.vertexCount * 3), vertexCursor * 3);
1535
+ if (outUvs && uvs) outUvs.set(uvs.subarray(window.vertexStart * 2, (window.vertexStart + window.vertexCount) * 2), vertexCursor * 2);
1536
+ if (outColors && colors) outColors.set(colors.subarray(componentStart, componentStart + window.vertexCount * 3), vertexCursor * 3);
1537
+ const windowStart = window.vertexStart;
1538
+ const windowEnd = window.vertexStart + window.vertexCount;
1539
+ const shift = vertexCursor - window.vertexStart;
1540
+ for (let i = 0; i < window.indexCount; i++) {
1541
+ const indexValue = indices[window.indexStart + i];
1542
+ if (indexValue < windowStart || indexValue >= windowEnd) throw new Error(`Index ${indexValue} outside vertex window [${windowStart}, ${windowEnd})`);
1543
+ outIndices[indexCursor + i] = indexValue + shift;
1544
+ }
1545
+ vertexCursor += window.vertexCount;
1546
+ indexCursor += window.indexCount;
1547
+ }
1548
+ results.push({
1549
+ positions,
1550
+ indices: outIndices,
1551
+ uvs: outUvs,
1552
+ colors: outColors
1553
+ });
1554
+ }
1555
+ return results;
1556
+ }
1557
+ /**
1558
+ * Worker script running {@link assembleGeometries} and {@link finishGeometry} off the main thread.
1559
+ * Protocol: receives `{id, input}`, replies `{id, geometries}` (each a {@link FinishedGeometry})
1560
+ * with every output buffer transferred, or `{id, error}`. Pinned by a test that evals this source
1561
+ * against a stub `self`.
1562
+ */
1563
+ function meshAssemblyWorkerSource() {
1564
+ return [
1565
+ `const assemble = ${assembleGeometries.toString()};`,
1566
+ `const isClosedSolid = ${isClosedSolid.toString()};`,
1567
+ `const splitCreases = ${splitCreases.toString()};`,
1568
+ `const grainTangents = ${grainTangents.toString()};`,
1569
+ `self.onmessage = (event) => {`,
1570
+ ` const { id, input } = event.data;`,
1571
+ ` try {`,
1572
+ ` const geometries = assemble(input).map((g, i) => {`,
1573
+ ` const windows = input.jobs[i].windows;`,
1574
+ ` const known = windows.every((w) => typeof w.closed === 'boolean');`,
1575
+ ` const closed = known`,
1576
+ ` ? windows.length > 0 && windows.every((w) => w.closed)`,
1577
+ ` : isClosedSolid(g.positions, g.indices, windows.map((w) => w.indexCount));`,
1578
+ ` const c = splitCreases(g.positions, g.indices, g.uvs, g.colors);`,
1579
+ ` const tangents = c.uvs ? grainTangents(c.positions, c.normals, c.indices, c.uvs) : null;`,
1580
+ ` return { ...c, closed, tangents };`,
1581
+ ` });`,
1582
+ ` const transfer = [];`,
1583
+ ` for (const g of geometries) {`,
1584
+ ` transfer.push(g.positions.buffer, g.normals.buffer, g.indices.buffer);`,
1585
+ ` if (g.uvs) transfer.push(g.uvs.buffer);`,
1586
+ ` if (g.tangents) transfer.push(g.tangents.buffer);`,
1587
+ ` if (g.colors) transfer.push(g.colors.buffer);`,
1588
+ ` }`,
1589
+ ` self.postMessage({ id, geometries }, transfer);`,
1590
+ ` } catch (error) {`,
1591
+ ` self.postMessage({ id, error: String((error && error.message) || error) });`,
1592
+ ` }`,
1593
+ `};`
1594
+ ].join("\n");
1595
+ }
1596
+ let assemblyWorker;
1597
+ const pendingAssemblies = /* @__PURE__ */ new Map();
1598
+ let nextAssemblyRequestId = 1;
1599
+ function getAssemblyWorker() {
1600
+ if (assemblyWorker !== void 0) return assemblyWorker;
1601
+ if (typeof Worker === "undefined" || typeof Blob === "undefined" || typeof URL === "undefined" || typeof URL.createObjectURL !== "function") {
1602
+ assemblyWorker = null;
1603
+ return null;
1604
+ }
1605
+ try {
1606
+ const url = URL.createObjectURL(new Blob([meshAssemblyWorkerSource()], { type: "text/javascript" }));
1607
+ const worker = new Worker(url);
1608
+ worker.onmessage = (event) => {
1609
+ const { id, geometries, error } = event.data;
1610
+ const pending = pendingAssemblies.get(id);
1611
+ if (!pending) return;
1612
+ pendingAssemblies.delete(id);
1613
+ if (geometries) pending.resolve(geometries);
1614
+ else pending.reject(new Error(error ?? "mesh assembly failed in worker"));
1615
+ };
1616
+ worker.onerror = () => {
1617
+ for (const pending of pendingAssemblies.values()) pending.reject(/* @__PURE__ */ new Error("mesh assembly worker crashed"));
1618
+ pendingAssemblies.clear();
1619
+ worker.terminate();
1620
+ assemblyWorker = null;
1621
+ };
1622
+ assemblyWorker = worker;
1623
+ } catch {
1624
+ assemblyWorker = null;
1625
+ }
1626
+ return assemblyWorker;
1627
+ }
1628
+ function requestAssembly(worker, input, transfer) {
1629
+ return new Promise((resolve, reject) => {
1630
+ const id = nextAssemblyRequestId++;
1631
+ pendingAssemblies.set(id, {
1632
+ resolve,
1633
+ reject
1634
+ });
1635
+ worker.postMessage({
1636
+ id,
1637
+ input
1638
+ }, transfer);
1639
+ });
1640
+ }
1641
+ // src/parse/webdisplay/apply-texture.ts
1642
+ /**
1643
+ * Anisotropic-filtering samples applied to material textures, keeping them sharp at grazing angles
1644
+ * instead of blurring. Ceiling is hardware-defined (`renderer.capabilities.getMaxAnisotropy()`,
1645
+ * typically 16). Defaults to three's default (1 — no anisotropy) until a renderer reports in.
1646
+ */
1647
+ let maxAnisotropy = 1;
1648
+ /**
1649
+ * Subscribed to the renderer's own report below, so no host wiring is needed; still exported for a
1650
+ * host embedding a foreign renderer that wants to set it directly. Applies to textures loaded from
1651
+ * here on — textures already decoded keep the value they were given.
1652
+ */
1653
+ function setTextureAnisotropy(value) {
1654
+ maxAnisotropy = Math.max(1, value);
1655
+ }
1656
+ require_gpu_dispose.observeMaxAnisotropy(setTextureAnisotropy);
1657
+ /**
1658
+ * Assigns a texture to `material[slot]` once fetched and decoded — the mesh renders without it for
1659
+ * the first frames. Load failures log a warning and leave the slot empty rather than breaking the
1660
+ * batch.
1661
+ *
1662
+ * Each call loads independently: no caching, no cross-material sharing. The texture is owned by the
1663
+ * material it is assigned to, so the scene's normal dispose walk frees it like any other resource.
1664
+ */
1665
+ function applyTexture(material, url, slot, mapSize) {
1666
+ if (typeof document === "undefined") return;
1667
+ new three.TextureLoader().load(url, (texture) => {
1668
+ texture.colorSpace = slot === "map" ? three.SRGBColorSpace : three.NoColorSpace;
1669
+ texture.anisotropy = maxAnisotropy;
1670
+ if (mapSize) {
1671
+ const image = texture.image;
1672
+ const aspect = image?.width && image.height ? image.height / image.width : 1;
1673
+ texture.wrapS = three.RepeatWrapping;
1674
+ texture.wrapT = three.RepeatWrapping;
1675
+ texture.repeat.set(1 / mapSize, 1 / (mapSize * aspect));
1676
+ }
1677
+ material[slot] = texture;
1678
+ material.needsUpdate = true;
1679
+ }, void 0, (error) => {
1680
+ require_gpu_dispose.getLogger().warn(`Failed to load material ${slot} ${url}:`, error);
1681
+ });
1682
+ }
1683
+ // src/parse/webdisplay/batch/surface-finish.ts
1684
+ /**
1685
+ * Procedural surfaces, so a preset looks like its material with no texture files to ship or upload.
1686
+ * UVs are millimetres, so a brick is brick-sized at any model scale. Each recipe sets `tone` (albedo
1687
+ * multiplier), `gloss` (roughness multiplier) and `height` (relief in metres, to match the viewer).
1688
+ * Every pattern goes through `selvaDetail`, colour included: a noise cell or a brick under a pixel
1689
+ * samples to static, so a whole building has to settle to its flat colour.
1690
+ */
1691
+ const RECIPES$1 = {
1692
+ wood: `
1693
+ float warp = selvaFbm( uv * vec2( 0.003, 0.02 ) );
1694
+ float rings = 0.5 + 0.5 * sin( ( uv.y + warp * 90.0 ) * 0.28 );
1695
+ float fibre = selvaNoise( uv * vec2( 0.03, 1.4 ) );
1696
+ float grain = ( rings * 0.65 + fibre * 0.35 ) - 0.5;
1697
+ grain *= selvaDetail( 4.0 );
1698
+ tone = vec3( 1.0 + grain * 0.42 );
1699
+ gloss = 1.0 + grain * 0.35;
1700
+ height = fibre * 0.00012 * selvaDetail( 1.0 );`,
1701
+ concrete: `
1702
+ float blotch = ( selvaFbm( uv * 0.0035 ) - 0.5 ) * selvaDetail( 40.0 );
1703
+ float holes = smoothstep( 0.8, 0.88, selvaNoise( uv * 0.22 ) ) * selvaDetail( 5.0 );
1704
+ float speck = selvaNoise( uv * 0.9 ) - 0.5;
1705
+ tone = vec3( 1.0 + blotch * 0.26 + speck * 0.05 * selvaDetail( 1.0 ) - holes * 0.2 );
1706
+ gloss = 1.0 + blotch * 0.3;
1707
+ height = -holes * 0.0005;`,
1708
+ plaster: `
1709
+ float trowel = 0.5 + ( selvaFbm( uv * 0.05 ) - 0.5 ) * selvaDetail( 6.0 );
1710
+ tone = vec3( 1.0 + ( trowel - 0.5 ) * 0.07 );
1711
+ gloss = 1.0 + ( trowel - 0.5 ) * 0.2;
1712
+ height = trowel * 0.0007;`,
1713
+ brick: `
1714
+ vec2 course = vec2( 225.0, 75.0 );
1715
+ float row = floor( uv.y / course.y );
1716
+ vec2 laid = vec2( uv.x + mod( row, 2.0 ) * course.x * 0.5, uv.y );
1717
+ vec2 id = floor( laid / course );
1718
+ vec2 edge = abs( fract( laid / course ) - 0.5 ) * course;
1719
+ float joint = 1.0 - smoothstep( 0.0, 1.5, min( course.x * 0.5 - 5.0 - edge.x, course.y * 0.5 - 5.0 - edge.y ) );
1720
+ joint *= selvaDetail( 10.0 );
1721
+ float face = 1.0 + ( ( selvaHash( id ) - 0.5 ) * 0.48 + ( selvaFbm( uv * 0.05 ) - 0.5 ) * 0.25 ) * selvaDetail( 65.0 );
1722
+ tone = mix( vec3( face ), vec3( 1.5, 1.7, 1.8 ), joint );
1723
+ gloss = 1.0 + joint * 0.15;
1724
+ height = -joint * 0.005;`,
1725
+ corten: `
1726
+ float bloom = 0.5 + ( selvaFbm( uv * vec2( 0.006, 0.0025 ) ) - 0.5 ) * selvaDetail( 25.0 );
1727
+ float scale = 0.5 + ( selvaFbm( uv * 0.06 ) - 0.5 ) * selvaDetail( 8.0 );
1728
+ tone = mix( vec3( 0.62, 0.52, 0.46 ), vec3( 1.28, 1.08, 0.9 ), smoothstep( 0.32, 0.68, bloom ) );
1729
+ tone *= 1.0 + ( scale - 0.5 ) * 0.3;
1730
+ gloss = 1.0 + ( scale - 0.5 ) * 0.25;
1731
+ height = scale * 0.0004;`
1732
+ };
1733
+ const f$1 = (n) => n.toFixed(4);
1734
+ /** Null for the finishes `finish.ts` draws itself. */
1735
+ function surfacePatch(finish, strength) {
1736
+ const recipe = RECIPES$1[finish];
1737
+ if (!recipe || !(strength > 0)) return null;
1738
+ const patch = (shader) => {
1739
+ shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
1740
+ float selvaFootprint;
1741
+ // Hash without sine: sin() loses precision on the large arguments mm UVs produce.
1742
+ float selvaHash( vec2 p ) {
1743
+ vec3 p3 = fract( vec3( p.xyx ) * 0.1031 );
1744
+ p3 += dot( p3, p3.yzx + 33.33 );
1745
+ return fract( ( p3.x + p3.y ) * p3.z );
1746
+ }
1747
+ float selvaNoise( vec2 p ) {
1748
+ vec2 i = floor( p );
1749
+ vec2 f = fract( p );
1750
+ vec2 s = f * f * ( 3.0 - 2.0 * f );
1751
+ return mix(
1752
+ mix( selvaHash( i ), selvaHash( i + vec2( 1.0, 0.0 ) ), s.x ),
1753
+ mix( selvaHash( i + vec2( 0.0, 1.0 ) ), selvaHash( i + vec2( 1.0, 1.0 ) ), s.x ),
1754
+ s.y
1755
+ );
1756
+ }
1757
+ float selvaFbm( vec2 p ) {
1758
+ float sum = 0.0;
1759
+ float amplitude = 0.5;
1760
+ for ( int i = 0; i < 4; i ++ ) {
1761
+ sum += amplitude * selvaNoise( p );
1762
+ p = p * 2.03 + vec2( 17.0, 9.0 );
1763
+ amplitude *= 0.5;
1764
+ }
1765
+ return sum / 0.9375;
1766
+ }
1767
+ float selvaDetail( float featureMm ) {
1768
+ return 1.0 - smoothstep( 0.5, 1.5, selvaFootprint / featureMm );
1769
+ }`).replace("#include <color_fragment>", `#include <color_fragment>
1770
+ selvaFootprint = max( max( fwidth( vUv.x ), fwidth( vUv.y ) ), 1e-6 );
1771
+ vec2 uv = vUv;
1772
+ vec3 tone = vec3( 1.0 );
1773
+ float gloss = 1.0;
1774
+ float height = 0.0;
1775
+ ${recipe}
1776
+ diffuseColor.rgb *= mix( vec3( 1.0 ), tone, ${f$1(strength)} );`).replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
1777
+ roughnessFactor = clamp( roughnessFactor * mix( 1.0, gloss, ${f$1(strength)} ), 0.0, 1.0 );`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
1778
+ {
1779
+ // Bump from screen derivatives, as three's perturbNormalArb.
1780
+ vec3 dx = dFdx( - vViewPosition );
1781
+ vec3 dy = dFdy( - vViewPosition );
1782
+ vec3 r1 = cross( dy, normal );
1783
+ vec3 r2 = cross( normal, dx );
1784
+ float det = dot( dx, r1 ) * faceDirection;
1785
+ vec3 grad = sign( det ) * ( dFdx( height ) * r1 + dFdy( height ) * r2 ) * ${f$1(strength)};
1786
+ normal = normalize( abs( det ) * normal - grad );
1787
+ }`);
1788
+ };
1789
+ return {
1790
+ key: `surface:${finish}:${f$1(strength)}`,
1791
+ patch
1792
+ };
1793
+ }
1794
+ // src/parse/webdisplay/batch/finish.ts
1795
+ const RECIPES = {
1796
+ brushed: {
1797
+ fibres: [{
1798
+ pitch: .04,
1799
+ length: [2, 12],
1800
+ depth: .3
1801
+ }, {
1802
+ pitch: .25,
1803
+ length: [5, 40],
1804
+ depth: .18
1805
+ }],
1806
+ bands: []
1807
+ },
1808
+ rolled: {
1809
+ fibres: [{
1810
+ pitch: .3,
1811
+ length: [80, 600],
1812
+ depth: .07
1813
+ }],
1814
+ bands: [{
1815
+ width: 20,
1816
+ length: 1500,
1817
+ depth: .05
1818
+ }]
1819
+ }
1820
+ };
1821
+ const TILT = .6;
1822
+ const FIBRE_FADE = .35;
1823
+ const f = (n) => n.toFixed(4);
1824
+ /** The shader patch for a finish, or null when it would draw nothing. `key` extends the program cache key. */
1825
+ function finishPatch(finish, strength) {
1826
+ const recipe = RECIPES[finish];
1827
+ if (!recipe) return surfacePatch(finish, strength);
1828
+ if (!(strength > 0)) return null;
1829
+ const fibres = recipe.fibres.map((fb, i) => `selvaFibres( vUv, footprint, ${f(fb.pitch)}, vec2( ${f(fb.length[0])}, ${f(fb.length[1])} ), ${f(fb.depth)}, ${f(i * 31 + 3)} )`).join(" + ") || "0.0";
1830
+ const bands = recipe.bands.map((b) => `selvaBand( vUv, footprint, vec2( ${f(b.length)}, ${f(b.width)} ), ${f(b.depth)} )`).join(" + ") || "0.0";
1831
+ const patch = (shader) => {
1832
+ shader.fragmentShader = shader.fragmentShader.replace("#include <common>", `#include <common>
3
1833
  // Hash without sine: sin() loses precision on the large arguments mm UVs produce.
4
1834
  float selvaHash( vec2 p ) {
5
1835
  vec3 p3 = fract( vec3( p.xyx ) * 0.1031 );
@@ -34,20 +1864,20 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=requi
34
1864
  float selvaFibres( vec2 uv, float footprint, float pitch, vec2 len, float depth, float seed ) {
35
1865
  float level = clamp( log2( footprint / pitch ) + 0.5, 0.0, 14.0 );
36
1866
  float l0 = floor( level );
37
- float a = selvaFibreRow( uv, pitch * exp2( l0 ), len, seed + 7.0 * l0 ) * exp2( -${Y(he)} * l0 );
1867
+ float a = selvaFibreRow( uv, pitch * exp2( l0 ), len, seed + 7.0 * l0 ) * exp2( -${f(FIBRE_FADE)} * l0 );
38
1868
  float b = selvaFibreRow( uv, pitch * exp2( l0 + 1.0 ), len, seed + 7.0 * l0 + 7.0 )
39
- * exp2( -${Y(he)} * ( l0 + 1.0 ) );
1869
+ * exp2( -${f(FIBRE_FADE)} * ( l0 + 1.0 ) );
40
1870
  return depth * mix( a, b, level - l0 );
41
1871
  }
42
1872
  float selvaBand( vec2 uv, float footprint, vec2 size, float depth ) {
43
1873
  float fade = 1.0 - smoothstep( 0.5, 1.0, footprint / size.y );
44
1874
  return depth * fade * ( selvaNoise( uv / size ) * 2.0 - 1.0 );
45
- }`).replace(`#include <color_fragment>`,`#include <color_fragment>
1875
+ }`).replace("#include <color_fragment>", `#include <color_fragment>
46
1876
  float footprint = max( fwidth( vUv.y ), 1e-6 );
47
- float selvaFibre = ( ${r} ) * ${Y(t)};
48
- float selvaStreak = selvaFibre + ( ${i} ) * ${Y(t)};
49
- diffuseColor.rgb *= 1.0 + 0.5 * selvaStreak;`).replace(`#include <roughnessmap_fragment>`,`#include <roughnessmap_fragment>
50
- roughnessFactor = clamp( roughnessFactor * ( 1.0 + selvaStreak ), 0.0, 1.0 );`).replace(`#include <normal_fragment_maps>`,`#include <normal_fragment_maps>
1877
+ float selvaFibre = ( ${fibres} ) * ${f(strength)};
1878
+ float selvaStreak = selvaFibre + ( ${bands} ) * ${f(strength)};
1879
+ diffuseColor.rgb *= 1.0 + 0.5 * selvaStreak;`).replace("#include <roughnessmap_fragment>", `#include <roughnessmap_fragment>
1880
+ roughnessFactor = clamp( roughnessFactor * ( 1.0 + selvaStreak ), 0.0, 1.0 );`).replace("#include <normal_fragment_maps>", `#include <normal_fragment_maps>
51
1881
  #ifdef USE_ANISOTROPY
52
1882
  vec3 selvaAcross = tbn[ 1 ];
53
1883
  #else
@@ -57,12 +1887,960 @@ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=requi
57
1887
  vec3 selvaAcross = cross( selvaQ1, normal ) * dFdx( vUv ).y + cross( normal, selvaQ0 ) * dFdy( vUv ).y;
58
1888
  selvaAcross *= inversesqrt( max( dot( selvaAcross, selvaAcross ), 1e-12 ) );
59
1889
  #endif
60
- normal = normalize( normal + ${Y(.6)} * selvaFibre * selvaAcross );`)}}}function _e(e,t){return t?.appearance?.cullBackfaces?!0:(t?.closed??!1)&&!e.transparent&&e.opacity>=1}function ve(e,t){let n=new Map;return(r,i)=>{let a=e[r],o=_e(a,{...t,closed:i}),s=`${r}:${o}`,c=n.get(s);return c||(c=ye(a,{...t,closed:o}),n.set(s,c)),c}}function ye(e,r){let i=t.o(e.color),a=r?.vertexColors??!1,o=r?.appearance,s=new n.MeshPhysicalMaterial({color:i,metalness:e.metalness,roughness:e.roughness,opacity:e.opacity,transparent:e.transparent,vertexColors:a,side:_e(e,r)?n.FrontSide:n.DoubleSide,polygonOffset:!0,polygonOffsetFactor:.25,polygonOffsetUnits:0,depthWrite:!0,depthTest:!0});e.envMapIntensity==null?o?.envMapIntensity!=null&&(s.envMapIntensity=t.u(o.envMapIntensity,e.metalness)):(s.envMapIntensity=e.envMapIntensity,s.userData[t.l]=e.envMapIntensity),e.clearcoat!=null&&(s.clearcoat=e.clearcoat,s.clearcoatRoughness=e.clearcoatRoughness??0),e.anisotropy!=null&&e.anisotropy>0&&r?.hasUvs&&(s.anisotropy=e.anisotropy,s.anisotropyRotation=e.anisotropyRotation??0),e.transmission!=null&&(s.transmission=e.transmission,s.thickness=0),e.ior!=null&&(s.ior=e.ior),a&&xe(s);let c=e.finish&&r?.hasUvs?ge(e.finish,e.finishStrength??1):null;c&&(s.defines={...s.defines,USE_UV:``},be(s,c.key,c.patch));let{mapSize:l}=e;return e.map&&J(s,e.map,`map`,l),e.roughnessMap&&J(s,e.roughnessMap,`roughnessMap`,l),e.normalMap&&J(s,e.normalMap,`normalMap`,l),s}function be(e,t,n){let r=e.onBeforeCompile,i=e.customProgramCacheKey.bind(e);e.onBeforeCompile=(t,i)=>{r.call(e,t,i),n(t)},e.customProgramCacheKey=()=>`${i()}|${t}`}function xe(e){be(e,`srgb-vertex-colors`,e=>{e.vertexShader=e.vertexShader.replace(`#include <color_vertex>`,`#include <color_vertex>
1890
+ normal = normalize( normal + ${f(TILT)} * selvaFibre * selvaAcross );`);
1891
+ };
1892
+ return {
1893
+ key: `finish:${finish}:${f(strength)}`,
1894
+ patch
1895
+ };
1896
+ }
1897
+ // src/parse/webdisplay/batch/materials.ts
1898
+ const SURFACE_OFFSET_FACTOR = .25;
1899
+ function cullsBackFaces(matData, options) {
1900
+ if (options?.appearance?.cullBackfaces) return true;
1901
+ return (options?.closed ?? false) && !matData.transparent && matData.opacity >= 1;
1902
+ }
1903
+ function createMaterialPicker(materialsSrc, options) {
1904
+ const built = /* @__PURE__ */ new Map();
1905
+ return (materialId, closed) => {
1906
+ const matData = materialsSrc[materialId];
1907
+ const culled = cullsBackFaces(matData, {
1908
+ ...options,
1909
+ closed
1910
+ });
1911
+ const key = `${materialId}:${culled}`;
1912
+ let material = built.get(key);
1913
+ if (!material) {
1914
+ material = createMaterial(matData, {
1915
+ ...options,
1916
+ closed: culled
1917
+ });
1918
+ built.set(key, material);
1919
+ }
1920
+ return material;
1921
+ };
1922
+ }
1923
+ function createMaterial(matData, options) {
1924
+ const color = require_gpu_dispose.parseColor(matData.color);
1925
+ const vertexColors = options?.vertexColors ?? false;
1926
+ const appearance = options?.appearance;
1927
+ const material = new three.MeshPhysicalMaterial({
1928
+ color,
1929
+ metalness: matData.metalness,
1930
+ roughness: matData.roughness,
1931
+ opacity: matData.opacity,
1932
+ transparent: matData.transparent,
1933
+ vertexColors,
1934
+ side: cullsBackFaces(matData, options) ? three.FrontSide : three.DoubleSide,
1935
+ polygonOffset: true,
1936
+ polygonOffsetFactor: SURFACE_OFFSET_FACTOR,
1937
+ polygonOffsetUnits: 0,
1938
+ depthWrite: true,
1939
+ depthTest: true
1940
+ });
1941
+ if (matData.envMapIntensity != null) {
1942
+ material.envMapIntensity = matData.envMapIntensity;
1943
+ material.userData[require_gpu_dispose.OWN_ENV_MAP_INTENSITY] = matData.envMapIntensity;
1944
+ } else if (appearance?.envMapIntensity != null) material.envMapIntensity = require_gpu_dispose.lookEnvMapIntensity(appearance.envMapIntensity, matData.metalness);
1945
+ if (matData.emissive != null) {
1946
+ material.emissive.set(matData.emissive);
1947
+ material.emissiveIntensity = matData.emissiveIntensity ?? 1;
1948
+ material.userData[require_gpu_dispose.OWN_EMISSIVE] = true;
1949
+ if (matData.emissiveLight != null) material.userData[require_gpu_dispose.EMISSIVE_LIGHT] = matData.emissiveLight;
1950
+ }
1951
+ if (matData.clearcoat != null) {
1952
+ material.clearcoat = matData.clearcoat;
1953
+ material.clearcoatRoughness = matData.clearcoatRoughness ?? 0;
1954
+ }
1955
+ if (matData.anisotropy != null && matData.anisotropy > 0 && options?.hasUvs) {
1956
+ material.anisotropy = matData.anisotropy;
1957
+ material.anisotropyRotation = matData.anisotropyRotation ?? 0;
1958
+ }
1959
+ if (matData.transmission != null) {
1960
+ material.transmission = matData.transmission;
1961
+ material.thickness = 0;
1962
+ }
1963
+ if (matData.ior != null) material.ior = matData.ior;
1964
+ if (vertexColors) applyVertexColorSRGBDecode(material);
1965
+ const finish = matData.finish && options?.hasUvs ? finishPatch(matData.finish, matData.finishStrength ?? 1) : null;
1966
+ if (finish) {
1967
+ material.defines = {
1968
+ ...material.defines,
1969
+ USE_UV: ""
1970
+ };
1971
+ addShaderPatch(material, finish.key, finish.patch);
1972
+ }
1973
+ const { mapSize } = matData;
1974
+ if (matData.map) applyTexture(material, matData.map, "map", mapSize);
1975
+ if (matData.roughnessMap) applyTexture(material, matData.roughnessMap, "roughnessMap", mapSize);
1976
+ if (matData.normalMap) applyTexture(material, matData.normalMap, "normalMap", mapSize);
1977
+ return material;
1978
+ }
1979
+ /**
1980
+ * Chains `patch` after any earlier one. three caches compiled programs by `customProgramCacheKey`,
1981
+ * which defaults to the `onBeforeCompile` source; a chained wrapper's source is the same whatever
1982
+ * it wraps, so every patch must extend the key or two materials would share one program.
1983
+ */
1984
+ function addShaderPatch(material, key, patch) {
1985
+ const previous = material.onBeforeCompile;
1986
+ const previousKey = material.customProgramCacheKey.bind(material);
1987
+ material.onBeforeCompile = (shader, renderer) => {
1988
+ previous.call(material, shader, renderer);
1989
+ patch(shader);
1990
+ };
1991
+ material.customProgramCacheKey = () => `${previousKey()}|${key}`;
1992
+ }
1993
+ /**
1994
+ * three.js uploads vertex colors verbatim and multiplies them straight into linear working space
1995
+ * (unlike textures, which carry a `colorSpace` and get decoded) — so sRGB-authored vertex colors
1996
+ * render too bright without this shader patch. Done on the GPU rather than a CPU pass over the
1997
+ * buffer, to keep the hot per-solve parse cheap.
1998
+ */
1999
+ function applyVertexColorSRGBDecode(material) {
2000
+ addShaderPatch(material, "srgb-vertex-colors", (shader) => {
2001
+ shader.vertexShader = shader.vertexShader.replace("#include <color_vertex>", `#include <color_vertex>
61
2002
  #if defined( USE_COLOR ) || defined( USE_COLOR_ALPHA )
62
2003
  vColor.rgb = mix(
63
2004
  vColor.rgb / 12.92,
64
2005
  pow( ( vColor.rgb + 0.055 ) / 1.055, vec3( 2.4 ) ),
65
2006
  step( vec3( 0.04045 ), vColor.rgb )
66
2007
  );
67
- #endif`)})}function X(e,n){return new t.g(e,t.h.VALIDATION_ERROR,{context:n})}function Se(e,t,n,r){for(let i of e){if(!Number.isInteger(i.materialId)||i.materialId<0||i.materialId>=t)throw X(`Group materialId out of range of the materials array.`,{materialId:i.materialId,materialCount:t});for(let e of i.meshes){let t={vertexStart:e.vertexStart,vertexCount:e.vertexCount,indexStart:e.indexStart,indexCount:e.indexCount};for(let[n,r]of Object.entries(t))if(!Number.isInteger(r)||r<0)throw X(`Mesh metadata field "${n}" must be a non-negative integer.`,{meshName:e.name,field:n,value:r});if(e.vertexStart+e.vertexCount>n)throw X(`Mesh vertex window exceeds the batch vertex buffer.`,{meshName:e.name,vertexStart:e.vertexStart,vertexCount:e.vertexCount,totalVertexCount:n});if(e.indexStart+e.indexCount>r)throw X(`Mesh index window exceeds the batch index buffer.`,{meshName:e.name,indexStart:e.indexStart,indexCount:e.indexCount,totalIndexCount:r})}}}function Ce(e,t){return X(`Index references a vertex outside its mesh's vertex window.`,{meshName:t.name,indexValue:e,vertexStart:t.vertexStart,vertexCount:t.vertexCount})}function we(e,t,n){let r=new Float32Array(e.length),i=t[0],a=t[1],o=t[2],s=n[0],c=n[1],l=n[2];for(let t=0;t<e.length;t+=3)r[t]=i+(e[t]+32767)*s,r[t+1]=a+(e[t+1]+32767)*c,r[t+2]=o+(e[t+2]+32767)*l;return r}function Te(e){let t=new Map;for(let n of e.meshes){let e=n.layer??``,r=t.get(e);r||(r=[],t.set(e,r)),r.push(n)}return[...t.values()].map(t=>({materialId:e.materialId,meshes:t}))}function Ee(e,t,n,r,i=null,a=null){let o=0,s=0;for(let t of e.meshes)o+=t.vertexCount,s+=t.indexCount;let c=new Float32Array(o*3),l=new Uint32Array(s),u=i?new Float32Array(o*2):null,d=a?new Uint8Array(o*3):null,f=0,p=0;for(let r of e.meshes){let e=r.vertexStart*3,o=r.vertexCount*3;if(c.set(t.subarray(e,e+o),f*3),u&&i){let e=r.vertexStart*2;u.set(i.subarray(e,e+r.vertexCount*2),f*2)}d&&a&&d.set(a.subarray(e,e+o),f*3);let s=n.subarray(r.indexStart,r.indexStart+r.indexCount),m=f-r.vertexStart,h=r.vertexStart,g=r.vertexStart+r.vertexCount;for(let e=0;e<s.length;e++){let t=s[e];if(t<h||t>=g)throw Ce(t,r);l[p+e]=t+m}f+=r.vertexCount,p+=r.indexCount}let m=U({positions:c,indices:l,uvs:u,colors:d},e.meshes.map(e=>e.indexCount));return Oe(Z(m),e,r(e.materialId,m.closed))}function Z(e){let t=new n.BufferGeometry;return t.setAttribute(`position`,new n.BufferAttribute(e.positions,3)),t.setAttribute(`normal`,new n.BufferAttribute(e.normals,3)),t.setIndex(new n.BufferAttribute(e.indices,1)),e.uvs&&t.setAttribute(`uv`,new n.BufferAttribute(e.uvs,2)),e.tangents&&t.setAttribute(`tangent`,new n.BufferAttribute(e.tangents,4)),e.colors&&t.setAttribute(`color`,new n.BufferAttribute(e.colors,3,!0)),t}function De(e){let t=[],n=0;for(let r of e.meshes)t.push({trackingKey:r.id,name:r.name,layer:r.layer,metadata:r.metadata??{},indexStart:n,indexCount:r.indexCount}),n+=r.indexCount;return t}function Oe(e,t,r){let i=new n.Mesh(e,r),a=t.meshes[0],o=t.meshes.map(e=>e.name).filter(e=>e&&e.length>0);return i.name=o.length>0?o[0]:`merged_material_${t.materialId}`,i.castShadow=!0,i.receiveShadow=!0,i.userData={source:`compute`,name:i.name,layer:a?.layer??``,metadata:a?.metadata??{},members:De(t)},i}function ke(e,t,n,r,i=null,a=null){let o=[];for(let s of e.meshes){let c=s.vertexStart*3,l=s.vertexCount*3,u=t.slice(c,c+l),d=n.subarray(s.indexStart,s.indexStart+s.indexCount),f=new Uint32Array(d.length),p=s.vertexStart,m=s.vertexStart+s.vertexCount;for(let e=0;e<d.length;e++){let t=d[e];if(t<p||t>=m)throw Ce(t,s);f[e]=t-p}let h=s.vertexStart*2,g=U({positions:u,indices:f,uvs:i?i.slice(h,h+s.vertexCount*2):null,colors:a?a.slice(c,c+l):null},[f.length]);o.push(Ae(Z(g),s,r(e.materialId,g.closed)))}return o}function Ae(e,t,r){let i=new n.Mesh(e,r);return i.name=t.name,i.userData={source:`compute`,name:t.name,layer:t.layer??``,trackingKey:t.id,metadata:t.metadata??{}},i.castShadow=!0,i.receiveShadow=!0,i}async function je(e,t,n){let{mergeByMaterial:r=!0,debug:i=!1,material:a}=t??{},{parseTime:o=0,perfStart:s=i?performance.now():0}=n??{};if(!e.compressedData)return[];let c=await Ie(e.compressedData,{mergeByMaterial:r,debug:i,material:a,fallback:{materials:e.materials,groups:e.groups}});if(c)return c;let l=performance.now();return Fe(oe(e.compressedData),{mergeByMaterial:r,debug:i,material:a,parseTime:o,decodeTime:performance.now()-l,perfStart:s,blobBytes:i?Le(e.compressedData):0,fallback:{materials:e.materials,groups:e.groups}})}async function Me(e,t){let{mergeByMaterial:n=!0,debug:r=!1,material:i}=t??{},a=r?performance.now():0,o=await Ie(e,{mergeByMaterial:n,debug:r,material:i});if(o)return o;let s=performance.now(),c=oe(e),l=performance.now()-s,u=e.byteLength;return Fe(c,{mergeByMaterial:n,debug:r,material:i,parseTime:0,decodeTime:l,perfStart:a,blobBytes:u})}let Ne=!1;function Pe(e,n){if(e||Ne)return;let r=0;for(let e of n)r+=e.meshes.length;r<1e3||(Ne=!0,t.p().warn(`Parsing ${r} meshes with mergeByMaterial: false — each becomes its own THREE object, and render cost scales with object count. Drop the option to merge by material (the default); per-object identity survives it via userData.members.`))}function Fe(e,n){let{mergeByMaterial:r,debug:i,material:a,parseTime:o,decodeTime:s,perfStart:c,blobBytes:l,fallback:u}=n,d=e.metadata.materials??u?.materials??[],f=e.metadata.groups??u?.groups??[],p=!!(e.flags&1);Se(f,d.length,e.vertices.length/3,e.indices.length);let m=p?e.vertices:we(e.vertices,e.origin,e.scale);if(i){let n=e.vertices.byteLength+e.indices.byteLength;t.p().debug(`Mesh Batch Stats:`),t.p().debug(` Materials: ${d.length} | Groups: ${f.length}`),t.p().debug(` Vertices: ${e.vertices.length/3} | Indices: ${e.indices.length}`),t.p().debug(` Format: ${p?`float32`:`int16 quantized`}`),t.p().debug(` Blob: ${(l/1024/1024).toFixed(2)} MB | Geometry on wire: ${(n/1024/1024).toFixed(2)} MB`)}Pe(r,f);let h=performance.now(),g=ve(d,{vertexColors:e.colors!=null,hasUvs:e.uvs!=null,appearance:a}),_=[],v=r?f.flatMap(Te):f;for(let t of v)if(r&&t.meshes.length>1){let n=Ee(t,m,e.indices,g,e.uvs,e.colors);_.push(n)}else{let n=ke(t,m,e.indices,g,e.uvs,e.colors);_.push(...n)}let y=performance.now()-h;if(i){let e=performance.now()-c;t.p().debug(`Performance:`),o>0&&t.p().debug(` Parse JSON: ${o.toFixed(2)}ms`),t.p().debug(` Decode binary: ${s.toFixed(2)}ms`),t.p().debug(` Create Meshes: ${y.toFixed(2)}ms`),t.p().debug(` Total: ${e.toFixed(2)}ms`)}return Promise.resolve(_)}async function Ie(e,n){if(typeof Worker>`u`)return null;let r=V(e),i=r.planarByteSplit?r.indexData.length/(r.uint16Indices?2:4):r.indexData.length;if(i/3<5e4)return null;let a=fe();if(!a)return null;let o=r.metadata.materials??n.fallback?.materials??[],s=r.metadata.groups??n.fallback?.groups??[];Se(s,o.length,r.vertexCount,i);let c=e=>({vertexStart:e.vertexStart,vertexCount:e.vertexCount,indexStart:e.indexStart,indexCount:e.indexCount}),l=[],u=[],d=n.mergeByMaterial?s.flatMap(Te):s;for(let e of d)if(n.mergeByMaterial&&e.meshes.length>1)l.push({kind:`merged`,windows:e.meshes.map(c)}),u.push({kind:`merged`,group:e});else for(let t of e.meshes)l.push({kind:`single`,windows:[c(t)]}),u.push({kind:`single`,group:e,meshMeta:t});let f=r.vertexData.slice(),p=r.indexData.slice(),m=[f.buffer,p.buffer];r.uvs&&m.push(r.uvs.buffer),r.colors&&m.push(r.colors.buffer);let h;try{h=await pe(a,{vertexData:f,isFloat32:r.isFloat32,deltaEncoded:r.deltaEncoded,planarByteSplit:r.planarByteSplit,uint16Indices:r.uint16Indices,origin:r.origin,scale:r.scale,indexData:p,uvs:r.uvs,colors:r.colors,jobs:l},m)}catch(e){return t.p().warn(`Mesh assembly worker failed; falling back to main-thread parse.`,e),null}if(h.length!==l.length)return null;let g=ve(o,{vertexColors:r.colors!=null,hasUvs:r.uvs!=null,appearance:n.material}),_=[];for(let e=0;e<h.length;e++){let t=h[e],n=u[e],r=Z(t),i=g(n.group.materialId,t.closed),a=n.kind===`merged`?Oe(r,n.group,i):Ae(r,n.meshMeta,i);_.push(a)}return n.debug&&t.p().debug(`Mesh batch assembled off-thread: ${_.length} meshes, ${i/3} triangles`),_}function Le(e){return Math.floor(e.length*3/4)}const Q={Angstroms:1e-10,Nanometers:1e-9,Microns:1e-6,Millimeters:.001,Centimeters:.01,Decimeters:.1,Meters:1,Dekameters:10,Hectometers:100,Kilometers:1e3,Megameters:1e6,Gigameters:1e9,Microinches:2.54e-8,Mils:254e-7,Inches:.0254,Feet:.3048,Yards:.9144,Miles:1609.344,NauticalMiles:1852};function Re(e){let t=e.split(`.`);return t.includes(`Display`)||t.includes(`DisplayBatch`)}const $=new Set;async function ze(e,t){let n=performance.now(),r=[],{allowScaling:i=!0,allowAutoPosition:a=!1,groundAxis:o=`z`,debug:s=!1,parsing:c={}}=t??{};try{return await Ve(e,r,i?Be(e.modelunits):1,c,s),a&&Ge(r,o),r}catch(e){throw Ke(e,r),e}finally{s&&Je(n)}}function Be(e){let n=Q[e];return n===void 0?($.has(e)||($.add(e),t.p().warn(`Unknown Rhino model unit "${e}" — geometry will not be scaled (factor 1). Known units: ${Object.keys(Q).join(`, `)}.`)),1):n}async function Ve(e,t,n,r,i){for(let a of e.values){let e=a.InnerTree;for(let a in e){let o=e[a];o&&await He(o,t,n,r,i)}}}async function He(e,n,r,i,a){for(let o of e){if(!Re(o.type))continue;let e={debug:!1,...i,mergeByMaterial:i.mergeByMaterial??!0},s=Ue(o.data);if(!s){t.p().error(`Error parsing display batch envelope: invalid JSON`);continue}let c=await je(s,e),l=v(s.items),u=[...c,...l];if(r!==1)for(let e of u)e.scale.set(r,r,r);n.push(...u),a&&t.p().debug(`Extracted ${c.length} meshes and ${l.length} items from batch`)}}function Ue(e){return typeof e==`string`?We(e):e}function We(e){try{return JSON.parse(e)}catch{return}}function Ge(e,n){if(e.length===0)return;let r=t.a(e);t.i(e,r.min[n],n)}function Ke(e,n){t.p().error(`An unexpected error occurred:`,e),qe(n)}function qe(e){for(let t of e){let e=t;e.geometry&&e.geometry.dispose(),e.material&&(Array.isArray(e.material)?e.material.forEach(e=>e.dispose()):e.material.dispose())}}function Je(e){let n=performance.now()-e;t.p().info(`Time to process meshes:`,`${n.toFixed(2)}ms`)}exports.SCALE_FACTORS=Q,exports.getThreeObjectsFromComputeResponse=ze,exports.meshPolicy=p,exports.parseDisplayItems=v,exports.parseMeshBatchBlob=Me,exports.parseMeshBatchObject=je,exports.setTextureAnisotropy=q;
2008
+ #endif`);
2009
+ });
2010
+ }
2011
+ // src/parse/webdisplay/batch/metadata.ts
2012
+ function metadataFail(message, context) {
2013
+ return new require_gpu_dispose.VisualizationError(message, require_gpu_dispose.ErrorCodes.VALIDATION_ERROR, { context });
2014
+ }
2015
+ /**
2016
+ * Validates the batch's group/mesh metadata against the decoded geometry buffers before any of it
2017
+ * is used arithmetically. Throws on the first inconsistency — out-of-range `materialId`,
2018
+ * non-integer or negative offsets/counts, or a vertex/index window that overruns the buffers — so
2019
+ * malformed or version-skewed metadata fails the parse loudly instead of corrupting the render.
2020
+ */
2021
+ function validateGroupMetadata(groups, materialCount, totalVertexCount, totalIndexCount) {
2022
+ for (const group of groups) {
2023
+ if (!Number.isInteger(group.materialId) || group.materialId < 0 || group.materialId >= materialCount) throw metadataFail("Group materialId out of range of the materials array.", {
2024
+ materialId: group.materialId,
2025
+ materialCount
2026
+ });
2027
+ for (const mesh of group.meshes) {
2028
+ const fields = {
2029
+ vertexStart: mesh.vertexStart,
2030
+ vertexCount: mesh.vertexCount,
2031
+ indexStart: mesh.indexStart,
2032
+ indexCount: mesh.indexCount
2033
+ };
2034
+ for (const [field, value] of Object.entries(fields)) if (!Number.isInteger(value) || value < 0) throw metadataFail(`Mesh metadata field "${field}" must be a non-negative integer.`, {
2035
+ meshName: mesh.name,
2036
+ field,
2037
+ value
2038
+ });
2039
+ if (mesh.vertexStart + mesh.vertexCount > totalVertexCount) throw metadataFail("Mesh vertex window exceeds the batch vertex buffer.", {
2040
+ meshName: mesh.name,
2041
+ vertexStart: mesh.vertexStart,
2042
+ vertexCount: mesh.vertexCount,
2043
+ totalVertexCount
2044
+ });
2045
+ if (mesh.indexStart + mesh.indexCount > totalIndexCount) throw metadataFail("Mesh index window exceeds the batch index buffer.", {
2046
+ meshName: mesh.name,
2047
+ indexStart: mesh.indexStart,
2048
+ indexCount: mesh.indexCount,
2049
+ totalIndexCount
2050
+ });
2051
+ }
2052
+ }
2053
+ }
2054
+ /**
2055
+ * Error for an index outside its mesh's declared vertex window
2056
+ * `[vertexStart, vertexStart + vertexCount)`. Rebasing (`index - vertexStart`) writes into an
2057
+ * unsigned array, so an out-of-window index would otherwise wrap to ~4 billion and corrupt the
2058
+ * geometry. Range checks live inline in the copy loops (a function call per index measured
2059
+ * noticeably slower at millions of indices) — this only builds the failure.
2060
+ */
2061
+ function indexOutOfWindow(indexValue, meshMeta) {
2062
+ return metadataFail("Index references a vertex outside its mesh's vertex window.", {
2063
+ meshName: meshMeta.name,
2064
+ indexValue,
2065
+ vertexStart: meshMeta.vertexStart,
2066
+ vertexCount: meshMeta.vertexCount
2067
+ });
2068
+ }
2069
+ /**
2070
+ * Reconstructs world-unit float32 positions from int16 quantized values:
2071
+ * `world = origin + (q + 32767) * scale`. No rotation — the Three scene uses Rhino's Z-up frame,
2072
+ * so vertices pass through as they arrived.
2073
+ */
2074
+ function dequantizeInt16(q, origin, scale) {
2075
+ const out = new Float32Array(q.length);
2076
+ const ox = origin[0];
2077
+ const oy = origin[1];
2078
+ const oz = origin[2];
2079
+ const sx = scale[0];
2080
+ const sy = scale[1];
2081
+ const sz = scale[2];
2082
+ for (let i = 0; i < q.length; i += 3) {
2083
+ out[i] = ox + (q[i] + 32767) * sx;
2084
+ out[i + 1] = oy + (q[i + 1] + 32767) * sy;
2085
+ out[i + 2] = oz + (q[i + 2] + 32767) * sz;
2086
+ }
2087
+ return out;
2088
+ }
2089
+ // src/parse/webdisplay/batch/merge.ts
2090
+ /**
2091
+ * Split a material group into one sub-group per layer, preserving mesh order.
2092
+ *
2093
+ * Merging is by material alone, but a merged mesh is one THREE object and so can sit on exactly
2094
+ * one layer in the outliner. Without this split, two layers sharing a material collapse into a
2095
+ * single object filed under the first one, and hiding either layer hides both.
2096
+ */
2097
+ function splitGroupByLayer(group) {
2098
+ const byLayer = /* @__PURE__ */ new Map();
2099
+ for (const meshMeta of group.meshes) {
2100
+ const layer = meshMeta.layer ?? "";
2101
+ let bucket = byLayer.get(layer);
2102
+ if (!bucket) {
2103
+ bucket = [];
2104
+ byLayer.set(layer, bucket);
2105
+ }
2106
+ bucket.push(meshMeta);
2107
+ }
2108
+ return [...byLayer.values()].map((meshes) => ({
2109
+ materialId: group.materialId,
2110
+ meshes
2111
+ }));
2112
+ }
2113
+ /**
2114
+ * The groups a merge-by-material draws: every group of one material gathered into one, in order of
2115
+ * first appearance, then split by layer. The writer keeps meshes in input order, so one material
2116
+ * can head several groups.
2117
+ */
2118
+ function mergeGroupsOf(groups) {
2119
+ const byMaterial = /* @__PURE__ */ new Map();
2120
+ for (const group of groups) {
2121
+ const meshes = byMaterial.get(group.materialId);
2122
+ if (meshes) for (const mesh of group.meshes) meshes.push(mesh);
2123
+ else byMaterial.set(group.materialId, group.meshes.slice());
2124
+ }
2125
+ return [...byMaterial].flatMap(([materialId, meshes]) => splitGroupByLayer({
2126
+ materialId,
2127
+ meshes
2128
+ }));
2129
+ }
2130
+ /**
2131
+ * Merges a material group's meshes into one BufferGeometry. Parser indices already address the
2132
+ * combined vertex array (rebased by the C# pipeline during batch assembly), so this copies each
2133
+ * mesh's vertex/index slices into a fresh contiguous buffer and shifts indices to match.
2134
+ */
2135
+ function createMergedMesh(group, allVertices, allIndices, pickMaterial, allUvs = null, allColors = null) {
2136
+ let totalVertexCount = 0;
2137
+ let totalIndexCount = 0;
2138
+ for (const meshMeta of group.meshes) {
2139
+ totalVertexCount += meshMeta.vertexCount;
2140
+ totalIndexCount += meshMeta.indexCount;
2141
+ }
2142
+ const mergedVertices = new Float32Array(totalVertexCount * 3);
2143
+ const mergedIndices = new Uint32Array(totalIndexCount);
2144
+ const mergedUvs = allUvs ? new Float32Array(totalVertexCount * 2) : null;
2145
+ const mergedColors = allColors ? new Uint8Array(totalVertexCount * 3) : null;
2146
+ let vertexWriteCursor = 0;
2147
+ let indexWriteCursor = 0;
2148
+ for (const meshMeta of group.meshes) {
2149
+ const componentStart = meshMeta.vertexStart * 3;
2150
+ const componentLen = meshMeta.vertexCount * 3;
2151
+ mergedVertices.set(allVertices.subarray(componentStart, componentStart + componentLen), vertexWriteCursor * 3);
2152
+ if (mergedUvs && allUvs) {
2153
+ const uvStart = meshMeta.vertexStart * 2;
2154
+ mergedUvs.set(allUvs.subarray(uvStart, uvStart + meshMeta.vertexCount * 2), vertexWriteCursor * 2);
2155
+ }
2156
+ if (mergedColors && allColors) mergedColors.set(allColors.subarray(componentStart, componentStart + componentLen), vertexWriteCursor * 3);
2157
+ const indicesSlice = allIndices.subarray(meshMeta.indexStart, meshMeta.indexStart + meshMeta.indexCount);
2158
+ const indexShift = vertexWriteCursor - meshMeta.vertexStart;
2159
+ const windowStart = meshMeta.vertexStart;
2160
+ const windowEnd = meshMeta.vertexStart + meshMeta.vertexCount;
2161
+ for (let i = 0; i < indicesSlice.length; i++) {
2162
+ const indexValue = indicesSlice[i];
2163
+ if (indexValue < windowStart || indexValue >= windowEnd) throw indexOutOfWindow(indexValue, meshMeta);
2164
+ mergedIndices[indexWriteCursor + i] = indexValue + indexShift;
2165
+ }
2166
+ vertexWriteCursor += meshMeta.vertexCount;
2167
+ indexWriteCursor += meshMeta.indexCount;
2168
+ }
2169
+ const finished = finishGeometry({
2170
+ positions: mergedVertices,
2171
+ indices: mergedIndices,
2172
+ uvs: mergedUvs,
2173
+ colors: mergedColors
2174
+ }, group.meshes);
2175
+ return finalizeMergedMesh(geometryFrom(finished), group, pickMaterial(group.materialId, finished.closed));
2176
+ }
2177
+ function geometryFrom(finished) {
2178
+ const geometry = new three.BufferGeometry();
2179
+ geometry.setAttribute("position", new three.BufferAttribute(finished.positions, 3));
2180
+ geometry.setAttribute("normal", new three.BufferAttribute(finished.normals, 3));
2181
+ geometry.setIndex(new three.BufferAttribute(finished.indices, 1));
2182
+ if (finished.uvs) geometry.setAttribute("uv", new three.BufferAttribute(finished.uvs, 2));
2183
+ if (finished.tangents) geometry.setAttribute("tangent", new three.BufferAttribute(finished.tangents, 4));
2184
+ if (finished.colors) geometry.setAttribute("color", new three.BufferAttribute(finished.colors, 3, true));
2185
+ return geometry;
2186
+ }
2187
+ /**
2188
+ * Per-member records for a merged mesh, each carrying its window into the merged index buffer.
2189
+ * The windows are a prefix sum over `indexCount` because the merge concatenates members in this
2190
+ * order — see the copy loop in {@link createMergedMesh}.
2191
+ */
2192
+ function mergedMembers(group) {
2193
+ const members = [];
2194
+ let indexStart = 0;
2195
+ for (const mesh of group.meshes) {
2196
+ members.push({
2197
+ trackingKey: mesh.id,
2198
+ name: mesh.name,
2199
+ layer: mesh.layer,
2200
+ metadata: mesh.metadata ?? {},
2201
+ indexStart,
2202
+ indexCount: mesh.indexCount
2203
+ });
2204
+ indexStart += mesh.indexCount;
2205
+ }
2206
+ return members;
2207
+ }
2208
+ function finalizeMergedMesh(geometry, group, material) {
2209
+ const threeMesh = new three.Mesh(geometry, material);
2210
+ const firstMesh = group.meshes[0];
2211
+ const meshNames = group.meshes.map((m) => m.name).filter((name) => name && name.length > 0);
2212
+ threeMesh.name = meshNames.length > 0 ? meshNames[0] : `merged_material_${group.materialId}`;
2213
+ threeMesh.castShadow = true;
2214
+ threeMesh.receiveShadow = true;
2215
+ threeMesh.userData = {
2216
+ source: "compute",
2217
+ name: threeMesh.name,
2218
+ layer: firstMesh?.layer ?? "",
2219
+ metadata: firstMesh?.metadata ?? {},
2220
+ members: mergedMembers(group)
2221
+ };
2222
+ return threeMesh;
2223
+ }
2224
+ /**
2225
+ * Creates individual meshes from a material group. Each mesh's indices are rebased so they
2226
+ * address its own local vertex slice starting from 0.
2227
+ */
2228
+ function createIndividualMeshes(group, allVertices, allIndices, pickMaterial, allUvs = null, allColors = null) {
2229
+ const meshes = [];
2230
+ for (const meshMeta of group.meshes) {
2231
+ const componentStart = meshMeta.vertexStart * 3;
2232
+ const componentLen = meshMeta.vertexCount * 3;
2233
+ const vertices = allVertices.slice(componentStart, componentStart + componentLen);
2234
+ const indicesSlice = allIndices.subarray(meshMeta.indexStart, meshMeta.indexStart + meshMeta.indexCount);
2235
+ const rebasedIndices = new Uint32Array(indicesSlice.length);
2236
+ const baseIndex = meshMeta.vertexStart;
2237
+ const windowEnd = meshMeta.vertexStart + meshMeta.vertexCount;
2238
+ for (let i = 0; i < indicesSlice.length; i++) {
2239
+ const indexValue = indicesSlice[i];
2240
+ if (indexValue < baseIndex || indexValue >= windowEnd) throw indexOutOfWindow(indexValue, meshMeta);
2241
+ rebasedIndices[i] = indexValue - baseIndex;
2242
+ }
2243
+ const uvStart = meshMeta.vertexStart * 2;
2244
+ const finished = finishGeometry({
2245
+ positions: vertices,
2246
+ indices: rebasedIndices,
2247
+ uvs: allUvs ? allUvs.slice(uvStart, uvStart + meshMeta.vertexCount * 2) : null,
2248
+ colors: allColors ? allColors.slice(componentStart, componentStart + componentLen) : null
2249
+ }, [meshMeta]);
2250
+ meshes.push(finalizeSingleMesh(geometryFrom(finished), meshMeta, pickMaterial(group.materialId, finished.closed)));
2251
+ }
2252
+ return meshes;
2253
+ }
2254
+ function finalizeSingleMesh(geometry, meshMeta, material) {
2255
+ const mesh = new three.Mesh(geometry, material);
2256
+ mesh.name = meshMeta.name;
2257
+ mesh.userData = {
2258
+ source: "compute",
2259
+ name: meshMeta.name,
2260
+ layer: meshMeta.layer ?? "",
2261
+ trackingKey: meshMeta.id,
2262
+ metadata: meshMeta.metadata ?? {}
2263
+ };
2264
+ mesh.castShadow = true;
2265
+ mesh.receiveShadow = true;
2266
+ return mesh;
2267
+ }
2268
+ // src/parse/webdisplay/batch/finished-cache.ts
2269
+ const MAX_BYTES = 268435456;
2270
+ const entries = /* @__PURE__ */ new Map();
2271
+ let totalBytes = 0;
2272
+ function getFinishedBatch(geometryBytes, jobs) {
2273
+ const jobShape = shapeOf(jobs);
2274
+ const key = cacheKey(geometryBytes, jobShape);
2275
+ const entry = entries.get(key);
2276
+ if (!entry || !sameInts(entry.jobShape, jobShape) || !sameBytes(entry.geometryBytes, geometryBytes)) return null;
2277
+ entries.delete(key);
2278
+ entries.set(key, entry);
2279
+ return entry;
2280
+ }
2281
+ /** Wraps each finished geometry in a `BufferGeometry`, owned by the scene. */
2282
+ function buildBatch(batch) {
2283
+ const { geometries, ...rest } = batch;
2284
+ return {
2285
+ ...rest,
2286
+ buffers: geometries.map(geometryFrom),
2287
+ closed: geometries.map((g) => g.closed)
2288
+ };
2289
+ }
2290
+ /** Same as {@link buildBatch}, but the cache keeps the geometries for the next identical payload. */
2291
+ function storeFinishedBatch(geometryBytes, jobs, batch) {
2292
+ const built = buildBatch(batch);
2293
+ let bytes = geometryBytes.byteLength;
2294
+ for (const g of batch.geometries) {
2295
+ bytes += g.positions.byteLength + g.normals.byteLength + g.indices.byteLength;
2296
+ bytes += (g.uvs?.byteLength ?? 0) + (g.colors?.byteLength ?? 0) + (g.tangents?.byteLength ?? 0);
2297
+ }
2298
+ if (bytes > MAX_BYTES) return built;
2299
+ const jobShape = shapeOf(jobs);
2300
+ const key = cacheKey(geometryBytes, jobShape);
2301
+ const existing = entries.get(key);
2302
+ if (existing) {
2303
+ entries.delete(key);
2304
+ release(existing);
2305
+ }
2306
+ built.buffers.forEach(require_gpu_ownership.shareGeometry);
2307
+ entries.set(key, {
2308
+ ...built,
2309
+ geometryBytes: geometryBytes.slice(),
2310
+ jobShape,
2311
+ bytes
2312
+ });
2313
+ totalBytes += bytes;
2314
+ for (const [oldest, entry] of entries) {
2315
+ if (totalBytes <= MAX_BYTES) break;
2316
+ entries.delete(oldest);
2317
+ release(entry);
2318
+ }
2319
+ return built;
2320
+ }
2321
+ /**
2322
+ * Empties the cache and frees its geometries. The cache lives as long as the page, so a host calls
2323
+ * this when its viewer goes away; a mesh still drawing one of them uploads it again.
2324
+ */
2325
+ function clearGeometryCache() {
2326
+ entries.forEach(release);
2327
+ entries.clear();
2328
+ }
2329
+ function release(entry) {
2330
+ totalBytes -= entry.bytes;
2331
+ for (const buffer of entry.buffers) {
2332
+ require_gpu_ownership.unshareGeometry(buffer);
2333
+ buffer.dispose();
2334
+ }
2335
+ }
2336
+ function shapeOf(jobs) {
2337
+ let length = 0;
2338
+ for (const job of jobs) length += 2 + 4 * job.windows.length;
2339
+ const shape = new Int32Array(length);
2340
+ let i = 0;
2341
+ for (const job of jobs) {
2342
+ shape[i++] = job.kind === "merged" ? 1 : 0;
2343
+ shape[i++] = job.windows.length;
2344
+ for (const w of job.windows) {
2345
+ shape[i++] = w.vertexStart;
2346
+ shape[i++] = w.vertexCount;
2347
+ shape[i++] = w.indexStart;
2348
+ shape[i++] = w.indexCount;
2349
+ }
2350
+ }
2351
+ return shape;
2352
+ }
2353
+ function cacheKey(geometryBytes, jobShape) {
2354
+ const length = geometryBytes.length;
2355
+ const stride = Math.max(1, Math.floor(length / 1024));
2356
+ let hash = 2166136261;
2357
+ for (let i = 0; i < length; i += stride) hash = Math.imul(hash ^ geometryBytes[i], 16777619);
2358
+ let shapeHash = 2166136261;
2359
+ for (let i = 0; i < jobShape.length; i++) shapeHash = Math.imul(shapeHash ^ jobShape[i], 16777619);
2360
+ return `${length}:${hash >>> 0}:${jobShape.length}:${shapeHash >>> 0}`;
2361
+ }
2362
+ function sameInts(a, b) {
2363
+ if (a.length !== b.length) return false;
2364
+ for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
2365
+ return true;
2366
+ }
2367
+ function sameBytes(a, b) {
2368
+ if (a.length !== b.length) return false;
2369
+ let i = 0;
2370
+ if (a.byteOffset % 4 === 0 && b.byteOffset % 4 === 0) {
2371
+ const words = a.length >>> 2;
2372
+ const wa = new Uint32Array(a.buffer, a.byteOffset, words);
2373
+ const wb = new Uint32Array(b.buffer, b.byteOffset, words);
2374
+ for (; i < words; i++) if (wa[i] !== wb[i]) return false;
2375
+ i = words << 2;
2376
+ }
2377
+ for (; i < a.length; i++) if (a[i] !== b[i]) return false;
2378
+ return true;
2379
+ }
2380
+ // src/parse/webdisplay/batch-parser.ts
2381
+ /**
2382
+ * Parses a DisplayBatch object and creates Three.js meshes from its mesh blob.
2383
+ *
2384
+ * Synchronous internally — `parseBinaryMeshBatch` does no IO, just typed-array views over the
2385
+ * blob. Stays `async` so callers don't need to change shape if parsing moves into a worker later.
2386
+ *
2387
+ * @throws {VisualizationError} On a corrupt/truncated/unsupported mesh blob or malformed group metadata.
2388
+ */
2389
+ async function parseMeshBatchObject(batch, options, telemetry) {
2390
+ const { mergeByMaterial = true, debug = false, material } = options ?? {};
2391
+ const { parseTime = 0, perfStart = debug ? performance.now() : 0 } = telemetry ?? {};
2392
+ if (!batch.compressedData) return [];
2393
+ const decodeStart = performance.now();
2394
+ const assemblyOptions = {
2395
+ mergeByMaterial,
2396
+ debug,
2397
+ material,
2398
+ fallback: {
2399
+ materials: batch.materials,
2400
+ groups: batch.groups
2401
+ }
2402
+ };
2403
+ const opened = await openBatch(batch.compressedData, assemblyOptions);
2404
+ if (opened.meshes) return opened.meshes;
2405
+ const assembledMeshes = await tryBuildViaAssembly(opened, assemblyOptions);
2406
+ if (assembledMeshes) return assembledMeshes;
2407
+ return buildMeshesFromParsed(decodeRawMeshBatch(opened.raw), {
2408
+ mergeByMaterial,
2409
+ debug,
2410
+ material,
2411
+ parseTime,
2412
+ decodeTime: performance.now() - decodeStart,
2413
+ perfStart,
2414
+ blobBytes: debug ? approximateBase64DecodedBytes(batch.compressedData) : 0,
2415
+ fallback: {
2416
+ materials: batch.materials,
2417
+ groups: batch.groups
2418
+ }
2419
+ });
2420
+ }
2421
+ /**
2422
+ * Parses a raw binary mesh batch blob (SLVA wire format) and creates Three.js meshes.
2423
+ *
2424
+ * Use this entry point when the blob arrives as a binary WebSocket frame rather than inside a JSON
2425
+ * envelope — the blob is self-describing, with materials, groups, and per-object ids coming
2426
+ * from the container's table.
2427
+ *
2428
+ * @throws {VisualizationError} On a corrupt/truncated/unsupported mesh blob or malformed group metadata.
2429
+ */
2430
+ async function parseMeshBatchBlob(blob, options) {
2431
+ const { mergeByMaterial = true, debug = false, material } = options ?? {};
2432
+ const perfStart = debug ? performance.now() : 0;
2433
+ const decodeStart = performance.now();
2434
+ const assemblyOptions = {
2435
+ mergeByMaterial,
2436
+ debug,
2437
+ material
2438
+ };
2439
+ const opened = await openBatch(blob, assemblyOptions);
2440
+ if (opened.meshes) return opened.meshes;
2441
+ const assembledMeshes = await tryBuildViaAssembly(opened, assemblyOptions);
2442
+ if (assembledMeshes) return assembledMeshes;
2443
+ const parsed = decodeRawMeshBatch(opened.raw);
2444
+ const decodeTime = performance.now() - decodeStart;
2445
+ const blobBytes = blob.byteLength;
2446
+ return buildMeshesFromParsed(parsed, {
2447
+ mergeByMaterial,
2448
+ debug,
2449
+ material,
2450
+ parseTime: 0,
2451
+ decodeTime,
2452
+ perfStart,
2453
+ blobBytes
2454
+ });
2455
+ }
2456
+ const UNMERGED_MESH_WARN_THRESHOLD = 1e3;
2457
+ let warnedUnmerged = false;
2458
+ /**
2459
+ * Warns once when a caller opts out of merging on a batch large enough for it to hurt.
2460
+ *
2461
+ * Opting out is legitimate — it is what gives every source object its own THREE object — so this
2462
+ * does not override the caller. It exists because the failure is silent: nothing errors, the model
2463
+ * renders correctly, and the only symptom is a frame rate nobody traces back to a parse option.
2464
+ */
2465
+ function warnIfUnmergedAtScale(mergeByMaterial, groups) {
2466
+ if (mergeByMaterial || warnedUnmerged) return;
2467
+ let meshCount = 0;
2468
+ for (const group of groups) meshCount += group.meshes.length;
2469
+ if (meshCount < UNMERGED_MESH_WARN_THRESHOLD) return;
2470
+ warnedUnmerged = true;
2471
+ require_gpu_dispose.getLogger().warn(`Parsing ${meshCount} meshes with mergeByMaterial: false — each becomes its own THREE object, and render cost scales with object count. Drop the option to merge by material (the default); per-object identity survives it via userData.members.`);
2472
+ }
2473
+ function buildMeshesFromParsed(parsed, opts) {
2474
+ const { mergeByMaterial, debug, material: materialAppearance, parseTime, decodeTime, perfStart, blobBytes, fallback } = opts;
2475
+ const materialsSrc = parsed.metadata.materials ?? fallback?.materials ?? [];
2476
+ const groups = parsed.metadata.groups ?? fallback?.groups ?? [];
2477
+ const isFloat32 = (parsed.flags & 1) !== 0;
2478
+ validateGroupMetadata(groups, materialsSrc.length, parsed.vertices.length / 3, parsed.indices.length);
2479
+ const worldVertices = isFloat32 ? parsed.vertices : dequantizeInt16(parsed.vertices, parsed.origin, parsed.scale);
2480
+ if (debug) {
2481
+ const wireBytes = parsed.vertices.byteLength + parsed.indices.byteLength;
2482
+ require_gpu_dispose.getLogger().debug("Mesh Batch Stats:");
2483
+ require_gpu_dispose.getLogger().debug(` Materials: ${materialsSrc.length} | Groups: ${groups.length}`);
2484
+ require_gpu_dispose.getLogger().debug(` Vertices: ${parsed.vertices.length / 3} | Indices: ${parsed.indices.length}`);
2485
+ require_gpu_dispose.getLogger().debug(` Format: ${isFloat32 ? "float32" : "int16 quantized"}`);
2486
+ require_gpu_dispose.getLogger().debug(` Blob: ${(blobBytes / 1024 / 1024).toFixed(2)} MB | Geometry on wire: ${(wireBytes / 1024 / 1024).toFixed(2)} MB`);
2487
+ }
2488
+ warnIfUnmergedAtScale(mergeByMaterial, groups);
2489
+ const meshCreateStart = performance.now();
2490
+ const pickMaterial = createMaterialPicker(materialsSrc, {
2491
+ vertexColors: parsed.colors != null,
2492
+ hasUvs: parsed.uvs != null,
2493
+ appearance: materialAppearance
2494
+ });
2495
+ const meshes = [];
2496
+ const mergeGroups = mergeByMaterial ? mergeGroupsOf(groups) : groups;
2497
+ for (const group of mergeGroups) if (mergeByMaterial && group.meshes.length > 1) {
2498
+ const mergedMesh = createMergedMesh(group, worldVertices, parsed.indices, pickMaterial, parsed.uvs, parsed.colors);
2499
+ meshes.push(mergedMesh);
2500
+ } else {
2501
+ const individualMeshes = createIndividualMeshes(group, worldVertices, parsed.indices, pickMaterial, parsed.uvs, parsed.colors);
2502
+ for (const mesh of individualMeshes) meshes.push(mesh);
2503
+ }
2504
+ const meshCreateTime = performance.now() - meshCreateStart;
2505
+ if (debug) {
2506
+ const totalTime = performance.now() - perfStart;
2507
+ require_gpu_dispose.getLogger().debug("Performance:");
2508
+ if (parseTime > 0) require_gpu_dispose.getLogger().debug(` Parse JSON: ${parseTime.toFixed(2)}ms`);
2509
+ require_gpu_dispose.getLogger().debug(` Decode binary: ${decodeTime.toFixed(2)}ms`);
2510
+ require_gpu_dispose.getLogger().debug(` Create Meshes: ${meshCreateTime.toFixed(2)}ms`);
2511
+ require_gpu_dispose.getLogger().debug(` Total: ${totalTime.toFixed(2)}ms`);
2512
+ }
2513
+ return Promise.resolve(meshes);
2514
+ }
2515
+ /**
2516
+ * Unwraps an SLVM container only as far as its table and materials, and answers from the
2517
+ * finished-geometry cache before paying for the inflate. A bare SLVA/SLVZ blob (old `.gh` files)
2518
+ * keeps its metadata inside the compressed geometry, so it always decodes.
2519
+ */
2520
+ async function openBatch(input, opts) {
2521
+ const bytes = toUint8Array(input);
2522
+ if (!isSlvmContainer(bytes)) return {
2523
+ raw: parseBinaryMeshBatchRaw(bytes),
2524
+ geometryBytes: null,
2525
+ source: bytes,
2526
+ reparse: () => parseBinaryMeshBatchRaw(bytes)
2527
+ };
2528
+ const container = parseSlvmContainer(bytes);
2529
+ const cached = buildFromFinishedCache(container, opts);
2530
+ if (cached) return { meshes: cached };
2531
+ const geometryBlob = container.geometryBlob;
2532
+ const reparse = () => {
2533
+ const raw = parseBinaryMeshBatchRaw(geometryBlob);
2534
+ raw.metadata = container.metadata;
2535
+ return raw;
2536
+ };
2537
+ const inflated = await inflateInWorker(container);
2538
+ const raw = inflated ? parseBinaryMeshBatchRaw(inflated) : reparse();
2539
+ raw.metadata = container.metadata;
2540
+ return {
2541
+ raw,
2542
+ geometryBytes: geometryBlob,
2543
+ source: geometryBlob,
2544
+ reparse
2545
+ };
2546
+ }
2547
+ const INFLATE_IN_WORKER_MIN_BYTES = 4194304;
2548
+ /**
2549
+ * Inflates a large SLVZ geometry chunk off the calling thread. Null means inflate it here: a small
2550
+ * or uncompressed chunk, no worker support, or a worker that failed.
2551
+ */
2552
+ async function inflateInWorker(container) {
2553
+ if (slvzInflatedBytes(container.geometryBlob) < INFLATE_IN_WORKER_MIN_BYTES || !getAssemblyWorker()) return null;
2554
+ try {
2555
+ return await inflateSlvzAsync(container.geometryBlob);
2556
+ } catch {
2557
+ return null;
2558
+ }
2559
+ }
2560
+ function buildFromFinishedCache(container, opts) {
2561
+ const materialsSrc = container.metadata.materials ?? opts.fallback?.materials ?? [];
2562
+ const groups = container.metadata.groups ?? opts.fallback?.groups ?? [];
2563
+ const { jobs, jobRefs } = planJobs(groups, opts.mergeByMaterial);
2564
+ const cached = getFinishedBatch(container.geometryBlob, jobs);
2565
+ if (!cached) return null;
2566
+ validateGroupMetadata(groups, materialsSrc.length, cached.vertexCount, cached.indexCount);
2567
+ return meshesFromBuilt(cached, jobRefs, materialsSrc, opts.material);
2568
+ }
2569
+ /** One job per merged group or per unmerged mesh, with a parallel ref list to unwrap results by index. */
2570
+ function planJobs(groups, mergeByMaterial) {
2571
+ const windowOf = (m) => ({
2572
+ vertexStart: m.vertexStart,
2573
+ vertexCount: m.vertexCount,
2574
+ indexStart: m.indexStart,
2575
+ indexCount: m.indexCount,
2576
+ closed: m.closed
2577
+ });
2578
+ const jobs = [];
2579
+ const jobRefs = [];
2580
+ const mergeGroups = mergeByMaterial ? mergeGroupsOf(groups) : groups;
2581
+ for (const group of mergeGroups) if (mergeByMaterial && group.meshes.length > 1) {
2582
+ jobs.push({
2583
+ kind: "merged",
2584
+ windows: group.meshes.map(windowOf)
2585
+ });
2586
+ jobRefs.push({
2587
+ kind: "merged",
2588
+ group
2589
+ });
2590
+ } else for (const meshMeta of group.meshes) {
2591
+ jobs.push({
2592
+ kind: "single",
2593
+ windows: [windowOf(meshMeta)]
2594
+ });
2595
+ jobRefs.push({
2596
+ kind: "single",
2597
+ group,
2598
+ meshMeta
2599
+ });
2600
+ }
2601
+ return {
2602
+ jobs,
2603
+ jobRefs
2604
+ };
2605
+ }
2606
+ /**
2607
+ * Builds a heavy batch through the assembly functions, in the worker when there is one and inline
2608
+ * otherwise, and caches the result under `geometryBytes`. Returns `null` for a small batch or a
2609
+ * crashed worker, and the caller then runs the synchronous path. Malformed-blob/metadata errors
2610
+ * throw either way, matching the entry points' contract.
2611
+ */
2612
+ async function tryBuildViaAssembly(opened, opts) {
2613
+ const { raw, geometryBytes } = opened;
2614
+ const indexCount = raw.planarByteSplit ? raw.indexData.length / (raw.uint16Indices ? 2 : 4) : raw.indexData.length;
2615
+ if (indexCount / 3 < 5e4) return null;
2616
+ const materialsSrc = raw.metadata.materials ?? opts.fallback?.materials ?? [];
2617
+ const groups = raw.metadata.groups ?? opts.fallback?.groups ?? [];
2618
+ validateGroupMetadata(groups, materialsSrc.length, raw.vertexCount, indexCount);
2619
+ const { jobs, jobRefs } = planJobs(groups, opts.mergeByMaterial);
2620
+ const assembled = await assemble(raw, jobs, opened.source);
2621
+ if (!assembled) {
2622
+ if (ownsBuffer(raw, opened.source)) opened.raw = opened.reparse();
2623
+ return null;
2624
+ }
2625
+ const batch = {
2626
+ geometries: assembled,
2627
+ hasUvs: raw.uvs != null,
2628
+ hasColors: raw.colors != null,
2629
+ vertexCount: raw.vertexCount,
2630
+ indexCount
2631
+ };
2632
+ const built = geometryBytes ? storeFinishedBatch(geometryBytes, jobs, batch) : buildBatch(batch);
2633
+ if (opts.debug) require_gpu_dispose.getLogger().debug(`Mesh batch assembled: ${assembled.length} geometries, ${indexCount / 3} triangles`);
2634
+ return meshesFromBuilt(built, jobRefs, materialsSrc, opts.material);
2635
+ }
2636
+ /**
2637
+ * True when `raw`'s geometry arrays live in a buffer nobody else reads: the buffer a SLVZ chunk
2638
+ * was inflated into, not the WebSocket frame an uncompressed chunk still points into.
2639
+ */
2640
+ function ownsBuffer(raw, source) {
2641
+ return raw.vertexData.buffer !== source.buffer && raw.indexData.buffer !== source.buffer;
2642
+ }
2643
+ async function assemble(raw, jobs, source) {
2644
+ const worker = getAssemblyWorker();
2645
+ const input = {
2646
+ vertexData: raw.vertexData,
2647
+ isFloat32: raw.isFloat32,
2648
+ deltaEncoded: raw.deltaEncoded,
2649
+ planarByteSplit: raw.planarByteSplit,
2650
+ uint16Indices: raw.uint16Indices,
2651
+ origin: raw.origin,
2652
+ scale: raw.scale,
2653
+ indexData: raw.indexData,
2654
+ uvs: raw.uvs,
2655
+ colors: raw.colors,
2656
+ jobs
2657
+ };
2658
+ if (!worker) return assembleGeometries(input).map((geometry, i) => finishGeometry(geometry, jobs[i].windows));
2659
+ if (!ownsBuffer(raw, source)) {
2660
+ input.vertexData = raw.vertexData.slice();
2661
+ input.indexData = raw.indexData.slice();
2662
+ input.uvs = raw.uvs?.slice() ?? null;
2663
+ input.colors = raw.colors?.slice() ?? null;
2664
+ }
2665
+ const transfer = /* @__PURE__ */ new Set([input.vertexData.buffer, input.indexData.buffer]);
2666
+ if (input.uvs) transfer.add(input.uvs.buffer);
2667
+ if (input.colors) transfer.add(input.colors.buffer);
2668
+ try {
2669
+ const assembled = await requestAssembly(worker, input, [...transfer]);
2670
+ return assembled.length === jobs.length ? assembled : null;
2671
+ } catch (error) {
2672
+ require_gpu_dispose.getLogger().warn("Mesh assembly worker failed; falling back to main-thread parse.", error);
2673
+ return null;
2674
+ }
2675
+ }
2676
+ function meshesFromBuilt(built, jobRefs, materialsSrc, appearance) {
2677
+ const pickMaterial = createMaterialPicker(materialsSrc, {
2678
+ vertexColors: built.hasColors,
2679
+ hasUvs: built.hasUvs,
2680
+ appearance
2681
+ });
2682
+ const meshes = [];
2683
+ for (let i = 0; i < built.buffers.length; i++) {
2684
+ const geometry = built.buffers[i];
2685
+ const ref = jobRefs[i];
2686
+ const material = pickMaterial(ref.group.materialId, built.closed[i]);
2687
+ const mesh = ref.kind === "merged" ? finalizeMergedMesh(geometry, ref.group, material) : finalizeSingleMesh(geometry, ref.meshMeta, material);
2688
+ meshes.push(mesh);
2689
+ }
2690
+ return meshes;
2691
+ }
2692
+ function approximateBase64DecodedBytes(base64) {
2693
+ return Math.floor(base64.length * 3 / 4);
2694
+ }
2695
+ // src/parse/webdisplay/webdisplay-parser.ts
2696
+ /**
2697
+ * Metres per model unit, keyed by Rhino `UnitSystem` name (the `modelunits` string on the compute
2698
+ * response). Imperial factors are the exact international definitions. Units missing from this
2699
+ * table scale by 1 and log a one-time warning — see {@link getScaleFactor}.
2700
+ */
2701
+ const SCALE_FACTORS = {
2702
+ Angstroms: 1e-10,
2703
+ Nanometers: 1e-9,
2704
+ Microns: 1e-6,
2705
+ Millimeters: .001,
2706
+ Centimeters: .01,
2707
+ Decimeters: .1,
2708
+ Meters: 1,
2709
+ Dekameters: 10,
2710
+ Hectometers: 100,
2711
+ Kilometers: 1e3,
2712
+ Megameters: 1e6,
2713
+ Gigameters: 1e9,
2714
+ Microinches: 2.54e-8,
2715
+ Mils: 254e-7,
2716
+ Inches: .0254,
2717
+ Feet: .3048,
2718
+ Yards: .9144,
2719
+ Miles: 1609.344,
2720
+ NauticalMiles: 1852
2721
+ };
2722
+ const DISPLAY_COMPONENT_TYPE = "Display";
2723
+ const DISPLAY_BATCH_TYPE = "DisplayBatch";
2724
+ /**
2725
+ * True when a wire `type` denotes a Display payload: one of its dot-separated tokens is exactly
2726
+ * `Display` or `DisplayBatch`. Matches the bare `Display` used by older servers and the namespaced
2727
+ * `Selva.GH.Features.Display.Services.DisplayBatch`, but not e.g. `System.DisplayText` — matching
2728
+ * on tokens rather than substring avoids misrouting an unrelated type that merely contains "Display".
2729
+ */
2730
+ function isDisplayItemType(type) {
2731
+ const tokens = type.split(".");
2732
+ return tokens.includes(DISPLAY_COMPONENT_TYPE) || tokens.includes(DISPLAY_BATCH_TYPE);
2733
+ }
2734
+ /** Unknown-unit names already warned about, so a per-solve parse doesn't spam the log. */
2735
+ const warnedUnknownUnits = /* @__PURE__ */ new Set();
2736
+ /**
2737
+ * Extracts display meshes and items from a Grasshopper WebDisplay compute response: decompresses,
2738
+ * scales to meters, and optionally grounds them. Requires the VektorNode Rhino.Compute fork.
2739
+ *
2740
+ * Synchronous internally (large batches block the UI for their duration); `async` only so the
2741
+ * shape can stay stable if parsing moves off-thread later.
2742
+ *
2743
+ * @throws Rethrows unexpected errors after attempting to dispose any created meshes.
2744
+ */
2745
+ async function getThreeObjectsFromComputeResponse(data, options) {
2746
+ const startTime = performance.now();
2747
+ const objects = [];
2748
+ const { allowScaling = true, allowAutoPosition = false, groundAxis = "z", debug = false, parsing: parsingOptions = {} } = options ?? {};
2749
+ try {
2750
+ await extractDisplayFromData(data, objects, allowScaling ? getScaleFactor(data.modelunits) : 1, parsingOptions, debug);
2751
+ if (allowAutoPosition) applyGroundOffset(objects, groundAxis);
2752
+ return objects;
2753
+ } catch (error) {
2754
+ handleError(error, objects);
2755
+ throw error;
2756
+ } finally {
2757
+ if (debug) logProcessingTime(startTime);
2758
+ }
2759
+ }
2760
+ /**
2761
+ * Gets the metres-per-unit scale factor for a Rhino unit name. Unknown units fall back to 1 (no
2762
+ * scaling) with a one-time warning — a kilometers model rendering 1000x off should at least say why.
2763
+ */
2764
+ function getScaleFactor(modelUnits) {
2765
+ const factor = SCALE_FACTORS[modelUnits];
2766
+ if (factor !== void 0) return factor;
2767
+ if (!warnedUnknownUnits.has(modelUnits)) {
2768
+ warnedUnknownUnits.add(modelUnits);
2769
+ require_gpu_dispose.getLogger().warn(`Unknown Rhino model unit "${modelUnits}" — geometry will not be scaled (factor 1). Known units: ${Object.keys(SCALE_FACTORS).join(", ")}.`);
2770
+ }
2771
+ return 1;
2772
+ }
2773
+ async function extractDisplayFromData(data, objects, scaleFactor, parsingOptions, debug) {
2774
+ for (const value of data.values) {
2775
+ const innerTree = value.InnerTree;
2776
+ for (const path in innerTree) {
2777
+ const branch = innerTree[path];
2778
+ if (!branch) continue;
2779
+ await processDataBranch(branch, objects, scaleFactor, parsingOptions, debug);
2780
+ }
2781
+ }
2782
+ }
2783
+ /** Extracts a DisplayBatch's meshes (binary blob) and items (curves/points JSON) from one data branch. */
2784
+ async function processDataBranch(branch, objects, scaleFactor, parsingOptions, debug) {
2785
+ for (const item of branch) {
2786
+ if (!isDisplayItemType(item.type)) continue;
2787
+ const mergedParsingOptions = {
2788
+ debug: false,
2789
+ ...parsingOptions,
2790
+ mergeByMaterial: parsingOptions.mergeByMaterial ?? true
2791
+ };
2792
+ const batch = extractBatch(item.data);
2793
+ if (!batch) {
2794
+ require_gpu_dispose.getLogger().error("Error parsing display batch envelope: invalid JSON");
2795
+ continue;
2796
+ }
2797
+ const batchMeshes = await parseMeshBatchObject(batch, mergedParsingOptions);
2798
+ const batchItems = parseDisplayItems(batch.items);
2799
+ const batchObjects = [...batchMeshes, ...batchItems];
2800
+ if (scaleFactor !== 1) for (const obj of batchObjects) obj.scale.set(scaleFactor, scaleFactor, scaleFactor);
2801
+ for (const object of batchObjects) objects.push(object);
2802
+ if (debug) require_gpu_dispose.getLogger().debug(`Extracted ${batchMeshes.length} meshes and ${batchItems.length} items from batch`);
2803
+ }
2804
+ }
2805
+ /** Resolves `item.data` to a parsed DisplayBatch, tolerating either an already-parsed object or a JSON string. */
2806
+ function extractBatch(data) {
2807
+ return typeof data === "string" ? safeParse(data) : data;
2808
+ }
2809
+ function safeParse(s) {
2810
+ try {
2811
+ return JSON.parse(s);
2812
+ } catch {
2813
+ return;
2814
+ }
2815
+ }
2816
+ /**
2817
+ * Drops objects so their lowest point sits on the ground plane. `axis` isn't hardcoded to `z`
2818
+ * because subtracting `min.z` on a host with a non-default `sceneUp` would shove content sideways
2819
+ * instead of down.
2820
+ */
2821
+ function applyGroundOffset(meshes, axis) {
2822
+ if (meshes.length === 0) return;
2823
+ const combinedBoundingBox = require_gpu_dispose.computeCombinedBoundingBox(meshes);
2824
+ require_gpu_dispose.applyOffset(meshes, combinedBoundingBox.min[axis], axis);
2825
+ }
2826
+ function handleError(error, meshes) {
2827
+ require_gpu_dispose.getLogger().error("An unexpected error occurred:", error);
2828
+ disposeMeshes(meshes);
2829
+ }
2830
+ function disposeMeshes(meshes) {
2831
+ for (const mesh of meshes) require_gpu_dispose.disposeObjectTree(mesh);
2832
+ }
2833
+ function logProcessingTime(startTime) {
2834
+ const elapsed = performance.now() - startTime;
2835
+ require_gpu_dispose.getLogger().info("Time to process meshes:", `${elapsed.toFixed(2)}ms`);
2836
+ }
2837
+ exports.SCALE_FACTORS = SCALE_FACTORS;
2838
+ exports.clearGeometryCache = clearGeometryCache;
2839
+ exports.getThreeObjectsFromComputeResponse = getThreeObjectsFromComputeResponse;
2840
+ exports.meshPolicy = meshPolicy;
2841
+ exports.parseDisplayItems = parseDisplayItems;
2842
+ exports.parseMeshBatchBlob = parseMeshBatchBlob;
2843
+ exports.parseMeshBatchObject = parseMeshBatchObject;
2844
+ exports.setTextureAnisotropy = setTextureAnisotropy;
2845
+
68
2846
  //# sourceMappingURL=parse.cjs.map