@selvajs/visualization 1.6.0-beta.8 → 1.6.0-beta.9
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.
- package/dist/gpu-dispose-Cl3RXm-v.js +355 -0
- package/dist/gpu-dispose-Cl3RXm-v.js.map +1 -0
- package/dist/gpu-dispose-fRI12OCL.cjs +469 -0
- package/dist/gpu-dispose-fRI12OCL.cjs.map +1 -0
- package/dist/gpu-ownership-ADRZBxxs.cjs +102 -0
- package/dist/gpu-ownership-ADRZBxxs.cjs.map +1 -0
- package/dist/gpu-ownership-Bs9l7hrU.js +40 -0
- package/dist/gpu-ownership-Bs9l7hrU.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/merged-draw-CO4rr2Co.js +129 -0
- package/dist/merged-draw-CO4rr2Co.js.map +1 -0
- package/dist/merged-draw-ma_2r4N8.cjs +171 -0
- package/dist/merged-draw-ma_2r4N8.cjs.map +1 -0
- package/dist/parse.cjs +2789 -11
- package/dist/parse.cjs.map +1 -1
- package/dist/parse.d.cts +8 -2
- package/dist/parse.d.ts +8 -2
- package/dist/parse.js +2780 -11
- package/dist/parse.js.map +1 -1
- package/dist/render.cjs +3713 -6
- package/dist/render.cjs.map +1 -1
- package/dist/render.d.cts +23 -3
- package/dist/render.d.ts +23 -3
- package/dist/render.js +3692 -6
- package/dist/render.js.map +1 -1
- package/dist/scene.cjs +343 -1
- package/dist/scene.cjs.map +1 -1
- package/dist/scene.js +336 -1
- package/dist/scene.js.map +1 -1
- package/dist/{types-P5PyfYYE.d.ts → types-DW_qU4-Y.d.cts} +22 -3
- package/dist/{types-P5PyfYYE.d.cts → types-DW_qU4-Y.d.ts} +22 -3
- package/package.json +2 -2
- package/dist/gpu-dispose-Cestx__L.cjs +0 -2
- package/dist/gpu-dispose-Cestx__L.cjs.map +0 -1
- package/dist/gpu-dispose-gTGxGTL4.js +0 -2
- package/dist/gpu-dispose-gTGxGTL4.js.map +0 -1
- package/dist/gpu-ownership-DcHWbATk.js +0 -2
- package/dist/gpu-ownership-DcHWbATk.js.map +0 -1
- package/dist/gpu-ownership-ilj3JEdn.cjs +0 -2
- package/dist/gpu-ownership-ilj3JEdn.cjs.map +0 -1
- package/dist/merged-draw-DXJK-rdo.js +0 -2
- package/dist/merged-draw-DXJK-rdo.js.map +0 -1
- package/dist/merged-draw-oKAKlXCQ.cjs +0 -2
- package/dist/merged-draw-oKAKlXCQ.cjs.map +0 -1
package/dist/parse.cjs
CHANGED
|
@@ -1,5 +1,1835 @@
|
|
|
1
|
-
Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});const e=require("./gpu-ownership-ilj3JEdn.cjs"),t=require("./gpu-dispose-Cestx__L.cjs");let n=require("three");n=e.s(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){return e.byteLength>=8&&new DataView(e.buffer,e.byteOffset,8).getUint32(0,!0)===1515605075}function S(e){return x(e)?new DataView(e.buffer,e.byteOffset,8).getUint32(4,!0):0}function C(e){let t=new DataView(e.buffer,e.byteOffset,e.byteLength).getUint32(4,!0),n=e.subarray(8),r=Math.max(n.byteLength*1032+1024,1<<20);if(t>r)throw z(`SLVZ header declares an implausible uncompressed length`,{uncompressedLen:t,deflatedBytes:n.byteLength,maxPlausibleLen:r});return{deflated:n,uncompressedLen:t}}function w(e,t){return z(`Failed to inflate SLVZ blob: ${e instanceof Error?e.message:String(e)}`,{uncompressedLen:t.uncompressedLen,deflatedBytes:t.deflated.byteLength})}function T(e,t){if(e.byteLength!==t.uncompressedLen)throw z(`SLVZ payload inflated to a different size than the header declares.`,{declaredLen:t.uncompressedLen,actualLen:e.byteLength,deflatedBytes:t.deflated.byteLength});return e}function E(e){if(!x(e))return e;let t=C(e),n;try{n=(0,o.inflateSync)(t.deflated,{out:new Uint8Array(t.uncompressedLen+1)})}catch(e){throw w(e,t)}return T(n,t)}function D(e){return new Promise((t,n)=>{let r=C(e);try{(0,o.inflate)(r.deflated,{size:r.uncompressedLen+1},(e,i)=>{if(e)n(w(e,r));else try{t(T(i,r))}catch(e){n(e)}})}catch(e){n(w(e,r))}})}function O(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 k(e,t,n){if(n===0)return/* @__PURE__ */ 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 A(e,t,n){if(n===0)return/* @__PURE__ */ 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 j(e,t,n){if(n===0)return/* @__PURE__ */ 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 M(e,t,n){if(n===0)return/* @__PURE__ */ 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 N(e,t){if(e.length!==0&&!(e instanceof Uint16Array&&t>65535)){for(let n=0;n<e.length;n++)if(e[n]>=t)throw z(`Index out of range of vertexCount.`,{indexPosition:n,indexValue:e[n],vertexCount:t})}}function P(e){return e>>>1^-(e&1)}function F(e){let t=new Int16Array(e.length),n=0,r=0,i=0;for(let a=0;a<e.length;a+=3)n=n+P(e[a])<<16>>16,r=r+P(e[a+1])<<16>>16,i=i+P(e[a+2])<<16>>16,t[a]=n,t[a+1]=r,t[a+2]=i;return t}function I(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+P(e[t]|e[r*3+t]<<8)<<16>>16,a=a+P(e[r+t]|e[r*4+t]<<8)<<16>>16,o=o+P(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 ee(e,t){let n=new Uint16Array(t),r=0;for(let i=0;i<t;i++)r=r+P(e[i]|e[t+i]<<8)&65535,n[i]=r;return n}function L(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+P(a)>>>0,n[i]=r}return n}function R(e){let t=new Uint16Array(e.length),n=0;for(let r=0;r<e.length;r++)n=n+P(e[r])&65535,t[r]=n;return t}function te(e){let t=new Uint32Array(e.length),n=0;for(let r=0;r<e.length;r++)n=n+P(e[r])>>>0,t[r]=n;return t}function z(e,n){return new t.g(e,t.h.VALIDATION_ERROR,{context:n})}function ne(e,t,n,r,i,a=!1){if(n+36>e.byteLength)throw z(`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 z(`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=A(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+P(t[e]|t[i*2+e]<<8)&65535,o=o+P(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=j(e.buffer,m,d);h=new Float32Array(d);let n=0,r=0;for(let e=0;e<d;e+=2)i?(n=n+P(t[e])&65535,r=r+P(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 re(e,t,n,r){let i=n*3;if(t+i>e.byteLength)throw z(`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+P(a[e])&255,c=c+P(a[e+1])&255,l=l+P(a[e+2])&255,o[e]=s,o[e+1]=c,o[e+2]=l;return o}function ie(e){return e.byteLength>=4&&new DataView(e.buffer,e.byteOffset,4).getUint32(0,!0)===1297501267}function ae(e){let t=oe(e),n=null,r=null,i=null,a=null,o=[];for(let{type:e,payload:s}of t)switch(e){case 1297040711:n=s;break;case 1279410516:r=E(s);break;case 1280590157:i=O(s);break;case 1381516628:o.push(s);break;case 1146309699:a=E(s)}if(n===null)throw z(`SLVM container has no GEOM chunk.`,{chunkCount:t.length});if(r===null)throw z(`SLVM container has no TABL chunk.`,{chunkCount:t.length});let{groups:s}=ce(r,a),c=le(i,o);return{geometryBlob:n,metadata:{materials:c,groups:s}}}function oe(e){let t=new DataView(e.buffer,e.byteOffset,e.byteLength);if(e.byteLength<12||t.getUint32(0,!0)!==1297501267)throw z(`Not an SLVM container (bad magic).`,{byteLength:e.byteLength});let n=t.getUint32(4,!0);if(n!==3)throw z(`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 z(`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 z(`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 B(e){let t=0,n=0;for(;;){if(e.pos>=e.bytes.byteLength||n>28)throw z(`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 se(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[B(e)]??``;break;default:throw z(`Unknown SLVM string column mode: ${r}`,{pos:e.pos})}return i}function ce(e,t){let n={bytes:e,pos:0},r=B(n),i=B(n),a=B(n),o=r+i+a,s=B(n),c=Array(s);for(let t=0;t<s;t++){let r=B(n);c[t]=O(e.subarray(n.pos,n.pos+r)),n.pos+=r}let l=Array(r),u=Array(r);for(let e=0;e<r;e++)l[e]=B(n),u[e]=B(n);for(let e=0;e<i;e++)B(n);let d=B(n),f=[];for(let e=0;e<d;e++)f.push({materialId:B(n),meshCount:B(n)});let p=se(n,o,c),m=se(n,o,c),h=Array(o),g=B(n);for(let e=0;e<g;e++){let e=c[B(n)]??``,t=B(n),r=Array(t),i=0;for(let e=0;e<t;e++)i+=B(n),r[e]=i;for(let i=0;i<t;i++){let t=c[B(n)]??``,a=r[i];(h[a]??={})[e]=t}}let _=f.length>0||r===0?f:[{materialId:0,meshCount:r}],v=[],y=0,b=0,x=0;for(let e of _){let n=[];for(let i=0;i<e.meshCount&&y<r;i++,y++){let e=h[y],r=e?.id;e!==void 0&&delete e.id;let i={id:r,name:p[y],layer:m[y],vertexCount:l[y],indexCount:u[y]*3,vertexStart:b,indexStart:x,metadata:e??{}},a=t?.[y];(a===1||a===2)&&(i.closed=a===2),n.push(i),b+=l[y],x+=u[y]*3}v.push({materialId:e.materialId,meshes:n})}return{groups:v}}function le(e,t){if(e===null)return[];let n;try{n=JSON.parse(e).materials??[]}catch(t){throw z(`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 ue){let r=e[n];if(r?.startsWith(`slvm:tex:`)){let i=t[Number(r.slice(9))];i!==void 0&&(e[n]=de(i))}}return n}const ue=[`map`,`roughnessMap`,`normalMap`];function de(e){let t={bytes:e,pos:0},n=B(t),r=O(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 fe(e){let t;t=e.isFloat32?e.vertexData:e.planarByteSplit?I(e.vertexData,e.vertexCount):e.deltaEncoded?F(e.vertexData):e.vertexData;let n;if(e.planarByteSplit){let t=e.indexData;n=e.uint16Indices?ee(t,t.length/2):L(t,t.length/4)}else n=e.deltaEncoded?e.indexData instanceof Uint16Array?R(e.indexData):te(e.indexData):e.indexData;return N(n,e.vertexCount),{metadata:e.metadata,flags:e.flags,vertices:t,indices:n,origin:e.origin,scale:e.scale,uvs:e.uvs,colors:e.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(ie(n)){let e=ae(n),t=V(e.geometryBlob);return t.metadata=e.metadata,t}let r=E(n),i=new DataView(r.buffer,r.byteOffset,r.byteLength);if(r.byteLength<12)throw z(`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 z(`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 z(`Unsupported SLVA version: ${s}`,{minSupportedVersion:1,maxSupportedVersion:4,actualVersion:s});let c=i.getUint32(a,!0);if(a+=4,a+c>r.byteLength)throw z(`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(O(l))}catch(e){throw z(`Failed to parse metadata JSON: ${e instanceof Error?e.message:String(e)}`,{metadataLen:c})}if(a+56>r.byteLength)throw z(`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 x=!!(d&1),S=!!(d&4),C=!!(d&32),w=v*3,T=w*(x?4:2);if(a+T>r.byteLength)throw z(`Insufficient data to read vertices.`,{expectedBytes:T,availableBytes:r.byteLength-a,offset:a,useFloat32:x,vertexCount:v});let D=r.byteOffset+a,N;if(N=x?A(r.buffer,D,w):C?r.subarray(a,a+T):S?j(r.buffer,D,w):k(r.buffer,D,w),a+=T,a+4>r.byteLength)throw z(`Insufficient data to read index count.`,{expectedBytes:4,availableBytes:r.byteLength-a,offset:a});let P=i.getUint32(a,!0);a+=4;let F=!!(d&2),I=P*(F?2:4);if(a+I>r.byteLength)throw z(`Insufficient data to read indices.`,{expectedBytes:I,availableBytes:r.byteLength-a,offset:a,indexCount:P,useUint16Indices:F});let ee=C?r.subarray(a,a+I):F?j(r.buffer,r.byteOffset+a,P):M(r.buffer,r.byteOffset+a,P);a+=I;let L=null;if(d&8){let e=ne(r,i,a,v,S,C);L=e.uvs,a=e.offset}let R=null;return d&16&&(R=re(r,a,v,S)),{metadata:u,flags:d,vertexData:N,indexData:ee,isFloat32:x,deltaEncoded:S,planarByteSplit:C,uint16Indices:F,vertexCount:v,origin:[f,p,m],scale:[h,g,_],uvs:L,colors:R}}function pe(e,t,n){if(n.length===0)return!1;let r=e=>{let t=16;for(;t<e*2;)t*=2;return t},i=e.length/3,a=new Int32Array(i),o=new Int32Array(i),s=/* @__PURE__ */ new Int32Array,c=/* @__PURE__ */ new Int32Array,l=/* @__PURE__ */ new Int32Array,u=/* @__PURE__ */ new Int32Array,d=/* @__PURE__ */ new Int32Array,f=/* @__PURE__ */ new Int32Array,p=/* @__PURE__ */ new Int32Array,m=/* @__PURE__ */ new Int32Array,h=/* @__PURE__ */ new Int32Array,g=(e,t)=>e.length>=t?e:new Int32Array(t),_=(n,i,_)=>{if(_<12||_%3!=0)return!1;let v=2*n+1,y=v+1,b=0,x=0;for(let n=0;n<_;n++){let r=t[i+n];a[r]!==v&&(a[r]=v,x++,b=Math.max(b,Math.abs(e[r*3]),Math.abs(e[r*3+1]),Math.abs(e[r*3+2])))}if(!(b>0)||!Number.isFinite(b))return!1;let S=1/(2*2**(Math.floor(Math.log2(b))-23)),C=r(x),w=C-1;s=g(s,C),s.fill(-1,0,C),c=g(c,C),l=g(l,C),u=g(u,C);let T=(e,t,n)=>{let r=Math.imul(e,2654435761)^Math.imul(t,2246822519)^Math.imul(n,3266489917);for(r=(r^r>>>15)&w;s[r]!==-1&&(c[r]!==e||l[r]!==t||u[r]!==n);)r=r+1&w;return r},E=(e,t)=>{let n=e-t;return n>.25?t+1:n<-.25?t-1:t};d=g(d,_),f=g(f,x);let D=0;for(let n=0;n<_;n++){let r=t[i+n];if(a[r]===y){d[n]=o[r];continue}let p=e[r*3]*S,m=e[r*3+1]*S,h=e[r*3+2]*S,g=Math.round(p),_=Math.round(m),v=Math.round(h),b=E(p,g),x=E(m,_),C=E(h,v),w=-1,O=-1;for(let e=0;e<8&&w===-1;e++){if(e&1&&b===g||e&2&&x===_||e&4&&C===v)continue;let t=T(e&1?b:g,e&2?x:_,e&4?C:v);e===0&&(O=t),s[t]!==-1&&(w=s[t])}if(w===-1){let e=O;w=D++,s[e]=w,c[e]=g,l[e]=_,u[e]=v,f[w]=r}a[r]=y,o[r]=w,d[n]=w}p=g(p,D+1),p.fill(0,0,D+1);let O=0;for(let e=0;e<_;e+=3){let t=d[e],n=d[e+1],r=d[e+2];t!==n&&n!==r&&r!==t&&(p[t+1]+=1,p[n+1]+=1,p[r+1]+=1,O++)}if(O<4)return!1;for(let e=0;e<D;e++)p[e+1]+=p[e];m=g(m,p[D]),h=g(h,D),h.set(p.subarray(0,D));for(let e=0;e<_;e+=3){let t=d[e],n=d[e+1],r=d[e+2];t!==n&&n!==r&&r!==t&&(m[h[t]]=n,h[t]+=1,m[h[n]]=r,h[n]+=1,m[h[r]]=t,h[r]+=1)}for(let e=0;e<D;e++){let t=p[e],n=p[e+1];for(let r=t;r<n;r++){let n=m[r];for(let e=t;e<r;e++)if(m[e]===n)return!1;let i=!1;for(let t=p[n],r=p[n+1];t<r&&!i;t++)i=m[t]===e;if(!i)return!1}}let k=f[d[0]]*3,A=e[k],j=e[k+1],M=e[k+2],N=0;for(let t=0;t<_;t+=3){let n=f[d[t]]*3,r=f[d[t+1]]*3,i=f[d[t+2]]*3,a=e[n]-A,o=e[n+1]-j,s=e[n+2]-M,c=e[r]-A,l=e[r+1]-j,u=e[r+2]-M,p=e[i]-A,m=e[i+1]-j,h=e[i+2]-M;N+=a*(l*h-u*m)+o*(u*p-c*h)+s*(c*m-l*p)}return N>0},v=0;for(let e=0;e<n.length;e++){let t=n[e];if(!_(e,v,t))return!1;v+=t}return!0}function me(e,t,n,r){let i=Math.cos(30*Math.PI/180),a=e.length/3,o=t.length,s=o/3,c=/* @__PURE__ */ new Uint32Array;{let i=new Uint32Array(e.buffer,e.byteOffset,e.length),s=n?new Uint32Array(n.buffer,n.byteOffset,n.length):null,l=(e,t)=>e[t]===2147483648?0:e[t],u=16;for(;u<a*2;)u*=2;let d=u-1,f=new Int32Array(u).fill(-1),p=new Uint32Array(a),m=!1;for(let e=0;e<a;e++){let t=l(i,e*3),n=l(i,e*3+1),a=l(i,e*3+2),o=Math.imul(t,2654435761)^Math.imul(n,2246822519)^Math.imul(a,3266489917);for(o=(o^o>>>15)&d;;){let c=f[o];if(c===-1){f[o]=e,p[e]=e;break}if(l(i,c*3)===t&&l(i,c*3+1)===n&&l(i,c*3+2)===a&&(!r||r[c*3]===r[e*3]&&r[c*3+1]===r[e*3+1]&&r[c*3+2]===r[e*3+2])&&(!s||s[c*2]===s[e*2]&&s[c*2+1]===s[e*2+1])){p[e]=c,m=!0;break}o=o+1&d}}if(m){for(let e=0;e<o;e++)t[e]=p[t[e]];let e=0;for(let t=0;t<a;t++)p[t]!==t&&e++;c=new Uint32Array(e);for(let e=0,t=0;e<a;e++)p[e]!==e&&(c[t++]=e)}}let l=new Float32Array(s*3),u=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],c=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-c*f,h=c*d-o*p,g=o*f-s*d,_=Math.sqrt(m*m+h*h+g*g);_!==0&&(l[n*3]=m/_,l[n*3+1]=h/_,l[n*3+2]=g/_,u[n]=_)}let d=new Uint32Array(a+1);for(let e=0;e<o;e++)d[t[e]+1]+=1;for(let e=0;e<a;e++)d[e+1]+=d[e];let f=new Uint32Array(o),p=d.slice(0,a);for(let e=0;e<o;e++){let n=t[e];f[p[n]]=e,p[n]+=1}let m=0;for(let e=0;e<a;e++)m=Math.max(m,d[e+1]-d[e]);let h=new Uint32Array(o),g=new Uint32Array(a),_=new Float32Array(Math.max(1,m)*3);for(let e=0;e<a;e++){let t=0;for(let n=d[e];n<d[e+1];n++){let e=f[n],r=Math.floor(e/3)*3,a=l[r],o=l[r+1],s=l[r+2],c=-1;if(a===0&&o===0&&s===0)c=0;else for(let e=0;e<t;e++)if(_[e*3]*a+_[e*3+1]*o+_[e*3+2]*s>=i){c=e;break}c===-1&&(c=t,_[t*3]=a,_[t*3+1]=o,_[t*3+2]=s,t++),h[e]=c}g[e]=Math.max(1,t)}let v=new Uint32Array(a+1);for(let e=0;e<a;e++)v[e+1]=v[e]+g[e]-1;let y=v[a],b=a+Math.max(0,y-c.length),x=new Uint32Array(y);for(let e=0;e<y;e++)x[e]=e<c.length?c[e]:a+e-c.length;let S=e,C=n,w=r;if(y>0){S=new Float32Array(b*3),S.set(e),C=n?new Float32Array(b*2):null,C&&n&&C.set(n),w=r?new Uint8Array(b*3):null,w&&r&&w.set(r);for(let t=0;t<a;t++)for(let i=1;i<g[t];i++){let a=x[v[t]+i-1];S[a*3]=e[t*3],S[a*3+1]=e[t*3+1],S[a*3+2]=e[t*3+2],C&&n&&(C[a*2]=n[t*2],C[a*2+1]=n[t*2+1]),w&&r&&(w[a*3]=r[t*3],w[a*3+1]=r[t*3+1],w[a*3+2]=r[t*3+2])}}let T=new Float32Array(b*3);for(let e=0;e<s;e++){let n=u[e],r=l[e*3]*n,i=l[e*3+1]*n,a=l[e*3+2]*n;for(let n=e*3;n<e*3+3;n++){let e=t[n],o=h[n];o!==0&&(e=x[v[e]+o-1],t[n]=e),T[e*3]+=r,T[e*3+1]+=i,T[e*3+2]+=a}}for(let e=0;e<T.length;e+=3){let t=T[e],n=T[e+1],r=T[e+2],i=Math.sqrt(t*t+n*n+r*r)||1;T[e]=t/i,T[e+1]=n/i,T[e+2]=r/i}return{positions:S,normals:T,indices:t,uvs:C,colors:w}}function he(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 H(e,t){let n=t.every(e=>typeof e.closed==`boolean`)?t.length>0&&t.every(e=>e.closed):pe(e.positions,e.indices,t.map(e=>e.indexCount)),r=me(e.positions,e.indices,e.uvs,e.colors),i=r.uvs?he(r.positions,r.normals,r.indices,r.uvs):null;return{...r,closed:n,tangents:i}}function ge(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 _e(){return[`const assemble = ${ge.toString()};`,`const isClosedSolid = ${pe.toString()};`,`const splitCreases = ${me.toString()};`,`const grainTangents = ${he.toString()};`,`self.onmessage = (event) => {`,` const { id, input } = event.data;`,` try {`,` const geometries = assemble(input).map((g, i) => {`,` const windows = input.jobs[i].windows;`,` const known = windows.every((w) => typeof w.closed === 'boolean');`,` const closed = known`,` ? windows.length > 0 && windows.every((w) => w.closed)`,` : isClosedSolid(g.positions, g.indices, windows.map((w) => w.indexCount));`,` 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
|
-
|
|
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-fRI12OCL.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( -${
|
|
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( -${
|
|
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(
|
|
1875
|
+
}`).replace("#include <color_fragment>", `#include <color_fragment>
|
|
46
1876
|
float footprint = max( fwidth( vUv.y ), 1e-6 );
|
|
47
|
-
float selvaFibre = ( ${
|
|
48
|
-
float selvaStreak = selvaFibre + ( ${
|
|
49
|
-
diffuseColor.rgb *= 1.0 + 0.5 * selvaStreak;`).replace(
|
|
50
|
-
roughnessFactor = clamp( roughnessFactor * ( 1.0 + selvaStreak ), 0.0, 1.0 );`).replace(
|
|
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 + ${
|
|
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 q(e,n){return new t.g(e,t.h.VALIDATION_ERROR,{context:n})}function J(e,t,n,r){for(let i of e){if(!Number.isInteger(i.materialId)||i.materialId<0||i.materialId>=t)throw q(`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 q(`Mesh metadata field "${n}" must be a non-negative integer.`,{meshName:e.name,field:n,value:r});if(e.vertexStart+e.vertexCount>n)throw q(`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 q(`Mesh index window exceeds the batch index buffer.`,{meshName:e.name,indexStart:e.indexStart,indexCount:e.indexCount,totalIndexCount:r})}}}function je(e,t){return q(`Index references a vertex outside its mesh's vertex window.`,{meshName:t.name,indexValue:e,vertexStart:t.vertexStart,vertexCount:t.vertexCount})}function Me(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 Ne(e){let t=/* @__PURE__ */ 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 Pe(e){let t=/* @__PURE__ */ new Map;for(let n of e){let e=t.get(n.materialId);if(e)for(let t of n.meshes)e.push(t);else t.set(n.materialId,n.meshes.slice())}return[...t].flatMap(([e,t])=>Ne({materialId:e,meshes:t}))}function Fe(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 je(t,r);l[p+e]=t+m}f+=r.vertexCount,p+=r.indexCount}let m=H({positions:c,indices:l,uvs:u,colors:d},e.meshes);return Le(Y(m),e,r(e.materialId,m.closed))}function Y(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 Ie(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 Le(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:Ie(t)},i}function Re(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 je(t,s);f[e]=t-p}let h=s.vertexStart*2,g=H({positions:u,indices:f,uvs:i?i.slice(h,h+s.vertexCount*2):null,colors:a?a.slice(c,c+l):null},[s]);o.push(ze(Y(g),s,r(e.materialId,g.closed)))}return o}function ze(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}const Be=268435456,X=/* @__PURE__ */ new Map;let Z=0;function Ve(e,t){let n=Ge(t),r=Ke(e,n),i=X.get(r);return!i||!qe(i.jobShape,n)||!Je(i.geometryBytes,e)?null:(X.delete(r),X.set(r,i),i)}function He(e){let{geometries:t,...n}=e;return{...n,buffers:t.map(Y),closed:t.map(e=>e.closed)}}function Ue(t,n,r){let i=He(r),a=t.byteLength;for(let e of r.geometries)a+=e.positions.byteLength+e.normals.byteLength+e.indices.byteLength,a+=(e.uvs?.byteLength??0)+(e.colors?.byteLength??0)+(e.tangents?.byteLength??0);if(a>Be)return i;let o=Ge(n),s=Ke(t,o),c=X.get(s);c&&(X.delete(s),Q(c)),i.buffers.forEach(e.a),X.set(s,{...i,geometryBytes:t.slice(),jobShape:o,bytes:a}),Z+=a;for(let[e,t]of X){if(Z<=Be)break;X.delete(e),Q(t)}return i}function We(){X.forEach(Q),X.clear()}function Q(t){Z-=t.bytes;for(let n of t.buffers)e.o(n),n.dispose()}function Ge(e){let t=0;for(let n of e)t+=2+4*n.windows.length;let n=new Int32Array(t),r=0;for(let t of e){n[r++]=+(t.kind===`merged`),n[r++]=t.windows.length;for(let e of t.windows)n[r++]=e.vertexStart,n[r++]=e.vertexCount,n[r++]=e.indexStart,n[r++]=e.indexCount}return n}function Ke(e,t){let n=e.length,r=Math.max(1,Math.floor(n/1024)),i=2166136261;for(let t=0;t<n;t+=r)i=Math.imul(i^e[t],16777619);let a=2166136261;for(let e=0;e<t.length;e++)a=Math.imul(a^t[e],16777619);return`${n}:${i>>>0}:${t.length}:${a>>>0}`}function qe(e,t){if(e.length!==t.length)return!1;for(let n=0;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}function Je(e,t){if(e.length!==t.length)return!1;let n=0;if(e.byteOffset%4==0&&t.byteOffset%4==0){let r=e.length>>>2,i=new Uint32Array(e.buffer,e.byteOffset,r),a=new Uint32Array(t.buffer,t.byteOffset,r);for(;n<r;n++)if(i[n]!==a[n])return!1;n=r<<2}for(;n<e.length;n++)if(e[n]!==t[n])return!1;return!0}async function Ye(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=performance.now(),l={mergeByMaterial:r,debug:i,material:a,fallback:{materials:e.materials,groups:e.groups}},u=await et(e.compressedData,l);return u.meshes?u.meshes:await it(u,l)||$e(fe(u.raw),{mergeByMaterial:r,debug:i,material:a,parseTime:o,decodeTime:performance.now()-c,perfStart:s,blobBytes:i?ct(e.compressedData):0,fallback:{materials:e.materials,groups:e.groups}})}async function Xe(e,t){let{mergeByMaterial:n=!0,debug:r=!1,material:i}=t??{},a=r?performance.now():0,o=performance.now(),s={mergeByMaterial:n,debug:r,material:i},c=await et(e,s);if(c.meshes)return c.meshes;let l=await it(c,s);if(l)return l;let u=fe(c.raw),d=performance.now()-o,f=e.byteLength;return $e(u,{mergeByMaterial:n,debug:r,material:i,parseTime:0,decodeTime:d,perfStart:a,blobBytes:f})}let Ze=!1;function Qe(e,n){if(e||Ze)return;let r=0;for(let e of n)r+=e.meshes.length;r<1e3||(Ze=!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 $e(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);J(f,d.length,e.vertices.length/3,e.indices.length);let m=p?e.vertices:Me(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`)}Qe(r,f);let h=performance.now(),g=De(d,{vertexColors:e.colors!=null,hasUvs:e.uvs!=null,appearance:a}),_=[],v=r?Pe(f):f;for(let t of v)if(r&&t.meshes.length>1){let n=Fe(t,m,e.indices,g,e.uvs,e.colors);_.push(n)}else{let n=Re(t,m,e.indices,g,e.uvs,e.colors);for(let e of n)_.push(e)}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 et(e,t){let n=b(e);if(!ie(n))return{raw:V(n),geometryBytes:null,source:n,reparse:()=>V(n)};let r=ae(n),i=nt(r,t);if(i)return{meshes:i};let a=r.geometryBlob,o=()=>{let e=V(a);return e.metadata=r.metadata,e},s=await tt(r),c=s?V(s):o();return c.metadata=r.metadata,{raw:c,geometryBytes:a,source:a,reparse:o}}async function tt(e){if(S(e.geometryBlob)<4194304||!ye())return null;try{return await D(e.geometryBlob)}catch{return null}}function nt(e,t){let n=e.metadata.materials??t.fallback?.materials??[],r=e.metadata.groups??t.fallback?.groups??[],{jobs:i,jobRefs:a}=rt(r,t.mergeByMaterial),o=Ve(e.geometryBlob,i);return o?(J(r,n.length,o.vertexCount,o.indexCount),st(o,a,n,t.material)):null}function rt(e,t){let n=e=>({vertexStart:e.vertexStart,vertexCount:e.vertexCount,indexStart:e.indexStart,indexCount:e.indexCount,closed:e.closed}),r=[],i=[],a=t?Pe(e):e;for(let e of a)if(t&&e.meshes.length>1)r.push({kind:`merged`,windows:e.meshes.map(n)}),i.push({kind:`merged`,group:e});else for(let t of e.meshes)r.push({kind:`single`,windows:[n(t)]}),i.push({kind:`single`,group:e,meshMeta:t});return{jobs:r,jobRefs:i}}async function it(e,n){let{raw:r,geometryBytes:i}=e,a=r.planarByteSplit?r.indexData.length/(r.uint16Indices?2:4):r.indexData.length;if(a/3<5e4)return null;let o=r.metadata.materials??n.fallback?.materials??[],s=r.metadata.groups??n.fallback?.groups??[];J(s,o.length,r.vertexCount,a);let{jobs:c,jobRefs:l}=rt(s,n.mergeByMaterial),u=await ot(r,c,e.source);if(!u)return at(r,e.source)&&(e.raw=e.reparse()),null;let d={geometries:u,hasUvs:r.uvs!=null,hasColors:r.colors!=null,vertexCount:r.vertexCount,indexCount:a},f=i?Ue(i,c,d):He(d);return n.debug&&t.p().debug(`Mesh batch assembled: ${u.length} geometries, ${a/3} triangles`),st(f,l,o,n.material)}function at(e,t){return e.vertexData.buffer!==t.buffer&&e.indexData.buffer!==t.buffer}async function ot(e,n,r){let i=ye(),a={vertexData:e.vertexData,isFloat32:e.isFloat32,deltaEncoded:e.deltaEncoded,planarByteSplit:e.planarByteSplit,uint16Indices:e.uint16Indices,origin:e.origin,scale:e.scale,indexData:e.indexData,uvs:e.uvs,colors:e.colors,jobs:n};if(!i)return ge(a).map((e,t)=>H(e,n[t].windows));at(e,r)||(a.vertexData=e.vertexData.slice(),a.indexData=e.indexData.slice(),a.uvs=e.uvs?.slice()??null,a.colors=e.colors?.slice()??null);let o=/* @__PURE__ */ new Set([a.vertexData.buffer,a.indexData.buffer]);a.uvs&&o.add(a.uvs.buffer),a.colors&&o.add(a.colors.buffer);try{let e=await be(i,a,[...o]);return e.length===n.length?e:null}catch(e){return t.p().warn(`Mesh assembly worker failed; falling back to main-thread parse.`,e),null}}function st(e,t,n,r){let i=De(n,{vertexColors:e.hasColors,hasUvs:e.hasUvs,appearance:r}),a=[];for(let n=0;n<e.buffers.length;n++){let r=e.buffers[n],o=t[n],s=i(o.group.materialId,e.closed[n]),c=o.kind===`merged`?Le(r,o.group,s):ze(r,o.meshMeta,s);a.push(c)}return a}function ct(e){return Math.floor(e.length*3/4)}const $={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 lt(e){let t=e.split(`.`);return t.includes(`Display`)||t.includes(`DisplayBatch`)}const ut=/* @__PURE__ */ new Set;async function dt(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 pt(e,r,i?ft(e.modelunits):1,c,s),a&&_t(r,o),r}catch(e){throw vt(e,r),e}finally{s&&bt(n)}}function ft(e){let n=$[e];return n===void 0?(ut.has(e)||(ut.add(e),t.p().warn(`Unknown Rhino model unit "${e}" — geometry will not be scaled (factor 1). Known units: ${Object.keys($).join(`, `)}.`)),1):n}async function pt(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 mt(o,t,n,r,i)}}}async function mt(e,n,r,i,a){for(let o of e){if(!lt(o.type))continue;let e={debug:!1,...i,mergeByMaterial:i.mergeByMaterial??!0},s=ht(o.data);if(!s){t.p().error(`Error parsing display batch envelope: invalid JSON`);continue}let c=await Ye(s,e),l=v(s.items),u=[...c,...l];if(r!==1)for(let e of u)e.scale.set(r,r,r);for(let e of u)n.push(e);a&&t.p().debug(`Extracted ${c.length} meshes and ${l.length} items from batch`)}}function ht(e){return typeof e==`string`?gt(e):e}function gt(e){try{return JSON.parse(e)}catch{return}}function _t(e,n){if(e.length===0)return;let r=t.a(e);t.i(e,r.min[n],n)}function vt(e,n){t.p().error(`An unexpected error occurred:`,e),yt(n)}function yt(e){for(let n of e)t.t(n)}function bt(e){let n=performance.now()-e;t.p().info(`Time to process meshes:`,`${n.toFixed(2)}ms`)}exports.SCALE_FACTORS=$,exports.clearGeometryCache=We,exports.getThreeObjectsFromComputeResponse=dt,exports.meshPolicy=p,exports.parseDisplayItems=v,exports.parseMeshBatchBlob=Xe,exports.parseMeshBatchObject=Ye,exports.setTextureAnisotropy=Se;
|
|
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
|