1var e=document.createElement(`style`);e.textContent=` 2 /* ââ Layout ââ */ 3 .spz-workspace { 4 display: grid; 5 grid-template-columns: minmax(360px, 1fr) minmax(360px, 1.15fr); 6 gap: 24px; 7 align-items: stretch; 8 } 9 @media (max-width: 860px) { 10 .spz-workspace { grid-template-columns: 1fr; } 11 } 12 13 /* ââ Header ââ */ 14 .spz-header { margin-bottom: 40px; } 15 .spz-header-desc { font-size: 16px; line-height: 24px; color: #6b7280; max-width: 640px; margin-top: 8px; } 16 .spz-eyebrow { font-size: 12px; font-weight: 500; line-height: 16px; color: #678BC7; letter-spacing: 0.14em; text-transform: uppercase; } 17 18 /* ââ Drop panel ââ */ 19 .spz-drop-panel { 20 background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; 21 padding: 24px; box-shadow: 0 1px 4px rgba(0,0,0,0.06); 22 display: flex; flex-direction: column; justify-content: center; 23 } 24 .spz-drop-panel-label { 25 font: 600 11px/16px var(--font-mono, monospace); 26 color: #6b7280; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 16px; 27 } 28 29 /* ââ Drop zone ââ */ 30 .spz-drop-zone { 31 position: relative; border: 1.5px dashed #d1d5db; border-radius: 12px; 32 padding: 48px 24px; text-align: center; cursor: pointer; user-select: none; 33 transition: border-color 150ms ease, background 150ms ease; 34 } 35 .spz-drop-zone:hover, .spz-drop-zone[data-spz-over] { border-color: #678bc7; background: #eef2f9; } 36 .spz-drop-zone[data-spz-over] { border-style: solid; } 37 .spz-drop-zone input[type='file'] { display: none; } 38 39 .spz-drop-illus { 40 width: 72px; height: 72px; margin: 0 auto 16px; 41 background: #eef2f9; border-radius: 999px; 42 display: flex; align-items: center; justify-content: center; color: #678bc7; 43 } 44 .spz-drop-illus svg { width: 32px; height: 32px; } 45 .spz-drop-title { font-size: 16px; font-weight: 500; line-height: 24px; color: #111827; margin-bottom: 4px; } 46 .spz-drop-title strong { color: #678bc7; font-weight: 600; } 47 .spz-drop-hint { font-size: 13px; line-height: 20px; color: #6b7280; } 48 .spz-drop-formats { display: inline-flex; gap: 6px; margin-top: 14px; } 49 .spz-format-chip { 50 display: inline-flex; align-items: center; height: 22px; padding: 0 10px; border-radius: 999px; 51 font: 600 10px/14px var(--font-mono, monospace); letter-spacing: 0.1em; text-transform: uppercase; 52 background: #f3f4f6; color: #6b7280; border: 1px solid #e5e7eb; 53 } 54 .spz-privacy { 55 margin-top: 16px; padding: 12px 14px; 56 background: #eef2f9; border: 1px solid #c7d8ef; border-radius: 12px; 57 display: flex; align-items: flex-start; gap: 10px; 58 font-size: 12px; line-height: 18px; color: #3a5a8c; 59 } 60 61 /* ââ Result panel ââ */ 62 .spz-result-panel { 63 background: #fff; border: 1px solid #e5e7eb; border-radius: 16px; 64 box-shadow: 0 1px 4px rgba(0,0,0,0.06); 65 overflow: hidden; min-height: 520px; display: flex; flex-direction: column; 66 } 67 68 /* ââ Empty state ââ */ 69 .spz-empty-state { flex: 1; flex-direction: column; align-items: center; justify-content: center; text-align: center; padding: 48px 32px; } 70 .spz-empty-illus { width: 120px; height: 120px; margin-bottom: 24px; opacity: 0.9; filter: drop-shadow(0 4px 12px rgba(45,82,144,0.12)); } 71 .spz-empty-illus-fallback { width: 120px; height: 120px; margin-bottom: 24px; background: #eef2f9; border-radius: 999px; align-items: center; justify-content: center; color: #93b4da; } 72 .spz-empty-illus-fallback svg { width: 48px; height: 48px; } 73 .spz-empty-title { font-size: 18px; font-weight: 500; line-height: 24px; color: #111827; margin-bottom: 6px; } 74 .spz-empty-desc { font-size: 13px; line-height: 20px; color: #6b7280; max-width: 320px; } 75 .spz-empty-steps { display: flex; gap: 8px; margin-top: 24px; font: 500 11px/16px var(--font-mono, monospace); color: #9ca3af; letter-spacing: 0.06em; text-transform: uppercase; } 76 .spz-empty-steps .step { padding: 4px 10px; background: #f3f4f6; border-radius: 999px; } 77 .spz-empty-steps .sep { color: #d1d5db; } 78 79 /* ââ Loading state ââ */ 80 .spz-loading-state { flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 16px; padding: 48px 32px; } 81 .spz-spinner { width: 36px; height: 36px; border: 3px solid #dbeafe; border-top-color: #678bc7; border-radius: 50%; animation: spz-spin 0.9s linear infinite; } 82 @keyframes spz-spin { to { transform: rotate(360deg); } } 83 .spz-loading-label { font-size: 13px; font-weight: 500; line-height: 18px; color: #111827; } 84 .spz-loading-sub { font: 12px/16px var(--font-mono, monospace); color: #6b7280; letter-spacing: 0.04em; } 85 86 /* ââ Error state ââ */ 87 .spz-error-state { flex: 1; flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: 48px 32px; text-align: center; } 88 .spz-error-icon { width: 48px; height: 48px; border-radius: 999px; background: #fee2e2; color: #dc2626; display: flex; align-items: center; justify-content: center; } 89 .spz-error-title { font-size: 15px; font-weight: 600; line-height: 20px; color: #dc2626; margin: 0; } 90 .spz-error-msg { font-size: 13px; line-height: 20px; color: #6b7280; max-width: 360px; margin: 0; } 91 92 /* ââ Info state ââ */ 93 .spz-info-state { flex: 1; flex-direction: column; } 94 .spz-result-head { padding: 20px 24px; border-bottom: 1px solid #f3f4f6; display: flex; align-items: center; gap: 12px; } 95 .spz-file-icon { width: 40px; height: 40px; flex: none; border-radius: 8px; background: #111827; display: flex; align-items: center; justify-content: center; color: #22d3ee; font: 600 10px/12px var(--font-mono, monospace); letter-spacing: 0.06em; } 96 .spz-file-icon[data-spz-type='ply'] { color: #f97316; } 97 .spz-file-meta { flex: 1; min-width: 0; } 98 .spz-file-name { font-size: 14px; font-weight: 500; line-height: 20px; color: #111827; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } 99 .spz-file-sub { font: 11px/14px var(--font-mono, monospace); color: #6b7280; letter-spacing: 0.04em; margin-top: 2px; } 100 .spz-reset-btn { height: 32px; padding: 0 12px; background: transparent; border: 1px solid #e5e7eb; border-radius: 8px; cursor: pointer; font: 500 12px/18px sans-serif; color: #6b7280; display: inline-flex; align-items: center; gap: 6px; transition: background 150ms ease, color 150ms ease; } 101 .spz-reset-btn:hover { background: #f9fafb; color: #111827; } 102 103 .spz-info-body { flex: 1; overflow: auto; padding: 24px; } 104 .spz-info-section + .spz-info-section { margin-top: 24px; } 105 .spz-info-section-label { font: 600 11px/14px var(--font-mono, monospace); color: #6b7280; letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 12px; } 106 107 .spz-stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } 108 .spz-stat-tile { background: #f9fafb; border-radius: 10px; padding: 14px 16px; } 109 .spz-stat-key { font: 500 10px/14px var(--font-mono, monospace); color: #6b7280; letter-spacing: 0.08em; text-transform: uppercase; margin-bottom: 6px; } 110 .spz-stat-val { font-size: 20px; font-weight: 600; line-height: 24px; color: #111827; font-variant-numeric: tabular-nums; letter-spacing: -0.01em; } 111 .spz-stat-val--mono { font: 500 16px/20px var(--font-mono, monospace); letter-spacing: 0.02em; } 112 .spz-stat-unit { font: 500 12px/16px var(--font-mono, monospace); color: #6b7280; margin-left: 2px; } 113 114 .spz-bbox-table { background: #f9fafb; border-radius: 10px; padding: 4px 16px; } 115 .spz-bbox-row { display: grid; grid-template-columns: 20px 1fr auto 1fr; gap: 12px; align-items: center; padding: 10px 0; border-bottom: 1px solid #f3f4f6; } 116 .spz-bbox-row:last-child { border-bottom: 0; } 117 .spz-bbox-row__axis { font: 600 12px/16px var(--font-mono, monospace); color: #9ca3af; letter-spacing: 0.1em; } 118 .spz-bbox-row__num { font: 500 13px/18px var(--font-mono, monospace); color: #111827; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; } 119 .spz-bbox-row__arrow { color: #d1d5db; } 120 .spz-bbox-row__extent { font: 400 11px/14px var(--font-mono, monospace); color: #6b7280; margin-left: 8px; } 121 122 .spz-size-compare { background: #f9fafb; border-radius: 10px; padding: 14px 16px; } 123 .spz-size-compare-row { display: grid; grid-template-columns: 52px 1fr 80px; gap: 12px; align-items: center; padding: 6px 0; } 124 .spz-size-compare-row__lbl { font: 600 10px/14px var(--font-mono, monospace); color: #6b7280; letter-spacing: 0.08em; text-transform: uppercase; } 125 .spz-size-compare-row__bar-wrap { background: #e5e7eb; border-radius: 999px; height: 8px; overflow: hidden; } 126 .spz-size-compare-row__bar { height: 100%; border-radius: 999px; background: #678bc7; transition: width 400ms ease; } 127 .spz-size-compare-row__bar--alt { background: #f97316; } 128 .spz-size-compare-row__val { font: 500 12px/16px var(--font-mono, monospace); color: #111827; text-align: right; font-variant-numeric: tabular-nums; } 129 130 .spz-actions { padding: 16px 24px; border-top: 1px solid #f3f4f6; background: #f9fafb; display: flex; gap: 8px; flex-wrap: wrap; } 131 132 /* ââ Buttons ââ */ 133 .spz-btn { font: 500 13px/18px sans-serif; height: 38px; padding: 0 16px; border-radius: 10px; display: inline-flex; align-items: center; gap: 8px; cursor: pointer; border: 1px solid transparent; transition: background 150ms ease; white-space: nowrap; } 134 .spz-btn:focus-visible { outline: 2px solid #678bc7; outline-offset: 2px; } 135 .spz-btn--primary { background: #678bc7; color: #fff; } 136 .spz-btn--primary:hover { background: #5578b3; } 137 .spz-btn--secondary { background: #eef2f9; color: #3a5a8c; } 138 .spz-btn--secondary:hover { background: #dce8f5; } 139 .spz-btn svg { width: 16px; height: 16px; flex: none; } 140 141 /* ââ Format notice ââ */ 142 .spz-format-notice { margin-top: 24px; padding: 12px 16px; background: #fff7ed; border: 1px solid #fed7aa; border-radius: 12px; display: flex; align-items: flex-start; gap: 10px; font-size: 13px; line-height: 20px; color: #9a3412; } 143 .spz-format-notice svg { flex: none; margin-top: 2px; } 144 .spz-format-notice a { color: inherit; font-weight: 600; } 145 .spz-format-notice a:hover { text-decoration: underline; } 146 147 /* ââ Toasts ââ */ 148 .spz-toast-region { position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%); display: flex; flex-direction: column; gap: 8px; z-index: 9999; pointer-events: none; } 149 .spz-toast { min-width: 280px; max-width: 420px; padding: 12px 16px; background: #111827; color: #fff; border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,0.18); display: flex; align-items: center; gap: 10px; font: 500 13px/18px sans-serif; pointer-events: all; animation: spz-toast-in 200ms ease; } 150 .spz-toast--ok .spz-toast__icon { color: #86efac; } 151 .spz-toast--err .spz-toast__icon { color: #fca5a5; } 152 .spz-toast__close { margin-left: auto; cursor: pointer; color: rgba(255,255,255,0.5); background: none; border: none; font-size: 18px; line-height: 1; padding: 0; } 153 .spz-toast__close:hover { color: #fff; } 154 @keyframes spz-toast-in { from { transform: translateY(12px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } 155`,document.head.appendChild(e);var t=class extends HTMLElement{spz=null;moduleReady;currentFile=null;currentCloud=null;currentBuffer=null;currentType=null;currentSpzHeader=null;abortController=null;q(e){return this.querySelector(e)}connectedCallback(){this.abortController=new AbortController,this.moduleReady=this.loadSpz(),this.wireEvents(),this.showPanel(`empty`)}disconnectedCallback(){this.abortController?.abort()}async loadSpz(){try{let e=await Function(`path`,`return import(path)`)(`/js/spz.js`);this.spz=await e.default()}catch(e){console.warn(`spz.js failed to load:`,e)}}async getSpz(){if(await this.moduleReady,!this.spz)throw Error(`SPZ runtime could not be loaded.`);return this.spz}wireEvents(){let{signal:e}=this.abortController,t=this.q(`[data-spz="dropzone"]`),n=this.q(`[data-spz="file-input"]`),r=this.q(`[data-spz="reset-btn"]`);!t||!n||(t.addEventListener(`click`,()=>n.click(),{signal:e}
155),t.addEventListener(`keydown`,e=>{(e.key===`Enter`||e.key===` `)&&(e.preventDefault(),n.click())},{signal:e}),n.addEventListener(`change`,()=>{n.files?.[0]&&this.handleFile(n.files[0])},{signal:e}),[`dragenter`,`dragover`].forEach(n=>t.addEventListener(n,e=>{e.preventDefault(),t.dataset.spzOver=`true`},{signal:e})),[`dragleave`,`drop`].forEach(n=>t.addEventListener(n,e=>{(n!==`dragleave`||e.target===t)&&delete t.dataset.spzOver},{signal:e})),t.addEventListener(`drop`,e=>{e.preventDefault(),delete t.dataset.spzOver;let n=e.dataTransfer?.files[0];n&&this.handleFile(n)},{signal:e}),window.addEventListener(`dragover`,e=>e.preventDefault(),{signal:e}),window.addEventListener(`drop`,e=>{if(e.preventDefault(),e.target===t||t.contains(e.target))return;let n=e.dataTransfer?.files[0];n&&this.handleFile(n)},{signal:e}),r?.addEventListener(`click`,()=>this.resetAll(),{signal:e}))}showPanel(e){for(let{name:t,display:n}of[{name:`empty`,display:`flex`},{name:`loading`,display:`flex`},{name:`error`,display:`flex`},{name:`info`,display:`flex`}]){let r=this.q(`[data-spz="${t}"]`);r&&(r.style.display=e===t?n:`none`)}}setLoading(e,t=``){this.showPanel(`loading`);let n=this.q(`[data-spz="loading-label"]`),r=this.q(`[data-spz="loading-sub"]`);n&&(n.textContent=e),r&&(r.textContent=t)}setError(e,t,n){this.showPanel(`error`);let r=this.q(`[data-spz="error-title"]`),i=this.q(`[data-spz="error-msg"]`);r&&(r.textContent=e),i&&(n?i.innerHTML=n:i.textContent=t)}toast(e,t=`ok`){let n=this.q(`[data-spz="toasts"]`);if(!n)return;let r=document.createElement(`div`);r.className=`spz-toast spz-toast--${t}`,r.innerHTML=`${t===`ok`?`<svg class="spz-toast__icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`:`<svg class="spz-toast__icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`}<span>${e}</span><button class="spz-toast__close" aria-label="Dismiss">×</button>`,r.querySelector(`.spz-toast__close`).addEventListener(`click`,()=>r.remove()),n.appendChild(r),setTimeout(()=>r.remove(),4500)}download(e,t){let n=new Blob([t]),r=URL.createObjectURL(n),i=Object.assign(document.createElement(`a`),{href:r,download:e});document.body.appendChild(i),i.click(),document.body.removeChild(i),setTimeout(()=>URL.revokeObjectURL(r),1e4)}readFile(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=e=>t(e.target.result),r.onerror=e=>n(e.target.error||Error(`File read failed`)),r.readAsArrayBuffer(e)})}fmtBytes(e){return e<1024?e+` B`:e<1048576?(e/1024).toFixed(1)+` KB`:e<1073741824?(e/1048576).toFixed(2)+` MB`:(e/1073741824).toFixed(2)+` GB`}bbox(e){let t=1/0,n=1/0,r=1/0,i=-1/0,a=-1/0,o=-1/0;for(let s=0;s<e.length;s+=3)e[s]<t&&(t=e[s]),e[s]>i&&(i=e[s]),e[s+1]<n&&(n=e[s+1]),e[s+1]>a&&(a=e[s+1]),e[s+2]<r&&(r=e[s+2]),e[s+2]>o&&(o=e[s+2]);return[[t,i],[n,a],[r,o]]}degreeFromDim(e){return e<3?0:e<8?1:e<15?2:e<24?3:4}parsePly(e){let t=new Uint8Array(e),n=new TextEncoder().encode(`end_header 156`),r=-1;outer:for(let e=0;e<=t.length-n.length;e++){for(let r=0;r<n.length;r++)if(t[e+r]!==n[r])continue outer;r=e+n.length;break}if(r<0)throw Error(`Invalid PLY file: end_header not found.`);let i=new TextDecoder().decode(t.subarray(0,r)).split(` 157`).map(e=>e.trim()).filter(Boolean),a=``,o=0,s=!1,c=[];for(let e of i)if(e.startsWith(`format `))a=e.split(/\s+/)[1];else if(e.startsWith(`element vertex `))o=parseInt(e.split(/\s+/)[2],10),s=!0;else if(e.startsWith(`element `)&&!e.startsWith(`element vertex `))s=!1;else if(s&&e.startsWith(`property `)&&!e.startsWith(`property list`)){let[,t,n]=e.split(/\s+/);c.push({name:n,type:t})}if(a!==`binary_little_endian`)throw Error(`Unsupported PLY format "${a}". Only binary_little_endian is supported.`);let l={float:4,double:8,int:4,int32:4,uint:4,uint32:4,short:2,int16:2,ushort:2,uint16:2,char:1,int8:1,uchar:1,uint8:1},u=0,d={};for(let e of c){let t=l[e.type];if(!t)throw Error(`Unknown PLY type: "${e.type}"`);d[e.name]={offset:u,type:e.type},u+=t}for(let e of[`x`,`y`,`z`,`scale_0`,`scale_1`,`scale_2`,`rot_0`,`rot_1`,`rot_2`,`rot_3`,`opacity`,`f_dc_0`,`f_dc_1`,`f_dc_2`])if(!d[e])throw Error(`Missing PLY property: "${e}"`);let f=0;for(;d[`f_rest_${f}`];)f++;let p=Math.floor(f/3),m=new Float32Array(o*3),h=new Float32Array(o*3),g=new Float32Array(o*4),_=new Float32Array(o),v=new Float32Array(o*3),y=new Float32Array(o*p*3);
157if(c.every(e=>e.type===`float`)){let t=c.length,n=new Float32Array(e.slice(r,r+o*t*4)),i=e=>c.findIndex(t=>t.name===e),a=i(`x`),s=i(`y`),l=i(`z`),u=i(`scale_0`),d=i(`scale_1`),f=i(`scale_2`),b=i(`rot_0`),x=i(`rot_1`),S=i(`rot_2`),C=i(`rot_3`),w=i(`opacity`),T=i(`f_dc_0`),E=i(`f_dc_1`),D=i(`f_dc_2`),O=new Int32Array(p),k=new Int32Array(p),A=new Int32Array(p);for(let e=0;e<p;e++)O[e]=i(`f_rest_${e}`),k[e]=i(`f_rest_${e+p}`),A[e]=i(`f_rest_${e+2*p}`);for(let e=0;e<o;e++){let r=e*t,i=e*3,o=e*4;m[i]=n[r+a],m[i+1]=n[r+s],m[i+2]=n[r+l],h[i]=n[r+u],h[i+1]=n[r+d],h[i+2]=n[r+f],g[o]=n[r+x],g[o+1]=n[r+S],g[o+2]=n[r+C],g[o+3]=n[r+b],_[e]=n[r+w],v[i]=n[r+T],v[i+1]=n[r+E],v[i+2]=n[r+D];let c=e*p*3;for(let e=0;e<p;e++)y[c+e*3]=n[r+O[e]],y[c+e*3+1]=n[r+k[e]],y[c+e*3+2]=n[r+A[e]]}}else{let t=new DataView(e,r),n={float:(e,t)=>e.getFloat32(t,!0),double:(e,t)=>e.getFloat64(t,!0),int:(e,t)=>e.getInt32(t,!0),int32:(e,t)=>e.getInt32(t,!0),uint:(e,t)=>e.getUint32(t,!0),uint32:(e,t)=>e.getUint32(t,!0),short:(e,t)=>e.getInt16(t,!0),int16:(e,t)=>e.getInt16(t,!0),ushort:(e,t)=>e.getUint16(t,!0),uint16:(e,t)=>e.getUint16(t,!0),char:(e,t)=>e.getInt8(t),int8:(e,t)=>e.getInt8(t),uchar:(e,t)=>e.getUint8(t),uint8:(e,t)=>e.getUint8(t)},i=(e,r)=>{let i=d[e];return n[i.type](t,r*u+i.offset)};for(let e=0;e<o;e++){let t=e*3,n=e*4;m[t]=i(`x`,e),m[t+1]=i(`y`,e),m[t+2]=i(`z`,e),h[t]=i(`scale_0`,e),h[t+1]=i(`scale_1`,e),h[t+2]=i(`scale_2`,e),g[n]=i(`rot_1`,e),g[n+1]=i(`rot_2`,e),g[n+2]=i(`rot_3`,e),g[n+3]=i(`rot_0`,e),_[e]=i(`opacity`,e),v[t]=i(`f_dc_0`,e),v[t+1]=i(`f_dc_1`,e),v[t+2]=i(`f_dc_2`,e);let r=e*p*3;for(let t=0;t<p;t++)y[r+t*3]=i(`f_rest_${t}`,e),y[r+t*3+1]=i(`f_rest_${t+p}`,e),y[r+t*3+2]=i(`f_rest_${t+2*p}`,e)}}return{numPoints:o,shDegree:this.degreeFromDim(p),antialiased:!1,extensions:[],positions:m,scales:h,rotations:g,alphas:_,colors:v,sh:y}}writePly(e){let t=e.numPoints,n=e.sh&&e.sh.length>0?Math.round(e.sh.length/t/3):0,r=17+n*3,i=`ply 158format binary_little_endian 1.0 159element vertex ${t}\nproperty float x 160property float y 161property float z 162property float nx 163property float ny 164property float nz 165property float f_dc_0 166property float f_dc_1 167property float f_dc_2 168`;for(let e=0;e<n*3;e++)i+=`property float f_rest_${e}\n`;i+=`property float opacity 169property float scale_0 170property float scale_1 171property float scale_2 172property float rot_0 173property float rot_1 174property float rot_2 175property float rot_3 176end_header 177`;let a=new TextEncoder().encode(i),o=new Float32Array(t*r),{positions:s,scales:c,rotations:l,alphas:u,colors:d,sh:f}=e,p=0;for(let e=0;e<t;e++){let t=e*3,r=e*4;o[p++]=s[t],o[p++]=s[t+1],o[p++]=s[t+2],o[p++]=0,o[p++]=0,o[p++]=0,o[p++]=d[t],o[p++]=d[t+1],o[p++]=d[t+2];for(let t=0;t<n;t++)o[p++]=f[(e*n+t)*3];for(let t=0;t<n;t++)o[p++]=f[(e*n+t)*3+1];for(let t=0;t<n;t++)o[p++]=f[(e*n+t)*3+2];o[p++]=u[e],o[p++]=c[t],o[p++]=c[t+1],o[p++]=c[t+2],o[p++]=l[r+3],o[p++]=l[r],o[p++]=l[r+1],o[p++]=l[r+2]}let m=new Uint8Array(a.byteLength+o.byteLength);return m.set(a),m.set(new Uint8Array(o.buffer),a.byteLength),m}async parseSpzHeader(e){let t=new Uint8Array(e,0,2),n=t[0]===31&&t[1]===139,r;if(n){let t=new DecompressionStream(`gzip`),n=new Blob([e]).stream().pipeThrough(t);r=new Uint8Array(await new Response(n).arrayBuffer())}else r=new Uint8Array(e);if(r.byteLength<16)throw Error(`SPZ file too small for header.`);let i=new DataView(r.buffer,r.byteOffset,r.byteLength);if(i.getUint32(0,!0)!==1347635022)return null;let a={version:i.getUint32(4,!0),numPoints:i.getUint32(8,!0),shDegree:i.getUint8(12),fractionalBits:i.getUint8(13),flags:i.getUint8(14),numStreams:0},o=i.getUint8(15);return a.numStreams=o>0?o:a.version>=4?6:1,a}async handleFile(e){let t=e.name.toLowerCase();if(!t.endsWith(`.spz`)&&!t.endsWith(`.ply`)){this.setError(`Unsupported file type`,`Expected .spz or .ply â got "${e.name}".`);return}if(t.endsWith(`.ply`)&&e.size>2147483648){this.setError(`File too large (${this.fmtBytes(e.size)})`,``,`Web browsers cannot hold a PLY this large in memory, it would require several times the file size in RAM.<br>Convert to .spz with the <a href="https://github.com/nianticlabs/spz" target="_blank" rel="noopener">CLI tool</a>.`);return}if(t.endsWith(`.spz`)&&e.size>209715200){this.setError(`File too large (${this.fmtBytes(e.size)})`,``,`Web browsers cannot hold an SPZ this large in memory, it would require several times the file size in RAM.<br>Convert to .ply with the <a href="https://github.com/nianticlabs/spz" target="_blank" rel="noopener">CLI tool</a>.`);return}this.currentFile=e,this.currentType=t.endsWith(`.spz`)?`spz`:`ply`,this.setLoading(this.currentType===`spz`?`Loading SPZâ¦`:`Parsing PLYâ¦`,`Reading ${this.fmtBytes(e.size)}`);try{let t=await this.readFile(e);
177if(this.currentBuffer=t,this.currentType===`spz`){try{this.currentSpzHeader=await this.parseSpzHeader(t)}catch{this.currentSpzHeader=null}let e=await this.getSpz();this.currentCloud=e.loadSpzFromBuffer(new Uint8Array(t),{to:e.CoordinateSystem.RDF})}else this.currentSpzHeader=null,this.currentCloud=this.parsePly(t);this.renderInfo({file:e,cloud:this.currentCloud,type:this.currentType})}catch(e){console.error(e),this.setError(this.currentType===`spz`?`Failed to load SPZ`:`Failed to parse PLY`,e.message||String(e))}}renderInfo({file:e,cloud:t,type:n,spzSize:r=null,plySize:i=null}){this.showPanel(`info`);let a=(e,t)=>{let n=this.q(e);n&&(n.textContent=t)},o=this.q(`[data-spz="file-icon"]`);o&&(o.textContent=n.toUpperCase(),o.dataset.spzType=n),a(`[data-spz="file-name"]`,e.name),a(`[data-spz="file-sub"]`,`${this.fmtBytes(e.size)} · ${n===`spz`?`Compressed splat`:`Gaussian splat (PLY)`}`);let s=t.sh&&t.sh.length>0?Math.round(t.sh.length/t.numPoints/3):0,c=this.currentSpzHeader,l=this.q(`[data-spz="spz-header-section"]`);n===`spz`&&c&&l?(l.style.display=``,a(`[data-spz="stat-spz-version"]`,`v${c.version}`),a(`[data-spz="stat-spz-streams"]`,c.numStreams.toLocaleString())):l&&(l.style.display=`none`);let[[u,d],[f,p],[m,h]]=this.bbox(t.positions),g=e=>e.toFixed(3);a(`[data-spz="stat-points"]`,t.numPoints.toLocaleString()),a(`[data-spz="stat-sh-deg"]`,String(t.shDegree)),a(`[data-spz="stat-sh-dim"]`,s>0?` · ${s} coeff`:``),a(`[data-spz="stat-aa"]`,t.antialiased?`Yes`:`No`),a(`[data-spz="stat-size"]`,this.fmtBytes(e.size));let _=this.q(`[data-spz="bbox-table"]`);_&&(_.innerHTML=[[`X`,u,d],[`Y`,f,p],[`Z`,m,h]].map(([e,t,n])=>` 178 <div class="spz-bbox-row"> 179 <span class="spz-bbox-row__axis">${e}</span> 180 <span class="spz-bbox-row__num">${g(t)}</span> 181 <span class="spz-bbox-row__arrow">â</span> 182 <span class="spz-bbox-row__num">${g(n)}<span class="spz-bbox-row__extent">Î ${g(n-t)}</span></span> 183 </div>`).join(``));let v=this.q(`[data-spz="size-compare-section"]`);if(r!=null&&i!=null&&v){v.style.display=``;let e=Math.max(r,i),t=this.q(`[data-spz="ply-bar"]`),n=this.q(`[data-spz="spz-bar"]`);t&&(t.style.width=i/e*100+`%`),n&&(n.style.width=r/e*100+`%`),a(`[data-spz="ply-size"]`,this.fmtBytes(i)),a(`[data-spz="spz-size"]`,this.fmtBytes(r))}else v&&(v.style.display=`none`);let y=this.q(`[data-spz="actions"]`);y&&(y.innerHTML=` 184 <button class="spz-btn spz-btn--primary" data-spz-action="convert"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> ${n===`spz`?`Convert to .ply`:`Compress to .spz`}</button> 185 <button class="spz-btn spz-btn--secondary" data-spz-action="json"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/></svg> Export info as JSON</button> 186 `,this.q(`[data-spz-action="convert"]`)?.addEventListener(`click`,()=>n===`spz`?this.convertToPly():this.convertToSpz()),this.q(`[data-spz-action="json"]`)?.addEventListener(`click`,()=>this.exportInfoJson()))}exportInfoJson(){if(!this.currentCloud||!this.currentFile)return;let e=this.currentCloud,t=e.sh&&e.sh.length>0?Math.round(e.sh.length/e.numPoints/3):0,[[n,r],[i,a],[o,s]]=this.bbox(e.positions);this.download(this.currentFile.name.replace(/\.(spz|ply)$/i,``)+`-info.json`,new TextEncoder().encode(JSON.stringify({file:this.currentFile.name,type:this.currentType,size_bytes:this.currentFile.size,num_points:e.numPoints,sh_degree:e.shDegree,sh_coefficients:t,antialiased:e.antialiased,bounding_box:{x:[n,r],y:[i,a],z:[o,s]},...this.currentSpzHeader?{spz_header:this.currentSpzHeader}:{}},null,2))),this.toast(`Info exported`,`ok`)}async convertToPly(){if(!(!this.currentCloud||!this.currentFile)){this.setLoading(`Writing PLYâ¦`,`Encoding points and SH coefficients`);try{await new Promise(e=>setTimeout(e,50));let e=this.writePly(this.currentCloud),t=this.currentFile.name.replace(/\.spz$/i,`.ply`);this.download(t,e),this.renderInfo({file:this.currentFile,cloud:this.currentCloud,type:this.currentType,spzSize:this.currentFile.size,plySize:e.byteLength}),this.toast(`Exported ${t}`,`ok`)}catch(e){console.error(e),this.setError(`Conversion failed`,e.message||String(e))}}}async convertToSpz(){if(!(!this.currentCloud||!this.currentFile)){this.setLoading(`Compressing to SPZâ¦`,`Quantizing and packing coefficients`);
186try{let e=await this.getSpz();await new Promise(e=>setTimeout(e,50));let t=e.saveSpzToBuffer(this.currentCloud,{version:e.LATEST_SPZ_HEADER_VERSION,from:e.CoordinateSystem.RDF,sh1Bits:5,shRestBits:4});if(!t)throw Error(`saveSpzToBuffer returned null.`);let n=this.currentFile.name.replace(/\.ply$/i,`.spz`);this.download(n,t),this.renderInfo({file:this.currentFile,cloud:this.currentCloud,type:this.currentType,spzSize:t.byteLength,plySize:this.currentFile.size}),this.toast(`Exported ${n} (${(t.byteLength/this.currentFile.size*100).toFixed(1)}% of .ply)`,`ok`)}catch(e){console.error(e),this.setError(`Compression failed`,e.message||String(e))}}}resetAll(){this.currentFile=null,this.currentCloud=null,this.currentBuffer=null,this.currentType=null,this.currentSpzHeader=null;let e=this.q(`[data-spz="file-input"]`);e&&(e.value=``),this.showPanel(`empty`)}};customElements.get(`root-spz-converter`)||customElements.define(`root-spz-converter`,t);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.