@dtour/scatter 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/scatter.js +2 -2
  2. package/package.json +1 -1
package/dist/scatter.js CHANGED
@@ -2635,7 +2635,7 @@ function p(e) {
2635
2635
  }
2636
2636
  //#endregion
2637
2637
  //#region src/gpu/worker.ts?worker&inline
2638
- var m = "var e=`// PCA compute shader — two-pass reduction to compute covariance matrix.\n//\n// Pass 1 (reduce_partial): Each workgroup reduces its chunk of points into\n// per-workgroup partial sums for d means + d*(d+1)/2 cross-product terms.\n//\n// Pass 2 (final_reduce): A single workgroup reduces all partial sums into\n// the final means and covariance matrix.\n//\n// Data is normalized identically to the projection shader:\n// v = (raw - center) / range\n\nstruct PcaParams {\n num_points: u32,\n num_dims: u32,\n num_workgroups: u32,\n _pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: PcaParams;\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n@group(0) @binding(2) var<storage, read> norm_params: array<vec2f>;\n@group(0) @binding(3) var<storage, read_write> partials: array<f32>;\n// result layout: [mean_0 .. mean_{d-1}, cov_00, cov_01, ..., cov_{d-1,d-1}]\n// upper-triangle covariance packed as: d1*d - d1*(d1-1)/2 + (d2-d1)\n@group(0) @binding(4) var<storage, read_write> result: array<f32>;\n\nconst WG: u32 = 256u;\nconst MAX_DIMS: u32 = 32u;\n\nvar<workgroup> wg_reduce: array<f32, 256>;\n\nfn normalize(dim: u32, point_idx: u32, N: u32) -> f32 {\n let raw = data[dim * N + point_idx];\n let np = norm_params[dim];\n let range = max(np.y, 1e-6);\n return (raw - np.x) / range;\n}\n\nfn cov_idx(d: u32, d1: u32, d2: u32) -> u32 {\n return d1 * d - d1 * (d1 - 1u) / 2u + (d2 - d1);\n}\n\n// ─── Pass 1: per-workgroup partial sums ─────────────────────────────────────\n\n@compute @workgroup_size(256)\nfn reduce_partial(\n @builtin(global_invocation_id) gid: vec3u,\n @builtin(local_invocation_id) lid: vec3u,\n @builtin(workgroup_id) wid: vec3u,\n) {\n let N = params.num_points;\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let i = gid.x;\n let valid = i < N;\n\n // Load normalized values for this point into registers\n var vals: array<f32, 32>; // MAX_DIMS\n if (valid) {\n for (var dim = 0u; dim < d; dim++) {\n vals[dim] = normalize(dim, i, N);\n }\n }\n\n // Reduce mean terms\n for (var dim = 0u; dim < d; dim++) {\n wg_reduce[lid.x] = select(0.0, vals[dim], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) {\n wg_reduce[lid.x] += wg_reduce[lid.x + s];\n }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n partials[wid.x * num_terms + dim] = wg_reduce[0];\n }\n }\n\n // Reduce upper-triangle covariance terms (including diagonal)\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n wg_reduce[lid.x] = select(0.0, vals[d1] * vals[d2], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) {\n wg_reduce[lid.x] += wg_reduce[lid.x + s];\n }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n let ci = cov_idx(d, d1, d2);\n partials[wid.x * num_terms + d + ci] = wg_reduce[0];\n }\n }\n }\n}\n\n// ─── Pass 2: reduce across workgroups → final means + covariance ────────────\n\n@compute @workgroup_size(256)\nfn final_reduce(\n @builtin(local_invocation_id) lid: vec3u,\n) {\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let nwg = params.num_workgroups;\n let N_f32 = f32(params.num_points);\n\n // For each term, reduce across all workgroups\n for (var t = 0u; t < num_terms; t++) {\n // Each thread accumulates a stride of workgroups\n var sum = 0.0;\n var w = lid.x;\n while (w < nwg) {\n sum += partials[w * num_terms + t];\n w += WG;\n }\n wg_reduce[lid.x] = sum;\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) {\n wg_reduce[lid.x] += wg_reduce[lid.x + s];\n }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n // Store as E[X] or E[XY]\n result[t] = wg_reduce[0] / N_f32;\n }\n workgroupBarrier();\n }\n\n // Correct covariance: cov(X,Y) = E[XY] - E[X]*E[Y]\n if (lid.x == 0u) {\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n let ci = cov_idx(d, d1, d2);\n result[d + ci] -= result[d1] * result[d2];\n }\n }\n }\n}\n`;const t=(e,t)=>{let n=e,r=new Float32Array(t*t);for(let e=0;e<t;e++)r[e*t+e]=1;let i=1e-10;for(let e=0;e<50;e++){let e=0;for(let r=0;r<t;r++)for(let i=r+1;i<t;i++)e+=n[r*t+i]*n[r*t+i];if(e<i)break;for(let e=0;e<t;e++)for(let a=e+1;a<t;a++){let o=n[e*t+a];if(Math.abs(o)<i*.01)continue;let s=n[e*t+e],c=n[a*t+a],l=(c-s)/(2*o),u=Math.sign(l)/(Math.abs(l)+Math.sqrt(1+l*l)),d=1/Math.sqrt(1+u*u),f=u*d;n[e*t+e]=s-u*o,n[a*t+a]=c+u*o,n[e*t+a]=0,n[a*t+e]=0;for(let r=0;r<t;r++){if(r===e||r===a)continue;let i=n[r*t+e],o=n[r*t+a];n[r*t+e]=d*i-f*o,n[e*t+r]=d*i-f*o,n[r*t+a]=f*i+d*o,n[a*t+r]=f*i+d*o}for(let n=0;n<t;n++){let i=r[n*t+e],o=r[n*t+a];r[n*t+e]=d*i-f*o,r[n*t+a]=f*i+d*o}}}let a=new Float32Array(t),o=Array.from({length:t},(e,t)=>t);for(let e=0;e<t;e++)a[e]=n[e*t+e];o.sort((e,t)=>a[t]-a[e]);let s=new Float32Array(t),c=[];for(let e=0;e<t;e++){let n=o[e];s[e]=a[n];let i=new Float32Array(t);for(let e=0;e<t;e++)i[e]=r[e*t+n];c.push(i)}return{eigenvalues:s,eigenvectors:c}},n=t=>{let n=t.createShaderModule({label:`compute-pca`,code:e}),r=t.createBindGroupLayout({label:`pca-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}},{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}}]}),i=t.createPipelineLayout({bindGroupLayouts:[r]});return{partialPipeline:t.createComputePipeline({label:`pca-partial-pipeline`,layout:i,compute:{module:n,entryPoint:`reduce_partial`}}),finalPipeline:t.createComputePipeline({label:`pca-final-pipeline`,layout:i,compute:{module:n,entryPoint:`final_reduce`}}),bindGroupLayout:r,paramsBuffer:t.createBuffer({label:`pca-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},r=async(e,n,r,i,a,o)=>{let s=Math.min(o,32),c=s+s*(s+1)/2,l=Math.ceil(a/256),u=new ArrayBuffer(16),d=new Uint32Array(u);d[0]=a,d[1]=s,d[2]=l,d[3]=0,e.queue.writeBuffer(n.paramsBuffer,0,u);let f=e.createBuffer({label:`pca-partials`,size:l*c*4,usage:GPUBufferUsage.STORAGE}),p=c*4,m=e.createBuffer({label:`pca-result`,size:p,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC}),h=e.createBuffer({label:`pca-readback`,size:p,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),g=e.createBindGroup({label:`pca-bg`,layout:n.bindGroupLayout,entries:[{binding:0,resource:{buffer:n.paramsBuffer}},{binding:1,resource:{buffer:r}},{binding:2,resource:{buffer:i}},{binding:3,resource:{buffer:f}},{binding:4,resource:{buffer:m}}]}),_=e.createCommandEncoder({label:`pca-compute`}),v=_.beginComputePass({label:`pca-partial`});v.setPipeline(n.partialPipeline),v.setBindGroup(0,g),v.dispatchWorkgroups(l),v.end();let y=_.beginComputePass({label:`pca-final`});y.setPipeline(n.finalPipeline),y.setBindGroup(0,g),y.dispatchWorkgroups(1),y.end(),_.copyBufferToBuffer(m,0,h,0,p),e.queue.submit([_.finish()]),await h.mapAsync(GPUMapMode.READ);let b=new Float32Array(h.getMappedRange()).slice();h.unmap(),f.destroy(),m.destroy(),h.destroy();let x=new Float32Array(s*s);for(let e=0;e<s;e++)for(let t=e;t<s;t++){let n=e*s-e*(e-1)/2+(t-e);x[e*s+t]=b[s+n],x[t*s+e]=b[s+n]}return t(x,s)},i=(e,t)=>{let n=e.getContext(`webgpu`);if(!n)throw Error(`Failed to get WebGPU context from canvas. Ensure WebGPU is supported and the canvas is valid.`);let r=navigator.gpu.getPreferredCanvasFormat();return n.configure({device:t,format:r,alphaMode:`premultiplied`}),{canvas:e,context:n,format:r}},a=2e6,o=(e,t,n,r,i,o=[0,0,0])=>{let s=e.beginRenderPass({colorAttachments:[{view:t,clearValue:{r:o[0],g:o[1],b:o[2],a:1},loadOp:`clear`,storeOp:`store`}]});if(i>0){s.setPipeline(n),s.setBindGroup(0,r);for(let e=0;e<i;e+=a){let t=Math.min(a,i-e);s.draw(4,t,0,e)}}s.end()},s=(e,t,n,r)=>{let i=e.beginRenderPass({colorAttachments:[{view:t.context.getCurrentTexture().createView(),loadOp:`clear`,storeOp:`store`,clearValue:{r:0,g:0,b:0,a:1}}]});i.setPipeline(n),i.setBindGroup(0,r),i.draw(3),i.end()},c=1e-10,l=e=>{let t=e[0],n=e[1],r=e[2],i=e[3],a=t*t+r*r,o=t*n+r*i,s=n*n+i*i,l=a+s,u=a*s-o*o,d=Math.sqrt(Math.max(0,l*l*.25-u)),f=l*.5+d,p=l*.5-d,m=Math.sqrt(Math.max(0,f)),h=Math.sqrt(Math.max(0,p)),g=new Float32Array([m,h]),_=new Float32Array(4);if(Math.abs(o)>c){let e=f-s,t=Math.sqrt(e*e+o*o);_[0]=e/t,_[1]=o/t;let n=p-s,r=Math.sqrt(n*n+o*o);_[2]=n/r,_[3]=o/r}else _[0]=1,_[1]=0,_[2]=0,_[3]=1;let v=new Float32Array(4);return m>c&&(v[0]=(t*_[0]+n*_[1])/m,v[2]=(r*_[0]+i*_[1])/m),h>c&&(v[1]=(t*_[2]+n*_[3])/h,v[3]=(r*_[2]+i*_[3])/h),m<=c&&h>c?(v[0]=-v[3],v[2]=v[1]):h<=c&&m>c?(v[1]=-v[2],v[3]=v[0]):m<=c&&h<=c&&(v[0]=1,v[1]=0,v[2]=0,v[3]=1),{U:v,S:g,V:_}},u=(e,t)=>{let n=0;for(let r=0;r<t;r++)n+=e[r]*e[r];if(n=Math.sqrt(n),n>1e-10)for(let r=0;r<t;r++)e[r]=e[r]/n;let r=0;for(let n=0;n<t;n++)r+=e[n]*e[t+n];for(let n=0;n<t;n++)e[t+n]=e[t+n]-r*e[n];let i=0;for(let n=0;n<t;n++)i+=e[t+n]*e[t+n];if(i=Math.sqrt(i),i>1e-10)for(let n=0;n<t;n++)e[t+n]=e[t+n]/i},d=(e,t,n,r,i,a,o,s=!0)=>{let c=o*o,l=c*o,d=.5*(-o+2*c-l),f=.5*(2-5*c+3*l),p=.5*(o+4*c-3*l),m=.5*(-c+l);for(let o=0;o<a*2;o++)e[o]=d*t[o]+f*n[o]+p*r[o]+m*i[o];return s&&u(e,a),e},f=(e,t,n)=>{let r=new Float32Array(4);for(let i=0;i<2;i++)for(let a=0;a<2;a++){let o=0;for(let r=0;r<n;r++)o+=e[i*n+r]*t[a*n+r];r[i*2+a]=o}let{S:i}=l(r),a=Math.acos(Math.max(-1,Math.min(1,i[0]))),o=Math.acos(Math.max(-1,Math.min(1,i[1])));return Math.sqrt(a*a+o*o)},p=(e,t,n=!0)=>{let r=e.length,i=new Float32Array(r+1);if(i[0]=0,!n){for(let e=1;e<=r;e++)i[e]=e/r;return i}let a=new Float32Array(t*2),o=new Float32Array(t*2);for(let n=0;n<r;n++){let s=(n-1+r)%r,c=n,l=(n+1)%r,u=(n+2)%r,p=0;d(a,e[s],e[c],e[l],e[u],t,0);for(let n=1;n<=8;n++){let r=n/8;d(o,e[s],e[c],e[l],e[u],t,r),p+=f(a,o,t),a.set(o)}i[n+1]=i[n]+p}let s=i[r];if(s>1e-10)for(let e=1;e<=r;e++)i[e]=i[e]/s;return i},m=(e,t)=>{let n=e.length-1,r=Math.max(0,Math.min(1,t)),i=0,a=n-1;for(;i<a;){let t=i+a>>>1;e[t+1]<=r?i=t+1:a=t}let o=e[i],s=e[i+1]-o,c=s>1e-10?(r-o)/s:0;return{segment:i,localT:c}},h=(e,t,n,r,i,a=!0)=>{let o=t.length,{segment:s,localT:c}=m(n,i),l=(s-1+o)%o,u=s,f=(s+1)%o,p=(s+2)%o;return d(e,t[l],t[u],t[f],t[p],r,c,a)};var g=`// GPU categorical selection (bit-packed output)\n//\n// For each point, checks if its category index is in the selected set.\n// Output mask must be pre-cleared; bits are set via atomicOr.\n\nstruct Params {\n num_points: u32,\n num_labels: u32,\n}\n\n@group(0) @binding(0) var<storage, read> indices: array<u32>;\n@group(0) @binding(1) var<storage, read> selected: array<u32>; // 1 per label: selected or not\n@group(0) @binding(2) var<storage, read_write> mask: array<atomic<u32>>;\n@group(0) @binding(3) var<uniform> params: Params;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.num_points) { return; }\n let idx = indices[i];\n if (idx < params.num_labels && selected[idx] > 0u) {\n atomicOr(&mask[i / 32u], 1u << (i % 32u));\n }\n}\n`,_=`// GPU continuous selection (bit-packed output)\n//\n// For each point, checks if the column value falls within any [lo, hi] range.\n// Output mask must be pre-cleared; bits are set via atomicOr.\n\nstruct Params {\n num_points: u32,\n column_offset: u32, // columnIndex * numPoints\n num_ranges: u32,\n}\n\n@group(0) @binding(0) var<storage, read> data: array<f32>;\n@group(0) @binding(1) var<storage, read> ranges: array<f32>; // [lo0, hi0, lo1, hi1, ...]\n@group(0) @binding(2) var<storage, read_write> mask: array<atomic<u32>>;\n@group(0) @binding(3) var<uniform> params: Params;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.num_points) { return; }\n\n let v = data[params.column_offset + i];\n for (var r = 0u; r < params.num_ranges; r++) {\n if (v >= ranges[r * 2u] && v <= ranges[r * 2u + 1u]) {\n atomicOr(&mask[i / 32u], 1u << (i % 32u));\n break;\n }\n }\n}\n`;const v=e=>{let t=e.createBindGroupLayout({label:`color-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}}]}),n=e.createPipelineLayout({bindGroupLayouts:[t]}),r=(t,r)=>e.createComputePipeline({label:t,layout:n,compute:{module:e.createShaderModule({label:t,code:r}),entryPoint:`main`}}),i=e.createBuffer({label:`color-params`,size:32,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});return{selectContinuous:r(`select-continuous`,_),selectCategorical:r(`select-categorical`,g),bindGroupLayout:t,paramsBuffer:i}},y=(e,t,n,r,i,a)=>e.createBindGroup({label:`color-bg`,layout:t,entries:[{binding:0,resource:{buffer:n}},{binding:1,resource:{buffer:r}},{binding:2,resource:{buffer:i}},{binding:3,resource:{buffer:a}}]}),b=(e,t,n,r,i)=>{let a=new ArrayBuffer(32),o=new Uint32Array(a);o[0]=n,o[1]=r,o[2]=i,e.queue.writeBuffer(t,0,a)},x=(e,t,n,r)=>{let i=new ArrayBuffer(32),a=new Uint32Array(i);a[0]=n,a[1]=r,e.queue.writeBuffer(t,0,i)},ee=async()=>{if(!navigator.gpu)throw Error(`WebGPU is not supported in this environment. Please use Chrome 113+, Edge 113+, or Firefox Nightly with WebGPU enabled.`);let e=await navigator.gpu.requestAdapter({powerPreference:`high-performance`});if(!e)throw Error(`No WebGPU adapter found. Your GPU may not support WebGPU, or the browser may have blocked access.`);if(!e.features.has(`float32-blendable`))throw Error(`Your GPU does not support float32-blendable, which is required for HDR rendering. Please try a different browser or device with a GPU that supports this WebGPU feature.`);let t=await e.requestDevice({label:`dtour-scatter`,requiredFeatures:[`float32-blendable`],requiredLimits:{maxStorageBufferBindingSize:e.limits.maxStorageBufferBindingSize,maxBufferSize:e.limits.maxBufferSize}});return t.lost.then(e=>{console.error(`WebGPU device lost: ${e.message} (reason: ${e.reason})`)}),{adapter:e,device:t}};var S=`// Point renderer — instanced quads with SDF circle anti-aliasing\n//\n// Projects raw ND data to 2D inline using pre-adjusted basis weights and\n// biases (normalization folded into the basis on CPU). This eliminates the\n// separate compute-projection pass and its GPU barrier.\n//\n// Trade-off: the projection loop runs once per vertex (4x per point) since\n// vertex shaders can't share results across a strip. This is a net win\n// because eliminating the compute dispatch + barrier + projected-buffer\n// read costs more than the redundant ALU, and GPUs are throughput-oriented.\n// For very high dim counts (>200) this balance may shift — profile first.\n//\n// Applies 2D camera transform (pan/zoom/aspect). Each point is rendered as\n// a unit quad (triangle strip, 4 vertices) scaled by point_size (NDC units).\n//\n// Color mapping: instead of a pre-computed N-element color buffer, the shader\n// reads raw data values and looks up colors from a tiny LUT at render time.\n// This eliminates the 20MB color buffer for 5M points and makes color changes\n// instant (just swap the ~100-byte LUT + update uniforms).\n\nstruct Uniforms {\n point_size: f32,\n opacity: f32,\n // Color mode: 0 = uniform color, 1 = continuous, 2 = categorical\n color_mode: u32,\n useSelectionMask: f32,\n color: vec4f,\n useSubtractive: f32,\n num_points: u32,\n num_dims: u32,\n max_points: u32,\n bias: vec2f,\n // Color mapping params (used when color_mode > 0)\n color_column_offset: u32, // columnIndex * num_points (continuous only)\n color_min: f32,\n color_range: f32,\n color_num_stops: u32, // LUT size\n bias_z: f32, // z-axis bias (3D mode only)\n // 2D colormap params (used when color_mode == 3)\n color_column_offset_v: u32, // second column index * num_points\n color_min_v: f32,\n color_range_v: f32,\n color2d_map_index: u32, // 0-5 = LUT-based, 6 = oklab_polar\n scale_opacity_by_zoom: f32, // 1.0 = auto (zoom² density scaling), 0.0 = manual (fixed)\n}\n\nstruct Camera {\n pan_x: f32,\n pan_y: f32,\n zoom: f32,\n aspect: f32,\n viewport_height: f32,\n // NDC-space Y offset applied after zoom — used to shift the viewport\n // down when a toolbar covers the top of the canvas.\n inset_offset_y: f32,\n // Zoom multiplier for the inset — scales content to fit in the visible\n // area below the toolbar (typically (H - toolbar) / H).\n inset_zoom: f32,\n // 3D camera rotation: 0.0 = 2D mode (skip z projection), 1.0 = 3D mode\n use_3d: f32,\n // 3×3 rotation matrix (column-major, each column padded to vec4f for alignment)\n rot_col0: vec3f,\n _pad0: f32,\n rot_col1: vec3f,\n _pad1: f32,\n rot_col2: vec3f,\n _pad2: f32,\n}\n\n@group(0) @binding(0) var<uniform> uni: Uniforms;\n// Raw ND data (column-major): dim0[0..N], dim1[0..N], ..., dimP[0..N]\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n@group(0) @binding(2) var<uniform> camera: Camera;\n// Colormap/palette LUT — packed RGBA8 u32 entries (25 for continuous, up to 256 for categorical)\n@group(0) @binding(3) var<storage, read> colorLut: array<u32>;\n// Bit-packed selection mask (1 bit per point, 32 per u32)\n@group(0) @binding(4) var<storage, read> selectionMask: array<u32>;\n// Adjusted basis weights (normalization folded in):\n// first num_dims = x-weights, next num_dims = y-weights\n// adj_basis[d] = basis[d] / range[d] * viewport_scale\n@group(0) @binding(5) var<storage, read> adj_basis: array<f32>;\n// Categorical index buffer (per-point category index, used when color_mode == 2)\n@group(0) @binding(6) var<storage, read> catIndices: array<u32>;\n\nstruct VertOut {\n @builtin(position) clip_pos: vec4f,\n @location(0) uv: vec2f,\n @location(1) point_color: vec4f,\n @location(2) @interpolate(flat) selected: u32,\n @location(3) @interpolate(flat) effective_opacity: f32,\n}\n\n// Triangle strip: 4 vertices forming a unit quad [-1, 1]²\nfn quad_vertex(vi: u32) -> vec2f {\n switch vi {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n// Unpack RGBA8 from u32 (0xAABBGGRR little-endian)\nfn unpackColor(packed: u32) -> vec4f {\n let r = f32(packed & 0xFFu) / 255.0;\n let g = f32((packed >> 8u) & 0xFFu) / 255.0;\n let b = f32((packed >> 16u) & 0xFFu) / 255.0;\n let a = f32((packed >> 24u) & 0xFFu) / 255.0;\n return vec4f(r, g, b, a);\n}\n\n// PCG hash — fast, high-quality deterministic random from instance index.\n// Used for vertex-shader decimation when num_points > max_points.\nfn pcg_hash(input: u32) -> u32 {\n var state = input * 747796405u + 2891336453u;\n let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\n\nfn random_float(seed: u32) -> f32 {\n return f32(pcg_hash(seed)) / 4294967295.0;\n}\n\n// ─── 2D Colormap helpers ──────────────────────────────────────────────────\n\n// Read f32 from the colorLut buffer (which stores u32). Used for 2D colormap LUT curves.\nfn lut_f32(index: u32) -> f32 {\n return bitcast<f32>(colorLut[index]);\n}\n\n// Linearly interpolate a 16-entry LUT curve stored in colorLut at the given base offset.\nfn lut_interp(base: u32, t: f32) -> f32 {\n let pos = t * 15.0;\n let lo = min(u32(floor(pos)), 14u);\n let hi = lo + 1u;\n return mix(lut_f32(base + lo), lut_f32(base + hi), fract(pos));\n}\n\n// Reconstruct a 2D colormap color from SVD rank-1 LUT curves stored in colorLut.\n// Layout: R_X[16] R_Y[16] G_X[16] G_Y[16] B_X[16] B_Y[16] B_X2[16] B_Y2[16]\nfn colormap2d_lut(u: f32, v: f32) -> vec3f {\n let r = lut_interp(0u, u) * lut_interp(16u, v);\n let g = lut_interp(32u, u) * lut_interp(48u, v);\n let b = lut_interp(64u, u) * lut_interp(80u, v)\n + lut_interp(96u, u) * lut_interp(112u, v); // rank-2 correction\n return clamp(vec3f(r, g, b), vec3f(0.0), vec3f(1.0));\n}\n\n// Oklab → linear sRGB conversion\nfn oklab_to_linear(L: f32, a: f32, b: f32) -> vec3f {\n let l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n let m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n let s_ = L - 0.0894841775 * a - 1.2914855480 * b;\n let l = l_ * l_ * l_;\n let m = m_ * m_ * m_;\n let s = s_ * s_ * s_;\n return vec3f(\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s\n );\n}\n\n// Oklab polar 2D colormap: hue = angle, chroma = radius, lightness varies\nfn colormap2d_oklab_polar(u: f32, v: f32) -> vec3f {\n let cx = u - 0.5;\n let cy = v - 0.5;\n let radius = min(length(vec2f(cx, cy)) * 2.0, 1.0);\n let angle = atan2(cy, cx);\n let chroma = radius * 0.25;\n let lightness = 0.80 - radius * 0.30;\n let a_val = chroma * cos(angle);\n let b_val = chroma * sin(angle);\n let linear = oklab_to_linear(lightness, a_val, b_val);\n // Linear to sRGB gamma (approximate)\n return pow(clamp(linear, vec3f(0.0), vec3f(1.0)), vec3f(1.0 / 2.2));\n}\n\n// Dispatch 2D colormap by index\nfn colormap2d(map_index: u32, u: f32, v: f32) -> vec3f {\n if map_index == 6u {\n return colormap2d_oklab_polar(u, v);\n }\n return colormap2d_lut(u, v);\n}\n\n@vertex\nfn vs_main(\n @builtin(vertex_index) vi: u32,\n @builtin(instance_index) ii: u32,\n) -> VertOut {\n // Vertex-shader decimation: skip expensive projection for excess points.\n // Early exit before the ND projection loop saves ~50 cycles per rejected point.\n if (uni.max_points > 0u && uni.num_points > uni.max_points) {\n let threshold = f32(uni.max_points) / f32(uni.num_points);\n if (random_float(ii) > threshold) {\n var out: VertOut;\n out.clip_pos = vec4f(0.0, 0.0, 0.0, 0.0);\n out.uv = vec2f(0.0);\n out.point_color = vec4f(0.0);\n out.selected = 0u;\n out.effective_opacity = 0.0;\n return out;\n }\n }\n\n // Inline ND → 2D projection with pre-adjusted basis.\n // CPU folds normalization into the basis weights and biases so the\n // hot loop is just: x += raw * adjBasisX[d], y += raw * adjBasisY[d]\n // Must produce the same result as compute-projection.wgsl (used for lasso).\n let N = uni.num_points;\n let p = uni.num_dims;\n var x = uni.bias.x;\n var y = uni.bias.y;\n for (var d = 0u; d < p; d++) {\n let raw = data[d * N + ii];\n x += raw * adj_basis[d];\n y += raw * adj_basis[p + d];\n }\n\n // 3D camera rotation: project z-axis, rotate, then discard z (orthographic).\n if (camera.use_3d > 0.5) {\n var z = uni.bias_z;\n for (var d = 0u; d < p; d++) {\n z += data[d * N + ii] * adj_basis[2u * p + d];\n }\n let pos3 = vec3f(x, y, z);\n let rot = mat3x3f(camera.rot_col0, camera.rot_col1, camera.rot_col2);\n let rotated = rot * pos3;\n x = rotated.x;\n y = rotated.y;\n // z discarded — orthographic projection\n }\n\n // Apply camera: translate then scale, with aspect correction on x.\n // inset_zoom shrinks the scene to fit below the toolbar;\n // inset_offset_y shifts it down so it's centered in the visible area.\n let iz = camera.inset_zoom;\n let center = vec2f(\n (x + camera.pan_x) * camera.zoom * iz / camera.aspect,\n (y + camera.pan_y) * camera.zoom * iz + camera.inset_offset_y,\n );\n\n let q = quad_vertex(vi);\n // Clamp point size to [2, 32] pixels (physical).\n // min_ndc ensures points are always at least 2px — 1px quads can miss pixel\n // centers entirely depending on sub-pixel alignment, producing no fragments.\n let min_ndc = 4.0 / camera.viewport_height;\n let max_ndc = 64.0 / camera.viewport_height;\n let point_size = clamp(uni.point_size, min_ndc, max_ndc);\n // Aspect-correct the offset so circles stay circular on non-square canvases\n let offset = vec2f(q.x / camera.aspect, q.y) * (point_size * 0.5);\n\n // Scale opacity by zoom² to keep constant visual fill density (Reusser).\n // Only applied for auto-computed opacity; manual values are used as-is.\n let z = camera.zoom * camera.inset_zoom;\n let zoom_scale = select(1.0, z * z, uni.scale_opacity_by_zoom > 0.5);\n let eff_opacity = uni.opacity * zoom_scale;\n\n // Resolve per-point color from LUT\n var col: vec4f;\n if uni.color_mode == 1u {\n // Continuous: read raw value from color column, normalize, interpolate LUT\n let raw = data[uni.color_column_offset + ii];\n let inv_range = select(0.0, 1.0 / uni.color_range, uni.color_range > 0.0);\n let t = clamp((raw - uni.color_min) * inv_range, 0.0, 1.0);\n let stops = f32(uni.color_num_stops - 1u);\n let pos = t * stops;\n let idx = min(u32(floor(pos)), uni.color_num_stops - 2u);\n let frac = pos - f32(idx);\n let c0 = unpackColor(colorLut[idx]);\n let c1 = unpackColor(colorLut[idx + 1u]);\n col = mix(c0, c1, frac);\n } else if uni.color_mode == 2u {\n // Categorical: read category index, direct palette lookup\n let catIdx = catIndices[ii];\n col = unpackColor(colorLut[catIdx % uni.color_num_stops]);\n } else if uni.color_mode == 3u {\n // 2D colormap: read two columns, normalize to UV, look up from 2D colormap\n let raw_u = data[uni.color_column_offset + ii];\n let raw_v = data[uni.color_column_offset_v + ii];\n let inv_range_u = select(0.0, 1.0 / uni.color_range, uni.color_range > 0.0);\n let inv_range_v = select(0.0, 1.0 / uni.color_range_v, uni.color_range_v > 0.0);\n let u_val = clamp((raw_u - uni.color_min) * inv_range_u, 0.0, 1.0);\n let v_val = clamp((raw_v - uni.color_min_v) * inv_range_v, 0.0, 1.0);\n let rgb = colormap2d(uni.color2d_map_index, u_val, v_val);\n col = vec4f(rgb, 1.0);\n } else {\n col = uni.color;\n }\n\n // Read selection\n var sel: u32 = 1u;\n if uni.useSelectionMask > 0.5 {\n sel = (selectionMask[ii / 32u] >> (ii % 32u)) & 1u;\n }\n\n var out: VertOut;\n out.clip_pos = vec4f(center + offset, 0.0, 1.0);\n out.uv = q;\n out.point_color = col;\n out.selected = sel;\n out.effective_opacity = eff_opacity;\n return out;\n}\n\n// Shared fragment helper — computes edge, selection factor, and intensity.\nfn fragment_intensity(\n uv: vec2f,\n point_alpha: f32,\n selected: u32,\n effective_opacity: f32,\n) -> f32 {\n let dist = length(uv);\n // Smooth anti-aliased edge — naturally 0 for dist >= 1.0, so no discard needed.\n // Avoiding discard preserves early-Z and SIMD wavefront occupancy.\n let edge = 1.0 - smoothstep(0.75, 1.0, dist);\n\n // Selection: boost selected, dim unselected\n var sel_factor = 1.0;\n if uni.useSelectionMask > 0.5 {\n if selected == 0u {\n sel_factor = 0.1;\n } else {\n sel_factor = 1.0 / max(effective_opacity, 0.01);\n }\n }\n\n return edge * effective_opacity * point_alpha * sel_factor;\n}\n\n// ─── Fragment: single-target (additive / subtractive) ──────────────────────\n\n@fragment\nfn fs_main(\n @location(0) uv: vec2f,\n @location(1) point_color: vec4f,\n @location(2) @interpolate(flat) selected: u32,\n @location(3) @interpolate(flat) effective_opacity: f32,\n) -> @location(0) vec4f {\n let intensity = fragment_intensity(uv, point_color.a, selected, effective_opacity);\n // Subtractive mode (Reusser): output complement color so that\n // reverse-subtract blend (dst - src) on a white bg yields the correct hue.\n let rgb = select(point_color.rgb, vec3f(1.0) - point_color.rgb, uni.useSubtractive > 0.5);\n return vec4f(rgb * intensity, intensity);\n}\n`,C=`// Fullscreen tone-map pass — reads an HDR float texture and maps to [0,1]\n// for the canvas. Uses a single oversized triangle (3 vertices, no vertex\n// buffer) to cover the viewport.\n//\n// Mode 0 (additive): per-channel exponential compression 1-exp(-x).\n// Preserves hue ratios, dense regions desaturate toward white.\n// Mode 1 (normal / subtractive): simple clamp to [0,1].\n// Values are already bounded; tone mapping is identity.\n\nstruct Params {\n mode: f32,\n}\n\n@group(0) @binding(0) var hdrTexture: texture_2d<f32>;\n@group(0) @binding(1) var<uniform> params: Params;\n\n@vertex\nfn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n // Oversized triangle covering the full clip space:\n // vi=0 → (-1, -1) vi=1 → (3, -1) vi=2 → (-1, 3)\n let x = f32(i32(vi) / 2) * 4.0 - 1.0;\n let y = f32(i32(vi) % 2) * 4.0 - 1.0;\n return vec4f(x, y, 0.0, 1.0);\n}\n\n@fragment\nfn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n let hdr = textureLoad(hdrTexture, vec2u(pos.xy), 0);\n\n var mapped: vec3f;\n if params.mode < 0.5 {\n // Additive: exponential compression preserving density structure\n mapped = 1.0 - exp(-hdr.rgb);\n } else {\n // Normal / Subtractive: values already in [0,1], just clamp\n mapped = clamp(hdr.rgb, vec3f(0.0), vec3f(1.0));\n }\n return vec4f(mapped, 1.0);\n}\n`;const w={pointSize:.012,opacity:.7,color:[.25,.5,.9],scaleOpacityByZoom:!0},T={useSelectionMask:!1},E={mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},te={panX:0,panY:0,zoom:1,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1,use3d:!1,rotation:null},D=new ArrayBuffer(96),O=new Float32Array(D),k=new Uint32Array(D),A=new Float32Array(80/4),j=new Float32Array(4),ne=(e,t=`rgba32float`)=>{let n=e.createShaderModule({label:`point-shader`,code:S}),r=e.createBindGroupLayout({label:`point-bgl`,entries:[{binding:0,visibility:GPUShaderStage.VERTEX|GPUShaderStage.FRAGMENT,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.VERTEX,buffer:{type:`uniform`}},{binding:3,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:4,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:5,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:6,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}}]}),i=e.createPipelineLayout({bindGroupLayouts:[r]}),a=e.createRenderPipeline({label:`point-pipeline-additive`,layout:i,vertex:{module:n,entryPoint:`vs_main`},fragment:{module:n,entryPoint:`fs_main`,targets:[{format:t,blend:{color:{srcFactor:`one`,dstFactor:`one`},alpha:{srcFactor:`zero`,dstFactor:`one`}}}]},primitive:{topology:`triangle-strip`}}),o=e.createRenderPipeline({label:`point-pipeline-normal`,layout:i,vertex:{module:n,entryPoint:`vs_main`},fragment:{module:n,entryPoint:`fs_main`,targets:[{format:t,blend:{color:{srcFactor:`one`,dstFactor:`one-minus-src-alpha`},alpha:{srcFactor:`one`,dstFactor:`one-minus-src-alpha`}}}]},primitive:{topology:`triangle-strip`}}),s=e.createRenderPipeline({label:`point-pipeline-subtractive`,layout:i,vertex:{module:n,entryPoint:`vs_main`},fragment:{module:n,entryPoint:`fs_main`,targets:[{format:t,blend:{color:{operation:`reverse-subtract`,srcFactor:`one`,dstFactor:`one`},alpha:{srcFactor:`zero`,dstFactor:`one`}}}]},primitive:{topology:`triangle-strip`}}),c=e.createBuffer({label:`point-uniforms`,size:96,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),l=e.createBuffer({label:`camera-uniforms`,size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),u=e.createBuffer({label:`default-color-lut-buf`,size:4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),d=e.createBuffer({label:`default-selection-buf`,size:4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),f=e.createBuffer({label:`default-cat-indices-buf`,size:4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});return M(e,c,w,T,E),re(e,l,te),{additivePipeline:a,normalPipeline:o,subtractivePipeline:s,bindGroupLayout:r,uniformBuffer:c,cameraBuffer:l,defaultColorLutBuffer:u,defaultSelectionBuffer:d,defaultCatIndicesBuffer:f}},M=(e,t,n,r=T,i=E,a=!1,o=0,s=0,c=0,l=0,u=0,d=0)=>{O[0]=n.pointSize,O[1]=n.opacity,k[2]=i.mode,O[3]=r.useSelectionMask?1:0,O[4]=n.color[0],O[5]=n.color[1],O[6]=n.color[2],O[7]=1,O[8]=a?1:0,k[9]=o,k[10]=s,k[11]=u,O[12]=c,O[13]=l,k[14]=i.columnOffset,O[15]=i.min,O[16]=i.range,k[17]=i.numStops,O[18]=d,k[19]=i.columnOffsetV,O[20]=i.minV,O[21]=i.rangeV,k[22]=i.mapIndex,O[23]=n.scaleOpacityByZoom?1:0,e.queue.writeBuffer(t,0,D)},re=(e,t,n)=>{A[0]=n.panX,A[1]=n.panY,A[2]=n.zoom,A[3]=n.aspect,A[4]=n.viewportHeight,A[5]=n.insetOffsetY,A[6]=n.insetZoom,A[7]=n.use3d?1:0;let r=n.rotation;A[8]=r?r[0]:1,A[9]=r?r[1]:0,A[10]=r?r[2]:0,A[11]=0,A[12]=r?r[3]:0,A[13]=r?r[4]:1,A[14]=r?r[5]:0,A[15]=0,A[16]=r?r[6]:0,A[17]=r?r[7]:0,A[18]=r?r[8]:1,A[19]=0,e.queue.writeBuffer(t,0,A)},ie=(e,t,n,r,i,a,o,s,c)=>e.createBindGroup({label:`point-bg`,layout:t,entries:[{binding:0,resource:{buffer:n}},{binding:1,resource:{buffer:r}},{binding:2,resource:{buffer:i}},{binding:3,resource:{buffer:a}},{binding:4,resource:{buffer:o}},{binding:5,resource:{buffer:s}},{binding:6,resource:{buffer:c}}]}),ae=(e,t,n)=>{j[0]=n,e.queue.writeBuffer(t,0,j,0,1)},oe=(e,t)=>{let n=e.createShaderModule({label:`tonemap-shader`,code:C}),r=e.createBindGroupLayout({label:`tonemap-bgl`,entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:`unfilterable-float`}},{binding:1,visibility:GPUShaderStage.FRAGMENT,buffer:{type:`uniform`}}]});return{pipeline:e.createRenderPipeline({label:`tonemap-pipeline`,layout:e.createPipelineLayout({bindGroupLayouts:[r]}),vertex:{module:n,entryPoint:`vs`},fragment:{module:n,entryPoint:`fs`,targets:[{format:t}]},primitive:{topology:`triangle-list`}}),bindGroupLayout:r,paramsBuffer:e.createBuffer({label:`tonemap-params`,size:4,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},se=(e,t,n,r)=>e.createBindGroup({label:`tonemap-bg`,layout:t,entries:[{binding:0,resource:n},{binding:1,resource:{buffer:r}}]});var ce=`// ND → 2D projection compute shader (used only for lasso selection readback)\n//\n// Reads p-dimensional point data from a single concatenated storage buffer\n// (column-major: dim0[0..N], dim1[0..N], ..., dimP[0..N]),\n// normalizes each dimension using per-dim [center, range] params,\n// and projects to 2D using the supplied p×2 basis matrix.\n//\n// Output: N interleaved [x, y] pairs in the projected buffer.\n//\n// IMPORTANT: This must stay mathematically equivalent to the render-path\n// projection in point.wgsl + computeAdjustedBasis() in worker.ts.\n// The render path folds normalization into the basis on CPU (adj_basis, bias)\n// while this shader normalizes per-point on GPU. Both produce identical\n// projected coordinates: ((raw - center) / range) * basis * viewport_scale.\n\nstruct Params {\n num_points: u32,\n num_dims: u32,\n viewport_scale: f32,\n _pad: f32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n// norm_params[d] = vec2f(center, range) for dimension d\n@group(0) @binding(2) var<storage, read> norm_params: array<vec2f>;\n// basis: column-major p×2 — first p floats = x-weights, next p = y-weights\n@group(0) @binding(3) var<storage, read> basis: array<f32>;\n@group(0) @binding(4) var<storage, read_write> projected: array<f32>;\n\n@compute @workgroup_size(256)\nfn project(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.num_points) { return; }\n\n let N = params.num_points;\n let p = params.num_dims;\n\n var x = 0.0;\n var y = 0.0;\n\n for (var d = 0u; d < p; d++) {\n let raw = data[d * N + i];\n let np = norm_params[d];\n let range = max(np.y, 1e-6);\n let v = (raw - np.x) / range;\n\n x += v * basis[d];\n y += v * basis[p + d];\n }\n\n projected[i * 2u] = x * params.viewport_scale;\n projected[i * 2u + 1u] = y * params.viewport_scale;\n}\n`;const le=e=>{let t=e.createShaderModule({label:`compute-projection`,code:ce}),n=e.createBindGroupLayout({label:`projection-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}}]});return{pipeline:e.createComputePipeline({label:`projection-pipeline`,layout:e.createPipelineLayout({bindGroupLayouts:[n]}),compute:{module:t,entryPoint:`project`}}),bindGroupLayout:n,paramsBuffer:e.createBuffer({label:`projection-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},ue=(e,t,n,r,i)=>{let a=new ArrayBuffer(16),o=new Uint32Array(a),s=new Float32Array(a);o[0]=n,o[1]=r,s[2]=i,s[3]=0,e.queue.writeBuffer(t,0,a)},de=(e,t,n)=>({dataBuffer:e.createBuffer({label:`nd-data`,size:t*n*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC}),normParamsBuffer:e.createBuffer({label:`norm-params`,size:n*8,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),basisBuffer:e.createBuffer({label:`basis`,size:n*2*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),projectedBuffer:e.createBuffer({label:`projected`,size:t*2*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC}),bindGroup:null}),fe=(e,t,n,r)=>e.createBindGroup({label:`projection-bg`,layout:t,entries:[{binding:0,resource:{buffer:n}},{binding:1,resource:{buffer:r.dataBuffer}},{binding:2,resource:{buffer:r.normParamsBuffer}},{binding:3,resource:{buffer:r.basisBuffer}},{binding:4,resource:{buffer:r.projectedBuffer}}]}),pe=(e,t,n,r)=>{let i=e.createCommandEncoder({label:`projection-dispatch`}),a=i.beginComputePass({label:`project-points`});return a.setPipeline(t.pipeline),a.setBindGroup(0,n),a.dispatchWorkgroups(Math.ceil(r/256)),a.end(),i.finish()};var me=`// Residual-PC compute shader — two-pass reduction.\n//\n// Computes the covariance matrix of the residual after projecting out\n// the current p×2 tour basis. The top eigenvector of this covariance\n// is the \"most informative direction not currently visible.\"\n//\n// Pass 1 (reduce_partial): each workgroup reduces its chunk of N points\n// into partial sums for p means + p*(p+1)/2 upper-triangle cross-products\n// of the per-point residuals.\n//\n// Pass 2 (final_reduce): one workgroup sums all partial sums → final\n// mean-corrected covariance. Result layout:\n// [mean_0 .. mean_{p-1}, cov_00, cov_01, ..., cov_{p-1,p-1}]\n// where the covariance entries are the upper triangle packed as:\n// index(d1,d2) = d1*p - d1*(d1-1)/2 + (d2-d1) (d2 >= d1)\n//\n// Power iteration (15 steps) is performed on the CPU after readback;\n// p is always small (≤64) so this is negligible.\n\nstruct Params {\n num_points: u32,\n num_dims: u32,\n num_workgroups: u32,\n _pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n@group(0) @binding(2) var<storage, read> norm_params: array<vec2f>;\n// basis: column-major p×2 — first p floats = x-weights, next p = y-weights\n@group(0) @binding(3) var<storage, read> basis: array<f32>;\n@group(0) @binding(4) var<storage, read_write> partials: array<f32>;\n@group(0) @binding(5) var<storage, read_write> result: array<f32>;\n\nconst WG: u32 = 256u;\nconst MAX_DIMS: u32 = 64u;\n\nvar<workgroup> wg_reduce: array<f32, 256>;\n\nfn cov_idx(d: u32, d1: u32, d2: u32) -> u32 {\n return d1 * d - d1 * (d1 - 1u) / 2u + (d2 - d1);\n}\n\n// ─── Pass 1: per-workgroup partial sums ─────────────────────────────────────\n\n@compute @workgroup_size(256)\nfn reduce_partial(\n @builtin(global_invocation_id) gid: vec3u,\n @builtin(local_invocation_id) lid: vec3u,\n @builtin(workgroup_id) wid: vec3u,\n) {\n let N = params.num_points;\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let i = gid.x;\n let valid = i < N;\n\n // Load normalized values into registers, then project out the 2D basis.\n var vals: array<f32, 64>;\n if (valid) {\n var dot_x = 0.0;\n var dot_y = 0.0;\n for (var dim = 0u; dim < d; dim++) {\n let raw = data[dim * N + i];\n let np = norm_params[dim];\n let range = max(np.y, 1e-6);\n let norm = (raw - np.x) / range;\n vals[dim] = norm;\n dot_x += norm * basis[dim];\n dot_y += norm * basis[d + dim];\n }\n // Subtract projection onto span{bx, by} → residual is orthogonal to basis\n for (var dim = 0u; dim < d; dim++) {\n vals[dim] -= dot_x * basis[dim] + dot_y * basis[d + dim];\n }\n }\n\n // Reduce mean terms (used for mean-correction in pass 2)\n for (var dim = 0u; dim < d; dim++) {\n wg_reduce[lid.x] = select(0.0, vals[dim], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) { wg_reduce[lid.x] += wg_reduce[lid.x + s]; }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n partials[wid.x * num_terms + dim] = wg_reduce[0];\n }\n }\n\n // Reduce upper-triangle cross-products\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n wg_reduce[lid.x] = select(0.0, vals[d1] * vals[d2], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) { wg_reduce[lid.x] += wg_reduce[lid.x + s]; }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n partials[wid.x * num_terms + d + cov_idx(d, d1, d2)] = wg_reduce[0];\n }\n }\n }\n}\n\n// ─── Pass 2: reduce across workgroups → final mean-corrected covariance ─────\n\n@compute @workgroup_size(256)\nfn final_reduce(@builtin(local_invocation_id) lid: vec3u) {\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let nwg = params.num_workgroups;\n let N_f32 = f32(params.num_points);\n\n for (var t = 0u; t < num_terms; t++) {\n var sum = 0.0;\n var w = lid.x;\n while (w < nwg) {\n sum += partials[w * num_terms + t];\n w += WG;\n }\n wg_reduce[lid.x] = sum;\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) { wg_reduce[lid.x] += wg_reduce[lid.x + s]; }\n workgroupBarrier();\n }\n if (lid.x == 0u) { result[t] = wg_reduce[0] / N_f32; }\n workgroupBarrier();\n }\n\n // Mean-corrected covariance: cov(X,Y) = E[XY] - E[X]*E[Y]\n if (lid.x == 0u) {\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n let ci = cov_idx(d, d1, d2);\n result[d + ci] -= result[d1] * result[d2];\n }\n }\n }\n}\n`;const he=e=>{let t=e.createShaderModule({label:`compute-residual-pc`,code:me}),n=e.createBindGroupLayout({label:`residual-pc-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}},{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}}]}),r=e.createPipelineLayout({bindGroupLayouts:[n]});return{partialPipeline:e.createComputePipeline({label:`residual-pc-partial`,layout:r,compute:{module:t,entryPoint:`reduce_partial`}}),finalPipeline:e.createComputePipeline({label:`residual-pc-final`,layout:r,compute:{module:t,entryPoint:`final_reduce`}}),bindGroupLayout:n,paramsBuffer:e.createBuffer({label:`residual-pc-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},ge=async(e,t,n,r,i,a,o,s)=>{let c=Math.min(s,64),l=c+c*(c+1)/2,u=Math.ceil(o/256),d=new ArrayBuffer(16),f=new Uint32Array(d);f[0]=o,f[1]=c,f[2]=u,f[3]=0,e.queue.writeBuffer(t.paramsBuffer,0,d);let p=e.createBuffer({label:`residual-pc-partials`,size:u*l*4,usage:GPUBufferUsage.STORAGE}),m=e.createBuffer({label:`residual-pc-result`,size:l*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC}),h=e.createBuffer({label:`residual-pc-readback`,size:l*4,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),g=e.createBindGroup({label:`residual-pc-bg`,layout:t.bindGroupLayout,entries:[{binding:0,resource:{buffer:t.paramsBuffer}},{binding:1,resource:{buffer:n}},{binding:2,resource:{buffer:r}},{binding:3,resource:{buffer:i}},{binding:4,resource:{buffer:p}},{binding:5,resource:{buffer:m}}]}),_=e.createCommandEncoder({label:`residual-pc`}),v=_.beginComputePass({label:`residual-pc-partial`});v.setPipeline(t.partialPipeline),v.setBindGroup(0,g),v.dispatchWorkgroups(u),v.end();let y=_.beginComputePass({label:`residual-pc-final`});y.setPipeline(t.finalPipeline),y.setBindGroup(0,g),y.dispatchWorkgroups(1),y.end(),_.copyBufferToBuffer(m,0,h,0,l*4),e.queue.submit([_.finish()]),await h.mapAsync(GPUMapMode.READ);let b=new Float32Array(h.getMappedRange()).slice();return h.unmap(),p.destroy(),m.destroy(),h.destroy(),_e(b,c,s,a)},_e=(e,t,n,r)=>{let i=new Float64Array(t*t);for(let n=0;n<t;n++)for(let r=n;r<t;r++){let a=e[t+(n*t-n*(n-1)/2+(r-n))];i[n*t+r]=a,i[r*t+n]=a}let a=new Float64Array(t),o=new Float64Array(t);for(let e=0;e<t;e++)a[e]=r[e],o[e]=r[n+e];let s=new Float64Array(t);for(let e=0;e<t;e++)s[e]=1;N(s,a,o,t),P(s,t);let c=new Float64Array(t);for(let e=0;e<15;e++){for(let e=0;e<t;e++){let n=0;for(let r=0;r<t;r++)n+=i[e*t+r]*s[r];c[e]=n}N(c,a,o,t),P(c,t);let e=s;s=c,c=e}let l=new Float32Array(n);for(let e=0;e<t;e++)l[e]=s[e];return l},N=(e,t,n,r)=>{let i=0,a=0;for(let o=0;o<r;o++)i+=e[o]*t[o],a+=e[o]*n[o];for(let o=0;o<r;o++)e[o]=e[o]-(i*t[o]+a*n[o])},P=(e,t)=>{let n=0;for(let r=0;r<t;r++)n+=e[r]*e[r];if(n=Math.sqrt(n),n>1e-12)for(let r=0;r<t;r++)e[r]=e[r]/n};let F=null,I=[],L=null;const R=(e,t)=>{self.postMessage(e,t??[])},ve=(e,t,n,r,i,a)=>{if(e===0||t===0||n===0)return{pointSize:.012,opacity:.7};let o=t*r,s=n*r,c=o*s*i/e,l=Math.sqrt(c/Math.PI),u=a*r*.5,d,f;return l>=u?(d=l,f=1):(d=u,f=Math.max(.01,c/(Math.PI*u*u))),{pointSize:2*d/s,opacity:f}},ye=(e,t)=>e*F.dpr/t,be=e=>{let{style:t,numPoints:n,dpr:r}=F;if(t.pointSize!==`auto`&&t.opacity!==`auto`)return{pointSize:ye(t.pointSize,e.height),opacity:t.opacity,color:t.color,scaleOpacityByZoom:!1};let i=ve(n,e.width/r,e.height/r,r,t.fillTarget,t.minPointSize),a=t.opacity===`auto`;return{pointSize:t.pointSize===`auto`?i.pointSize:ye(t.pointSize,e.height),opacity:a?i.opacity:t.opacity,color:t.color,scaleOpacityByZoom:a}},xe=()=>{let{device:e,mainView:t,tonemapPipeline:n}=F,{width:r,height:i}=t.canvas,a=F.mainHdrTexture;if(a&&a.width===r&&a.height===i)return;a?.destroy();let o=e.createTexture({label:`hdr-main`,size:[r,i],format:F.hdrFormat,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),s=o.createView();F.mainHdrTexture=o,F.mainHdrTextureView=s,F.mainTonemapBindGroup=se(e,n.bindGroupLayout,s,n.paramsBuffer)},Se=()=>{if(!F.previewGpuCanvas)return;let{device:e,tonemapPipeline:t}=F,{width:n,height:r}=F.previewGpuCanvas,i=F.previewHdrTexture;if(i&&i.width===n&&i.height===r)return;i?.destroy();let a=e.createTexture({label:`hdr-preview`,size:[n,r],format:F.hdrFormat,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),o=a.createView();F.previewHdrTexture=a,F.previewHdrTextureView=o,F.previewTonemapBindGroup=se(e,t.bindGroupLayout,o,t.paramsBuffer)},Ce=e=>Math.ceil(e/32)*4,we=()=>{if(!F)throw Error(`state not initialized`);let e=Ce(F.numPoints);return(!F.selectionBuffer||F.selectionBuffer.size!==e)&&(F.selectionBuffer?.destroy(),F.selectionBuffer=F.device.createBuffer({label:`selection-mask`,size:e,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST})),F.selectionBuffer};let z=null;const Te=(e,t,n,r,i=2)=>{let a=r*i;(!z||z.length!==a)&&(z=new Float32Array(a));let o=0,s=0,c=0;for(let a=0;a<r;a++){let l=Math.max(n[a],1e-6),u=e[a],d=e[r+a],f=2/l;z[a]=u*f,z[r+a]=d*f;let p=-t[a]/l;if(o+=p*u*2,s+=p*d*2,i===3){let t=e[2*r+a];z[2*r+a]=t*f,c+=p*t*2}}return{weights:z,biasX:o,biasY:s,biasZ:c}},Ee=()=>{if(!F?.normMins||!F?.normRanges)return null;let e=F.numDims,t=new Float32Array(e);if(F.centering===`mean`&&F.normMeans)t.set(F.normMeans);else for(let n=0;n<e;n++)t[n]=F.normMins[n]+F.normRanges[n]*.5;return t},B=()=>{if(!F?.projectionResources||!F.normRanges)return;let e=F.numDims,t=Ee();if(!t)return;let n=new Float32Array(e*2);for(let r=0;r<e;r++)n[r*2]=t[r],n[r*2+1]=F.normRanges[r];F.device.queue.writeBuffer(F.projectionResources.normParamsBuffer,0,n)};let V=null,De=0,H=!0,U=!1;const W=()=>H?(H=!1,U=!1,J(),F.device.queue.onSubmittedWorkDone().then(()=>{H=!0,U&&F&&!V&&(U=!1,W())},()=>{H=!0,R({type:`error`,message:`WebGPU device lost during rendering`})}),!0):(U=!0,!1),Oe=e=>{if(!V||!F?.tour)return;let t=V.prevTime===null?void 0:e-V.prevTime;if(t!==void 0){let e=t/1e3*V.speed*V.direction/20,n=F.tour.position+e;n-=Math.floor(n),F.tour.position=n}V.prevTime=e,F.directBasis=null,W()&&(V.lastRenderedRafTime!==null&&V.frameTimes.push(e-V.lastRenderedRafTime),V.lastRenderedRafTime=e),e-De>=33&&(R({type:`playbackTick`,position:F.tour.position,frameMs:t}),De=e),V.rafId=requestAnimationFrame(Oe)},ke=(e,t)=>{V?(V.speed=e,V.direction=t):(H=!0,V={speed:e,direction:t,prevTime:null,rafId:0,frameTimes:[],lastRenderedRafTime:null},V.rafId=requestAnimationFrame(Oe))},Ae=()=>{if(V){cancelAnimationFrame(V.rafId),F?.tour&&R({type:`playbackTick`,position:F.tour.position});let e=Float64Array.from(V.frameTimes);R({type:`playbackStopped`,frameTimes:e},[e.buffer]),V=null}},G=(e,t,n,r,i,a)=>{if(!F?.projectionResources||!F.adjBasisBuffer||!F.normRanges)return;let{device:c,numPoints:l,numDims:u}=F,d=F.is3dActive&&F.residualPC,f=d?3:2,p;d?(p=new Float32Array(u*3),p.set(e.subarray(0,u*2)),p.set(F.residualPC,u*2)):p=e;let m=Ee();if(!m)return;let{weights:h,biasX:g,biasY:_,biasZ:v}=Te(p,m,F.normRanges,u,f),y=u*f*4;F.adjBasisBuffer.size<y&&(F.adjBasisBuffer.destroy(),F.adjBasisBuffer=c.createBuffer({label:`adj-basis`,size:y,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),Y()),c.queue.writeBuffer(F.adjBasisBuffer,0,h);let b=be(t.canvas),x=F.backgroundColor,ee=.2126*x[0]+.7152*x[1]+.0722*x[2],S=F.colorState.mode>0,C=!S&&ee>.5,w,T;S?(w=F.pointPipeline.normalPipeline,T=1):C?(w=F.pointPipeline.subtractivePipeline,T=1):(w=F.pointPipeline.additivePipeline,T=0),M(c,F.pointPipeline.uniformBuffer,b,F.styleFlags,F.colorState,C,l,u,g,_,F.maxPoints,v),ae(c,F.tonemapPipeline.paramsBuffer,T);let E=t.canvas,te=E.width/E.height||1;re(c,F.pointPipeline.cameraBuffer,{...i,aspect:te,viewportHeight:E.height});let D=c.createCommandEncoder({label:`render-frame`});o(D,n,w,a,l,x),s(D,t,F.tonemapPipeline.pipeline,r),c.queue.submit([D.finish()])},je={panX:0,panY:0,zoom:1,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1,use3d:!1,rotation:null},Me=()=>{!F||F.previewGpuCanvas||(F.previewGpuCanvas=new OffscreenCanvas(300,300),F.previewGpuView=i(F.previewGpuCanvas,F.device))};let K=!1;const Ne=(e,t)=>{if(!F?.projectionResources||!F.renderBindGroup)return;let n=F.previewCanvases.get(e);n&&(Me(),Se(),!(!F.previewGpuView||!F.previewHdrTextureView||!F.previewTonemapBindGroup)&&((F.previewGpuCanvas.width!==n.canvas.width||F.previewGpuCanvas.height!==n.canvas.height)&&(F.previewGpuCanvas.width=n.canvas.width,F.previewGpuCanvas.height=n.canvas.height,Se(),!F.previewHdrTextureView||!F.previewTonemapBindGroup)||(G(t,F.previewGpuView,F.previewHdrTextureView,F.previewTonemapBindGroup,je,F.renderBindGroup),n.ctx2d.drawImage(F.previewGpuCanvas,0,0),R({type:`rendered`,viewIndex:e}))))},Pe=()=>{if(!F?.projectionResources||!F.tour)return;let{tour:e,previewCanvases:t}=F;for(let[n]of t)n<e.bases.length&&Ne(n,e.bases[n])},Fe=()=>{K||(K=!0,setTimeout(()=>{K=!1,Pe()},0))},q=()=>{F?.projectionResources&&(J(),Fe())},J=()=>{if(!F?.projectionResources||!F.renderBindGroup||(xe(),!F.mainHdrTextureView||!F.mainTonemapBindGroup))return;if(F.directBasis){G(F.directBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),R({type:`rendered`,viewIndex:0});return}if(!F.tour)return;let{tour:e}=F;h(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),G(e.interpolatedBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),R({type:`rendered`,viewIndex:0})},Y=()=>{if(!F?.projectionResources||!F.adjBasisBuffer)return;let{device:e,projectionPipeline:t,pointPipeline:n,projectionResources:r}=F,i=F.colorLutBuffer??n.defaultColorLutBuffer,a=F.selectionBuffer??n.defaultSelectionBuffer,o=n.defaultCatIndicesBuffer;F.colorState.mode===2&&F.activeCatColumnName&&(o=F.categoricalBuffers.get(F.activeCatColumnName)??o),F.projectionBindGroup=fe(e,t.bindGroupLayout,t.paramsBuffer,r),F.renderBindGroup=ie(e,n.bindGroupLayout,n.uniformBuffer,r.dataBuffer,n.cameraBuffer,i,a,F.adjBasisBuffer,o)},X=()=>{F&&(Y(),(F.tour||F.directBasis)&&F.projectionResources&&q())},Ie=()=>{F&&(F.styleFlags.useSelectionMask=!0,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q())};let Z=0;const Le=e=>{if(!F)return;let{device:t}=F;if(e.data.type===`setColorContinuous`){if(e.data.dataVersion!==Z)return;let{columnIndex:n,min:r,range:i,colormap:a}=e.data;F.colorLutBuffer?.destroy(),F.colorLutBuffer=t.createBuffer({label:`color-lut`,size:a.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.colorLutBuffer,0,a),F.colorState={mode:1,columnOffset:n*F.numPoints,min:r,range:i,numStops:a.length,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=null,X();return}if(e.data.type===`setColorCategorical`){if(e.data.dataVersion!==Z)return;let{catColumnName:n,palette:r}=e.data;if(!F.categoricalBuffers.has(n))return;F.colorLutBuffer?.destroy(),F.colorLutBuffer=t.createBuffer({label:`color-lut`,size:r.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.colorLutBuffer,0,r),F.colorState={mode:2,columnOffset:0,min:0,range:1,numStops:r.length,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=n,X();return}if(e.data.type===`setColor2D`){if(e.data.dataVersion!==Z)return;let{columnIndexX:n,columnIndexY:r,minX:i,rangeX:a,minY:o,rangeY:s,lut:c,mapIndex:l}=e.data;c&&(F.colorLutBuffer?.destroy(),F.colorLutBuffer=t.createBuffer({label:`color-lut-2d`,size:c.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.colorLutBuffer,0,c)),F.colorState={mode:3,columnOffset:n*F.numPoints,min:i,range:a,numStops:0,columnOffsetV:r*F.numPoints,minV:o,rangeV:s,mapIndex:l},F.activeCatColumnName=null,X();return}if(e.data.type===`selectContinuous`){if(e.data.dataVersion!==Z||!F.projectionResources)return;let{columnIndex:n,ranges:r}=e.data,{numPoints:i,colorPipelines:a}=F,o=we(),s=t.createBuffer({label:`select-ranges`,size:Math.max(r.byteLength,4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});t.queue.writeBuffer(s,0,r),b(t,a.paramsBuffer,i,n*i,r.length/2);let c=y(t,a.bindGroupLayout,F.projectionResources.dataBuffer,s,o,a.paramsBuffer),l=t.createCommandEncoder({label:`select-continuous`});l.clearBuffer(o,0,o.size);let u=l.beginComputePass({label:`select-continuous`});u.setPipeline(a.selectContinuous),u.setBindGroup(0,c),u.dispatchWorkgroups(Math.ceil(i/256)),u.end(),t.queue.submit([l.finish()]),s.destroy(),Ie();return}if(e.data.type===`selectCategorical`){if(e.data.dataVersion!==Z)return;let{catColumnName:n,selectedLabels:r}=e.data,i=F.categoricalBuffers.get(n);if(!i)return;let{numPoints:a,colorPipelines:o}=F,s=we(),c=t.createBuffer({label:`selected-labels`,size:Math.max(r.byteLength,4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});t.queue.writeBuffer(c,0,r),x(t,o.paramsBuffer,a,r.length);let l=y(t,o.bindGroupLayout,i,c,s,o.paramsBuffer),u=t.createCommandEncoder({label:`select-categorical`});u.clearBuffer(s,0,s.size);let d=u.beginComputePass({label:`select-categorical`});d.setPipeline(o.selectCategorical),d.setBindGroup(0,l),d.dispatchWorkgroups(Math.ceil(a/256)),d.end(),t.queue.submit([u.finish()]),c.destroy(),Ie();return}if(e.data.type!==`data`)return;let{dataVersion:n,dims:r,rows:i,buffers:a,mins:o,ranges:s,means:c,categoricalColumns:l}=e.data;if(Z=n,r<2||i===0||a.length<2){R({type:`error`,message:`Need at least 2 dimensions to render`});return}F.projectionResources&&(F.projectionResources.dataBuffer.destroy(),F.projectionResources.normParamsBuffer.destroy(),F.projectionResources.basisBuffer.destroy(),F.projectionResources.projectedBuffer.destroy()),F.adjBasisBuffer&&(F.adjBasisBuffer.destroy(),F.adjBasisBuffer=null),F.colorLutBuffer&&(F.colorLutBuffer.destroy(),F.colorLutBuffer=null),F.selectionBuffer&&(F.selectionBuffer.destroy(),F.selectionBuffer=null);for(let e of F.categoricalBuffers.values())e.destroy();F.categoricalBuffers.clear(),F.styleFlags={useSelectionMask:!1},F.colorState={mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=null,F.is3dActive=!1,F.rotation3d=null,F.residualPC=null,F.camera.use3d=!1,F.camera.rotation=null;let{projectionPipeline:u}=F,d=de(t,i,r);for(let e=0;e<r;e++)t.queue.writeBuffer(d.dataBuffer,e*i*4,a[e]);for(let e of l){let n=t.createBuffer({label:`cat-indices-${e.name}`,size:e.indices.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});t.queue.writeBuffer(n,0,e.indices),F.categoricalBuffers.set(e.name,n)}if(ue(t,u.paramsBuffer,i,r,2),F.adjBasisBuffer=t.createBuffer({label:`adj-basis`,size:r*2*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),F.normMins=new Float32Array(o),F.normRanges=new Float32Array(s),F.normMeans=new Float32Array(c),F.projectionResources={...d,bindGroup:null},F.numPoints=i,F.numDims=r,B(),Y(),F.tour&&F.tour.dims===r)q();else{let e=Math.max(1,F.previewCanvases.size),t=[];for(let n=0;n<e;n++){let i=new Float32Array(r*2),a=Math.floor(n/e*r);i[a]=1,i[r+(a+1)%r]=1,t.push(i)}F.tour={bases:t,arcLengths:p(t,r),dims:r,orthonormalize:!0,position:0,interpolatedBasis:new Float32Array(r*2)},q()}},Q=(e,t)=>{e.projectionResources&&(e.residualPcPipeline||=he(e.device),e.device.queue.writeBuffer(e.projectionResources.basisBuffer,0,t),ge(e.device,e.residualPcPipeline,e.projectionResources.dataBuffer,e.projectionResources.normParamsBuffer,e.projectionResources.basisBuffer,t,e.numPoints,e.numDims).then(e=>{F?.is3dActive&&(F.residualPC=e,R({type:`residualPC`,residualPC:new Float32Array(e)}),J())}))},$=e=>{if(F){if(e.type===`setBases`){let{bases:t}=e;if(t.length===0)return;let n=t[0].length/2,r=e.tourFamily!==`sequential`,i=p(t,n,r);F.tour={bases:t,arcLengths:i,dims:n,orthonormalize:r,position:F.tour?.position??0,interpolatedBasis:new Float32Array(n*2)},F.projectionResources&&q();return}if(e.type===`setTourPosition`){if(!F.tour)return;F.tour.position=e.position,F.directBasis=null,W();return}if(e.type===`setStyle`){F.style={pointSize:e.pointSize,opacity:e.opacity,color:e.color,minPointSize:e.minPointSize??F.style.minPointSize,fillTarget:e.fillTarget??F.style.fillTarget},(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setCamera`){F.camera={panX:e.pan[0],panY:e.pan[1],zoom:e.zoom,aspect:F.camera.aspect,viewportHeight:F.camera.viewportHeight,insetOffsetY:e.insetOffsetY,insetZoom:e.insetZoom,use3d:F.camera.use3d,rotation:F.camera.rotation},W();return}if(e.type===`setCentering`){if(F.centering=e.centering,B(),(F.tour||F.directBasis)&&F.projectionResources){if(F.is3dActive){let e=F.directBasis??F.tour?.interpolatedBasis;e&&Q(F,e)}q()}return}if(e.type===`resize`){e.dpr!==void 0&&(F.dpr=e.dpr),e.viewIndex===0&&(F.mainView.canvas.width=e.width,F.mainView.canvas.height=e.height,(F.tour||F.directBasis)&&F.projectionResources&&J());return}if(e.type===`addPreviewCanvas`){let t=e.canvas.getContext(`2d`);if(!t)return;F.previewCanvases.set(e.id,{canvas:e.canvas,ctx2d:t}),F.tour&&F.projectionResources&&e.id<F.tour.bases.length&&Ne(e.id,F.tour.bases[e.id]);return}if(e.type===`removePreviewCanvas`){F.previewCanvases.delete(e.id);return}if(e.type===`resizePreview`){let t=F.previewCanvases.get(e.id);if(!t)return;t.canvas.width=e.width,t.canvas.height=e.height,F.tour&&F.projectionResources&&Fe();return}if(e.type===`render`){(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setDirectBasis`){if(!F.projectionResources||!F.renderBindGroup)return;F.directBasis=e.basis,W(),F.is3dActive&&F.projectionResources&&Q(F,e.basis);return}if(e.type===`clearColors`){F.colorLutBuffer&&(F.colorLutBuffer.destroy(),F.colorLutBuffer=null),F.colorState={mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=null,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setBackgroundColor`){F.backgroundColor=e.color,(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setMaxPoints`){F.maxPoints=e.maxPoints,(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setSelectionMask`){let{mask:t}=e;F.selectionBuffer&&F.selectionBuffer.destroy(),F.selectionBuffer=F.device.createBuffer({label:`selection-mask`,size:t.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),F.device.queue.writeBuffer(F.selectionBuffer,0,t),F.styleFlags.useSelectionMask=!0,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q();let n=new Uint32Array(t);R({type:`selectionResult`,mask:n},[n.buffer]);return}if(e.type===`clearSelectionMask`){F.selectionBuffer&&(F.selectionBuffer.destroy(),F.selectionBuffer=null),F.styleFlags.useSelectionMask=!1,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q(),R({type:`selectionResult`,mask:new Uint32Array});return}if(e.type===`enable3d`){if(!F.projectionResources)return;let e=F.directBasis??F.tour?.interpolatedBasis;if(!e)return;if(F.is3dActive=!0,F.camera.use3d=!0,F.adjBasisBuffer){let e=F.numDims*3*4;F.adjBasisBuffer.size<e&&(F.adjBasisBuffer.destroy(),F.adjBasisBuffer=F.device.createBuffer({label:`adj-basis`,size:e,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),Y())}J(),Q(F,e);return}if(e.type===`disable3d`){F.is3dActive=!1,F.residualPC=null,F.rotation3d=null,F.camera.use3d=!1,F.camera.rotation=null,J();return}if(e.type===`set3dRotation`){F.rotation3d=e.matrix,F.camera.rotation=e.matrix,W();return}if(e.type===`startPlayback`){if(!F.tour)return;ke(e.speed,e.direction);return}if(e.type===`stopPlayback`){Ae();return}if(e.type===`computePCA`){if(!F.projectionResources||F.numDims<2){R({type:`error`,message:`No data loaded for PCA`});return}let{device:e,projectionResources:t,numPoints:i,numDims:a}=F;L||=n(e),r(e,L,t.dataBuffer,t.normParamsBuffer,i,a).then(({eigenvalues:e,eigenvectors:t})=>{R({type:`pcaResult`,eigenvectors:t,eigenvalues:e,numDims:t.length})}).catch(e=>{R({type:`error`,message:`PCA failed: ${e instanceof Error?e.message:String(e)}`})});return}if(e.type===`lassoSelect`){if(!F.projectionResources||!F.projectionBindGroup||F.numPoints===0)return;let{device:t,numPoints:n,projectionPipeline:r,projectionResources:i,camera:a}=F,{polygon:o}=e,s=o.length/2;if(s<3)return;let c=null;if(F.directBasis)c=F.directBasis;else if(F.tour){let{tour:e}=F;h(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),c=e.interpolatedBasis}if(!c)return;t.queue.writeBuffer(i.basisBuffer,0,c);let l=pe(t,r,F.projectionBindGroup,n);t.queue.submit([l]);let u=F.mainView.canvas,d=u.width/u.height||1,f=a.insetZoom,p=a.zoom*f,m=new Float32Array(o.length);for(let e=0;e<s;e++)m[e*2]=o[e*2]*d/p-a.panX,m[e*2+1]=(o[e*2+1]-a.insetOffsetY)/p-a.panY;let g=n*2*4,_=t.createBuffer({label:`proj-readback`,size:g,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),v=t.createCommandEncoder();v.copyBufferToBuffer(i.projectedBuffer,0,_,0,g),t.queue.submit([v.finish()]),_.mapAsync(GPUMapMode.READ).then(()=>{let e=new Float32Array(_.getMappedRange()),r=new Uint32Array(Math.ceil(n/32));for(let t=0;t<n;t++){let n=e[t*2],i=e[t*2+1];if(Ve(n,i,m,s)){let e=t>>5;r[e]=(r[e]??0)|1<<(t&31)}}_.unmap(),_.destroy(),F.selectionBuffer&&F.selectionBuffer.destroy(),F.selectionBuffer=t.createBuffer({label:`selection-mask`,size:r.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.selectionBuffer,0,r),F.styleFlags.useSelectionMask=!0,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q();let i=new Uint32Array(r);R({type:`selectionResult`,mask:i},[i.buffer])});return}if(e.type===`getProjectedPositions`){if(!F.projectionResources||!F.projectionBindGroup||F.numPoints===0){R({type:`projectedPositions`,positions:new Float32Array},[]);return}let{device:e,numPoints:t,projectionPipeline:n,projectionResources:r}=F,i=null;if(F.directBasis)i=F.directBasis;else if(F.tour){let{tour:e}=F;h(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),i=e.interpolatedBasis}if(!i){R({type:`projectedPositions`,positions:new Float32Array},[]);return}e.queue.writeBuffer(r.basisBuffer,0,i);let a=pe(e,n,F.projectionBindGroup,t);e.queue.submit([a]);let o=t*2*4,s=e.createBuffer({label:`proj-positions-readback`,size:o,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),c=e.createCommandEncoder();c.copyBufferToBuffer(r.projectedBuffer,0,s,0,o),e.queue.submit([c.finish()]),s.mapAsync(GPUMapMode.READ).then(()=>{let e=new Float32Array(s.getMappedRange()).slice();s.unmap(),s.destroy(),R({type:`projectedPositions`,positions:e},[e.buffer])});return}if(e.type===`getPointData`){if(!F.projectionResources||F.numPoints===0){R({type:`pointData`,pointIndex:e.pointIndex,numericValues:{},categoricalValues:{}});return}let{device:t,numPoints:n,numDims:r,projectionResources:i}=F,a=e.pointIndex;if(a<0||a>=n){R({type:`pointData`,pointIndex:a,numericValues:{},categoricalValues:{}});return}let o=r*4,s=t.createBuffer({label:`point-data-readback`,size:o,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),c=t.createCommandEncoder();for(let e=0;e<r;e++)c.copyBufferToBuffer(i.dataBuffer,(e*n+a)*4,s,e*4,4);let l=Array.from(F.categoricalBuffers.keys()),u=l.length*4,d=null;if(u>0){d=t.createBuffer({label:`point-cat-readback`,size:u,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});let e=0;for(let t of l){let n=F.categoricalBuffers.get(t);c.copyBufferToBuffer(n,a*4,d,e,4),e+=4}}t.queue.submit([c.finish()]);let f=s.mapAsync(GPUMapMode.READ).then(()=>{let e=new Float32Array(s.getMappedRange()).slice();return s.unmap(),s.destroy(),e}),p=d?d.mapAsync(GPUMapMode.READ).then(()=>{let e=new Uint32Array(d.getMappedRange()).slice();return d.unmap(),d.destroy(),e}):Promise.resolve(new Uint32Array);Promise.all([f,p]).then(([e,t])=>{let n={};for(let t=0;t<r;t++)n[String(t)]=e[t];let i={};for(let e=0;e<l.length;e++)i[l[e]]=t[e];R({type:`pointData`,pointIndex:a,numericValues:n,categoricalValues:i})});return}if(e.type===`benchmark`){Be(e.numFrames);return}if(e.type===`getMetrics`){ze();return}}},Re=()=>{if(!F)return 0;let e=0;if(F.projectionResources){let t=F.projectionResources;e+=t.dataBuffer.size+t.normParamsBuffer.size+t.basisBuffer.size+t.projectedBuffer.size}F.adjBasisBuffer&&(e+=F.adjBasisBuffer.size),F.selectionBuffer&&(e+=F.selectionBuffer.size),F.colorLutBuffer&&(e+=F.colorLutBuffer.size);for(let t of F.categoricalBuffers.values())e+=t.size;F.mainHdrTexture&&(e+=F.mainHdrTexture.width*F.mainHdrTexture.height*16),F.previewHdrTexture&&(e+=F.previewHdrTexture.width*F.previewHdrTexture.height*16);let t=F.pointPipeline;return e+=t.uniformBuffer.size+t.cameraBuffer.size,e+=t.defaultColorLutBuffer.size+t.defaultSelectionBuffer.size+t.defaultCatIndicesBuffer.size,e+=F.projectionPipeline.paramsBuffer.size,e+=F.tonemapPipeline.paramsBuffer.size,e},ze=()=>{if(!F)return;let e=0;if(F.tour){for(let t of F.tour.bases)e+=t.byteLength;e+=F.tour.interpolatedBasis.byteLength}R({type:`metricsResult`,gpuMemoryBytes:Re(),numPoints:F.numPoints,numDims:F.numDims,workerJsHeapBytes:e})},Be=async e=>{if(!F?.tour||!F.renderBindGroup||F.numPoints===0){R({type:`error`,message:`No data/tour loaded for benchmark`});return}let{device:t,numPoints:n,numDims:r,tour:i}=F,a=i.position;F.directBasis=null;for(let e=0;e<5;e++)i.position=e/5,h(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize),xe(),G(i.interpolatedBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),await t.queue.onSubmittedWorkDone();let o=new Float64Array(e);for(let n=0;n<e;n++){i.position=n/e,h(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize);let r=performance.now();G(i.interpolatedBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),await t.queue.onSubmittedWorkDone(),o[n]=performance.now()-r}i.position=a,R({type:`benchmarkResult`,frameTimes:o,numPoints:n,numDims:r},[o.buffer])},Ve=(e,t,n,r)=>{let i=!1;for(let a=0,o=r-1;a<r;o=a++){let r=n[a*2],s=n[a*2+1],c=n[o*2],l=n[o*2+1];s>t!=l>t&&e<(c-r)*(t-s)/(l-s)+r&&(i=!i)}return i};self.onmessage=async e=>{let t=e.data;if(t.type===`init`){try{let{device:e}=await ee(),n=i(t.canvas,e),r=navigator.gpu.getPreferredCanvasFormat(),a=`rgba32float`,o=ne(e,a),s=le(e),c=v(e),l=oe(e,r);F={device:e,mainView:n,previewGpuCanvas:null,previewGpuView:null,previewCanvases:new Map,pointPipeline:o,renderBindGroup:null,projectionPipeline:s,projectionResources:null,projectionBindGroup:null,colorPipelines:c,numPoints:0,numDims:0,categoricalBuffers:new Map,tour:null,style:{pointSize:`auto`,opacity:`auto`,color:[.25,.5,.9],minPointSize:2,fillTarget:.5},styleFlags:{useSelectionMask:!1},camera:{panX:0,panY:0,zoom:t.zoom,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1,use3d:!1,rotation:null},colorLutBuffer:null,colorState:{mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},activeCatColumnName:null,selectionBuffer:null,directBasis:null,backgroundColor:[0,0,0],dpr:t.dpr,hdrFormat:a,tonemapPipeline:l,mainHdrTexture:null,mainHdrTextureView:null,mainTonemapBindGroup:null,previewHdrTexture:null,previewHdrTextureView:null,previewTonemapBindGroup:null,maxPoints:0,adjBasisBuffer:null,normMins:null,normRanges:null,normMeans:null,centering:`midrange`,is3dActive:!1,rotation3d:null,residualPC:null,residualPcPipeline:null},t.dataPort.onmessage=Le;let u=I;if(I=null,u)for(let e of u)$(e);R({type:`ready`})}catch(e){R({type:`error`,message:e instanceof Error?e.message:String(e)})}return}if(I){I.push(t);return}$(t)};", h = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", m], { type: "text/javascript;charset=utf-8" });
2638
+ var m = "var e=`// PCA compute shader — two-pass reduction to compute covariance matrix.\n//\n// Pass 1 (reduce_partial): Each workgroup reduces its chunk of points into\n// per-workgroup partial sums for d means + d*(d+1)/2 cross-product terms.\n//\n// Pass 2 (final_reduce): A single workgroup reduces all partial sums into\n// the final means and covariance matrix.\n//\n// Data is normalized identically to the projection shader:\n// v = (raw - center) / range\n\nstruct PcaParams {\n num_points: u32,\n num_dims: u32,\n num_workgroups: u32,\n _pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: PcaParams;\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n@group(0) @binding(2) var<storage, read> norm_params: array<vec2f>;\n@group(0) @binding(3) var<storage, read_write> partials: array<f32>;\n// result layout: [mean_0 .. mean_{d-1}, cov_00, cov_01, ..., cov_{d-1,d-1}]\n// upper-triangle covariance packed as: d1*d - d1*(d1-1)/2 + (d2-d1)\n@group(0) @binding(4) var<storage, read_write> result: array<f32>;\n\nconst WG: u32 = 256u;\nconst MAX_DIMS: u32 = 32u;\n\nvar<workgroup> wg_reduce: array<f32, 256>;\n\nfn normalize(dim: u32, point_idx: u32, N: u32) -> f32 {\n let raw = data[dim * N + point_idx];\n let np = norm_params[dim];\n let range = max(np.y, 1e-6);\n return (raw - np.x) / range;\n}\n\nfn cov_idx(d: u32, d1: u32, d2: u32) -> u32 {\n return d1 * d - d1 * (d1 - 1u) / 2u + (d2 - d1);\n}\n\n// ─── Pass 1: per-workgroup partial sums ─────────────────────────────────────\n\n@compute @workgroup_size(256)\nfn reduce_partial(\n @builtin(global_invocation_id) gid: vec3u,\n @builtin(local_invocation_id) lid: vec3u,\n @builtin(workgroup_id) wid: vec3u,\n) {\n let N = params.num_points;\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let i = gid.x;\n let valid = i < N;\n\n // Load normalized values for this point into registers\n var vals: array<f32, 32>; // MAX_DIMS\n if (valid) {\n for (var dim = 0u; dim < d; dim++) {\n vals[dim] = normalize(dim, i, N);\n }\n }\n\n // Reduce mean terms\n for (var dim = 0u; dim < d; dim++) {\n wg_reduce[lid.x] = select(0.0, vals[dim], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) {\n wg_reduce[lid.x] += wg_reduce[lid.x + s];\n }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n partials[wid.x * num_terms + dim] = wg_reduce[0];\n }\n }\n\n // Reduce upper-triangle covariance terms (including diagonal)\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n wg_reduce[lid.x] = select(0.0, vals[d1] * vals[d2], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) {\n wg_reduce[lid.x] += wg_reduce[lid.x + s];\n }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n let ci = cov_idx(d, d1, d2);\n partials[wid.x * num_terms + d + ci] = wg_reduce[0];\n }\n }\n }\n}\n\n// ─── Pass 2: reduce across workgroups → final means + covariance ────────────\n\n@compute @workgroup_size(256)\nfn final_reduce(\n @builtin(local_invocation_id) lid: vec3u,\n) {\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let nwg = params.num_workgroups;\n let N_f32 = f32(params.num_points);\n\n // For each term, reduce across all workgroups\n for (var t = 0u; t < num_terms; t++) {\n // Each thread accumulates a stride of workgroups\n var sum = 0.0;\n var w = lid.x;\n while (w < nwg) {\n sum += partials[w * num_terms + t];\n w += WG;\n }\n wg_reduce[lid.x] = sum;\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) {\n wg_reduce[lid.x] += wg_reduce[lid.x + s];\n }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n // Store as E[X] or E[XY]\n result[t] = wg_reduce[0] / N_f32;\n }\n workgroupBarrier();\n }\n\n // Correct covariance: cov(X,Y) = E[XY] - E[X]*E[Y]\n if (lid.x == 0u) {\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n let ci = cov_idx(d, d1, d2);\n result[d + ci] -= result[d1] * result[d2];\n }\n }\n }\n}\n`;const t=(e,t)=>{let n=e,r=new Float32Array(t*t);for(let e=0;e<t;e++)r[e*t+e]=1;let i=1e-10;for(let e=0;e<50;e++){let e=0;for(let r=0;r<t;r++)for(let i=r+1;i<t;i++)e+=n[r*t+i]*n[r*t+i];if(e<i)break;for(let e=0;e<t;e++)for(let a=e+1;a<t;a++){let o=n[e*t+a];if(Math.abs(o)<i*.01)continue;let s=n[e*t+e],c=n[a*t+a],l=(c-s)/(2*o),u=Math.sign(l)/(Math.abs(l)+Math.sqrt(1+l*l)),d=1/Math.sqrt(1+u*u),f=u*d;n[e*t+e]=s-u*o,n[a*t+a]=c+u*o,n[e*t+a]=0,n[a*t+e]=0;for(let r=0;r<t;r++){if(r===e||r===a)continue;let i=n[r*t+e],o=n[r*t+a];n[r*t+e]=d*i-f*o,n[e*t+r]=d*i-f*o,n[r*t+a]=f*i+d*o,n[a*t+r]=f*i+d*o}for(let n=0;n<t;n++){let i=r[n*t+e],o=r[n*t+a];r[n*t+e]=d*i-f*o,r[n*t+a]=f*i+d*o}}}let a=new Float32Array(t),o=Array.from({length:t},(e,t)=>t);for(let e=0;e<t;e++)a[e]=n[e*t+e];o.sort((e,t)=>a[t]-a[e]);let s=new Float32Array(t),c=[];for(let e=0;e<t;e++){let n=o[e];s[e]=a[n];let i=new Float32Array(t);for(let e=0;e<t;e++)i[e]=r[e*t+n];c.push(i)}return{eigenvalues:s,eigenvectors:c}},n=t=>{let n=t.createShaderModule({label:`compute-pca`,code:e}),r=t.createBindGroupLayout({label:`pca-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}},{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}}]}),i=t.createPipelineLayout({bindGroupLayouts:[r]});return{partialPipeline:t.createComputePipeline({label:`pca-partial-pipeline`,layout:i,compute:{module:n,entryPoint:`reduce_partial`}}),finalPipeline:t.createComputePipeline({label:`pca-final-pipeline`,layout:i,compute:{module:n,entryPoint:`final_reduce`}}),bindGroupLayout:r,paramsBuffer:t.createBuffer({label:`pca-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},r=async(e,n,r,i,a,o)=>{let s=Math.min(o,32),c=s+s*(s+1)/2,l=Math.ceil(a/256),u=new ArrayBuffer(16),d=new Uint32Array(u);d[0]=a,d[1]=s,d[2]=l,d[3]=0,e.queue.writeBuffer(n.paramsBuffer,0,u);let f=e.createBuffer({label:`pca-partials`,size:l*c*4,usage:GPUBufferUsage.STORAGE}),p=c*4,m=e.createBuffer({label:`pca-result`,size:p,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC}),h=e.createBuffer({label:`pca-readback`,size:p,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),g=e.createBindGroup({label:`pca-bg`,layout:n.bindGroupLayout,entries:[{binding:0,resource:{buffer:n.paramsBuffer}},{binding:1,resource:{buffer:r}},{binding:2,resource:{buffer:i}},{binding:3,resource:{buffer:f}},{binding:4,resource:{buffer:m}}]}),_=e.createCommandEncoder({label:`pca-compute`}),v=_.beginComputePass({label:`pca-partial`});v.setPipeline(n.partialPipeline),v.setBindGroup(0,g),v.dispatchWorkgroups(l),v.end();let y=_.beginComputePass({label:`pca-final`});y.setPipeline(n.finalPipeline),y.setBindGroup(0,g),y.dispatchWorkgroups(1),y.end(),_.copyBufferToBuffer(m,0,h,0,p),e.queue.submit([_.finish()]),await h.mapAsync(GPUMapMode.READ);let b=new Float32Array(h.getMappedRange()).slice();h.unmap(),f.destroy(),m.destroy(),h.destroy();let x=new Float32Array(s*s);for(let e=0;e<s;e++)for(let t=e;t<s;t++){let n=e*s-e*(e-1)/2+(t-e);x[e*s+t]=b[s+n],x[t*s+e]=b[s+n]}return t(x,s)},i=(e,t)=>{let n=e.getContext(`webgpu`);if(!n)throw Error(`Failed to get WebGPU context from canvas. Ensure WebGPU is supported and the canvas is valid.`);let r=navigator.gpu.getPreferredCanvasFormat();return n.configure({device:t,format:r,alphaMode:`premultiplied`}),{canvas:e,context:n,format:r}},a=2e6,o=(e,t,n,r,i,o=[0,0,0])=>{let s=e.beginRenderPass({colorAttachments:[{view:t,clearValue:{r:o[0],g:o[1],b:o[2],a:1},loadOp:`clear`,storeOp:`store`}]});if(i>0){s.setPipeline(n),s.setBindGroup(0,r);for(let e=0;e<i;e+=a){let t=Math.min(a,i-e);s.draw(4,t,0,e)}}s.end()},s=(e,t,n,r)=>{let i=e.beginRenderPass({colorAttachments:[{view:t.context.getCurrentTexture().createView(),loadOp:`clear`,storeOp:`store`,clearValue:{r:0,g:0,b:0,a:1}}]});i.setPipeline(n),i.setBindGroup(0,r),i.draw(3),i.end()},c=1e-10,l=e=>{let t=e[0],n=e[1],r=e[2],i=e[3],a=t*t+r*r,o=t*n+r*i,s=n*n+i*i,l=a+s,u=a*s-o*o,d=Math.sqrt(Math.max(0,l*l*.25-u)),f=l*.5+d,p=l*.5-d,m=Math.sqrt(Math.max(0,f)),h=Math.sqrt(Math.max(0,p)),g=new Float32Array([m,h]),_=new Float32Array(4);if(Math.abs(o)>c){let e=f-s,t=Math.sqrt(e*e+o*o);_[0]=e/t,_[1]=o/t;let n=p-s,r=Math.sqrt(n*n+o*o);_[2]=n/r,_[3]=o/r}else _[0]=1,_[1]=0,_[2]=0,_[3]=1;let v=new Float32Array(4);return m>c&&(v[0]=(t*_[0]+n*_[1])/m,v[2]=(r*_[0]+i*_[1])/m),h>c&&(v[1]=(t*_[2]+n*_[3])/h,v[3]=(r*_[2]+i*_[3])/h),m<=c&&h>c?(v[0]=-v[3],v[2]=v[1]):h<=c&&m>c?(v[1]=-v[2],v[3]=v[0]):m<=c&&h<=c&&(v[0]=1,v[1]=0,v[2]=0,v[3]=1),{U:v,S:g,V:_}},u=(e,t)=>{let n=0;for(let r=0;r<t;r++)n+=e[r]*e[r];if(n=Math.sqrt(n),n>1e-10)for(let r=0;r<t;r++)e[r]=e[r]/n;let r=0;for(let n=0;n<t;n++)r+=e[n]*e[t+n];for(let n=0;n<t;n++)e[t+n]=e[t+n]-r*e[n];let i=0;for(let n=0;n<t;n++)i+=e[t+n]*e[t+n];if(i=Math.sqrt(i),i>1e-10)for(let n=0;n<t;n++)e[t+n]=e[t+n]/i},d=(e,t,n,r,i,a,o,s=!0)=>{let c=o*o,l=c*o,d=.5*(-o+2*c-l),f=.5*(2-5*c+3*l),p=.5*(o+4*c-3*l),m=.5*(-c+l);for(let o=0;o<a*2;o++)e[o]=d*t[o]+f*n[o]+p*r[o]+m*i[o];return s&&u(e,a),e},f=(e,t,n)=>{let r=new Float32Array(4);for(let i=0;i<2;i++)for(let a=0;a<2;a++){let o=0;for(let r=0;r<n;r++)o+=e[i*n+r]*t[a*n+r];r[i*2+a]=o}let{S:i}=l(r),a=Math.acos(Math.max(-1,Math.min(1,i[0]))),o=Math.acos(Math.max(-1,Math.min(1,i[1])));return Math.sqrt(a*a+o*o)},p=(e,t,n=!0)=>{let r=e.length,i=new Float32Array(r+1);if(i[0]=0,!n){for(let e=1;e<=r;e++)i[e]=e/r;return i}let a=new Float32Array(t*2),o=new Float32Array(t*2);for(let n=0;n<r;n++){let s=(n-1+r)%r,c=n,l=(n+1)%r,u=(n+2)%r,p=0;d(a,e[s],e[c],e[l],e[u],t,0);for(let n=1;n<=8;n++){let r=n/8;d(o,e[s],e[c],e[l],e[u],t,r),p+=f(a,o,t),a.set(o)}i[n+1]=i[n]+p}let s=i[r];if(s>1e-10)for(let e=1;e<=r;e++)i[e]=i[e]/s;return i},m=(e,t)=>{let n=e.length-1,r=Math.max(0,Math.min(1,t)),i=0,a=n-1;for(;i<a;){let t=i+a>>>1;e[t+1]<=r?i=t+1:a=t}let o=e[i],s=e[i+1]-o,c=s>1e-10?(r-o)/s:0;return{segment:i,localT:c}},h=(e,t,n,r,i,a=!0)=>{let o=t.length,{segment:s,localT:c}=m(n,i),l=(s-1+o)%o,u=s,f=(s+1)%o,p=(s+2)%o;return d(e,t[l],t[u],t[f],t[p],r,c,a)};var g=`// GPU categorical selection (bit-packed output)\n//\n// For each point, checks if its category index is in the selected set.\n// Output mask must be pre-cleared; bits are set via atomicOr.\n\nstruct Params {\n num_points: u32,\n num_labels: u32,\n}\n\n@group(0) @binding(0) var<storage, read> indices: array<u32>;\n@group(0) @binding(1) var<storage, read> selected: array<u32>; // 1 per label: selected or not\n@group(0) @binding(2) var<storage, read_write> mask: array<atomic<u32>>;\n@group(0) @binding(3) var<uniform> params: Params;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.num_points) { return; }\n let idx = indices[i];\n if (idx < params.num_labels && selected[idx] > 0u) {\n atomicOr(&mask[i / 32u], 1u << (i % 32u));\n }\n}\n`,_=`// GPU continuous selection (bit-packed output)\n//\n// For each point, checks if the column value falls within any [lo, hi] range.\n// Output mask must be pre-cleared; bits are set via atomicOr.\n\nstruct Params {\n num_points: u32,\n column_offset: u32, // columnIndex * numPoints\n num_ranges: u32,\n}\n\n@group(0) @binding(0) var<storage, read> data: array<f32>;\n@group(0) @binding(1) var<storage, read> ranges: array<f32>; // [lo0, hi0, lo1, hi1, ...]\n@group(0) @binding(2) var<storage, read_write> mask: array<atomic<u32>>;\n@group(0) @binding(3) var<uniform> params: Params;\n\n@compute @workgroup_size(256)\nfn main(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.num_points) { return; }\n\n let v = data[params.column_offset + i];\n for (var r = 0u; r < params.num_ranges; r++) {\n if (v >= ranges[r * 2u] && v <= ranges[r * 2u + 1u]) {\n atomicOr(&mask[i / 32u], 1u << (i % 32u));\n break;\n }\n }\n}\n`;const v=e=>{let t=e.createBindGroupLayout({label:`color-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}}]}),n=e.createPipelineLayout({bindGroupLayouts:[t]}),r=(t,r)=>e.createComputePipeline({label:t,layout:n,compute:{module:e.createShaderModule({label:t,code:r}),entryPoint:`main`}}),i=e.createBuffer({label:`color-params`,size:32,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});return{selectContinuous:r(`select-continuous`,_),selectCategorical:r(`select-categorical`,g),bindGroupLayout:t,paramsBuffer:i}},y=(e,t,n,r,i,a)=>e.createBindGroup({label:`color-bg`,layout:t,entries:[{binding:0,resource:{buffer:n}},{binding:1,resource:{buffer:r}},{binding:2,resource:{buffer:i}},{binding:3,resource:{buffer:a}}]}),b=(e,t,n,r,i)=>{let a=new ArrayBuffer(32),o=new Uint32Array(a);o[0]=n,o[1]=r,o[2]=i,e.queue.writeBuffer(t,0,a)},x=(e,t,n,r)=>{let i=new ArrayBuffer(32),a=new Uint32Array(i);a[0]=n,a[1]=r,e.queue.writeBuffer(t,0,i)},ee=async()=>{if(!navigator.gpu)throw Error(`WebGPU is not supported in this environment. Please use Chrome 113+, Edge 113+, or Firefox Nightly with WebGPU enabled.`);let e=await navigator.gpu.requestAdapter({powerPreference:`high-performance`});if(!e)throw Error(`No WebGPU adapter found. Your GPU may not support WebGPU, or the browser may have blocked access.`);if(!e.features.has(`float32-blendable`))throw Error(`Your GPU does not support float32-blendable, which is required for HDR rendering. Please try a different browser or device with a GPU that supports this WebGPU feature.`);let t=await e.requestDevice({label:`dtour-scatter`,requiredFeatures:[`float32-blendable`],requiredLimits:{maxStorageBufferBindingSize:e.limits.maxStorageBufferBindingSize,maxBufferSize:e.limits.maxBufferSize}});return t.lost.then(e=>{console.error(`WebGPU device lost: ${e.message} (reason: ${e.reason})`)}),{adapter:e,device:t}};var S=`// Point renderer — instanced quads with SDF circle anti-aliasing\n//\n// Projects raw ND data to 2D inline using pre-adjusted basis weights and\n// biases (normalization folded into the basis on CPU). This eliminates the\n// separate compute-projection pass and its GPU barrier.\n//\n// Trade-off: the projection loop runs once per vertex (4x per point) since\n// vertex shaders can't share results across a strip. This is a net win\n// because eliminating the compute dispatch + barrier + projected-buffer\n// read costs more than the redundant ALU, and GPUs are throughput-oriented.\n// For very high dim counts (>200) this balance may shift — profile first.\n//\n// Applies 2D camera transform (pan/zoom/aspect). Each point is rendered as\n// a unit quad (triangle strip, 4 vertices) scaled by point_size (NDC units).\n//\n// Color mapping: instead of a pre-computed N-element color buffer, the shader\n// reads raw data values and looks up colors from a tiny LUT at render time.\n// This eliminates the 20MB color buffer for 5M points and makes color changes\n// instant (just swap the ~100-byte LUT + update uniforms).\n\nstruct Uniforms {\n point_size: f32,\n opacity: f32,\n // Color mode: 0 = uniform color, 1 = continuous, 2 = categorical\n color_mode: u32,\n useSelectionMask: f32,\n color: vec4f,\n useSubtractive: f32,\n num_points: u32,\n num_dims: u32,\n max_points: u32,\n bias: vec2f,\n // Color mapping params (used when color_mode > 0)\n color_column_offset: u32, // columnIndex * num_points (continuous only)\n color_min: f32,\n color_range: f32,\n color_num_stops: u32, // LUT size\n bias_z: f32, // z-axis bias (3D mode only)\n // 2D colormap params (used when color_mode == 3)\n color_column_offset_v: u32, // second column index * num_points\n color_min_v: f32,\n color_range_v: f32,\n color2d_map_index: u32, // 0-5 = LUT-based, 6 = oklab_polar\n scale_opacity_by_zoom: f32, // 1.0 = auto (zoom² density scaling), 0.0 = manual (fixed)\n}\n\nstruct Camera {\n pan_x: f32,\n pan_y: f32,\n zoom: f32,\n aspect: f32,\n viewport_height: f32,\n // NDC-space Y offset applied after zoom — used to shift the viewport\n // down when a toolbar covers the top of the canvas.\n inset_offset_y: f32,\n // Zoom multiplier for the inset — scales content to fit in the visible\n // area below the toolbar (typically (H - toolbar) / H).\n inset_zoom: f32,\n // 3D camera rotation: 0.0 = 2D mode (skip z projection), 1.0 = 3D mode\n use_3d: f32,\n // 3×3 rotation matrix (column-major, each column padded to vec4f for alignment)\n rot_col0: vec3f,\n _pad0: f32,\n rot_col1: vec3f,\n _pad1: f32,\n rot_col2: vec3f,\n _pad2: f32,\n}\n\n@group(0) @binding(0) var<uniform> uni: Uniforms;\n// Raw ND data (column-major): dim0[0..N], dim1[0..N], ..., dimP[0..N]\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n@group(0) @binding(2) var<uniform> camera: Camera;\n// Colormap/palette LUT — packed RGBA8 u32 entries (25 for continuous, up to 256 for categorical)\n@group(0) @binding(3) var<storage, read> colorLut: array<u32>;\n// Bit-packed selection mask (1 bit per point, 32 per u32)\n@group(0) @binding(4) var<storage, read> selectionMask: array<u32>;\n// Adjusted basis weights (normalization folded in):\n// first num_dims = x-weights, next num_dims = y-weights\n// adj_basis[d] = basis[d] / range[d] * viewport_scale\n@group(0) @binding(5) var<storage, read> adj_basis: array<f32>;\n// Categorical index buffer (per-point category index, used when color_mode == 2)\n@group(0) @binding(6) var<storage, read> catIndices: array<u32>;\n\nstruct VertOut {\n @builtin(position) clip_pos: vec4f,\n @location(0) uv: vec2f,\n @location(1) point_color: vec4f,\n @location(2) @interpolate(flat) selected: u32,\n @location(3) @interpolate(flat) effective_opacity: f32,\n}\n\n// Triangle strip: 4 vertices forming a unit quad [-1, 1]²\nfn quad_vertex(vi: u32) -> vec2f {\n switch vi {\n case 0u: { return vec2f(-1.0, -1.0); }\n case 1u: { return vec2f( 1.0, -1.0); }\n case 2u: { return vec2f(-1.0, 1.0); }\n default: { return vec2f( 1.0, 1.0); }\n }\n}\n\n// Unpack RGBA8 from u32 (0xAABBGGRR little-endian)\nfn unpackColor(packed: u32) -> vec4f {\n let r = f32(packed & 0xFFu) / 255.0;\n let g = f32((packed >> 8u) & 0xFFu) / 255.0;\n let b = f32((packed >> 16u) & 0xFFu) / 255.0;\n let a = f32((packed >> 24u) & 0xFFu) / 255.0;\n return vec4f(r, g, b, a);\n}\n\n// PCG hash — fast, high-quality deterministic random from instance index.\n// Used for vertex-shader decimation when num_points > max_points.\nfn pcg_hash(input: u32) -> u32 {\n var state = input * 747796405u + 2891336453u;\n let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\n\nfn random_float(seed: u32) -> f32 {\n return f32(pcg_hash(seed)) / 4294967295.0;\n}\n\n// ─── 2D Colormap helpers ──────────────────────────────────────────────────\n\n// Read f32 from the colorLut buffer (which stores u32). Used for 2D colormap LUT curves.\nfn lut_f32(index: u32) -> f32 {\n return bitcast<f32>(colorLut[index]);\n}\n\n// Linearly interpolate a 16-entry LUT curve stored in colorLut at the given base offset.\nfn lut_interp(base: u32, t: f32) -> f32 {\n let pos = t * 15.0;\n let lo = min(u32(floor(pos)), 14u);\n let hi = lo + 1u;\n return mix(lut_f32(base + lo), lut_f32(base + hi), fract(pos));\n}\n\n// Reconstruct a 2D colormap color from SVD rank-1 LUT curves stored in colorLut.\n// Layout: R_X[16] R_Y[16] G_X[16] G_Y[16] B_X[16] B_Y[16] B_X2[16] B_Y2[16]\nfn colormap2d_lut(u: f32, v: f32) -> vec3f {\n let r = lut_interp(0u, u) * lut_interp(16u, v);\n let g = lut_interp(32u, u) * lut_interp(48u, v);\n let b = lut_interp(64u, u) * lut_interp(80u, v)\n + lut_interp(96u, u) * lut_interp(112u, v); // rank-2 correction\n return clamp(vec3f(r, g, b), vec3f(0.0), vec3f(1.0));\n}\n\n// Oklab → linear sRGB conversion\nfn oklab_to_linear(L: f32, a: f32, b: f32) -> vec3f {\n let l_ = L + 0.3963377774 * a + 0.2158037573 * b;\n let m_ = L - 0.1055613458 * a - 0.0638541728 * b;\n let s_ = L - 0.0894841775 * a - 1.2914855480 * b;\n let l = l_ * l_ * l_;\n let m = m_ * m_ * m_;\n let s = s_ * s_ * s_;\n return vec3f(\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s\n );\n}\n\n// Oklab polar 2D colormap: hue = angle, chroma = radius, lightness varies\nfn colormap2d_oklab_polar(u: f32, v: f32) -> vec3f {\n let cx = u - 0.5;\n let cy = v - 0.5;\n let radius = min(length(vec2f(cx, cy)) * 2.0, 1.0);\n let angle = atan2(cy, cx);\n let chroma = radius * 0.25;\n let lightness = 0.80 - radius * 0.30;\n let a_val = chroma * cos(angle);\n let b_val = chroma * sin(angle);\n let linear = oklab_to_linear(lightness, a_val, b_val);\n // Linear to sRGB gamma (approximate)\n return pow(clamp(linear, vec3f(0.0), vec3f(1.0)), vec3f(1.0 / 2.2));\n}\n\n// Dispatch 2D colormap by index\nfn colormap2d(map_index: u32, u: f32, v: f32) -> vec3f {\n if map_index == 6u {\n return colormap2d_oklab_polar(u, v);\n }\n return colormap2d_lut(u, v);\n}\n\n@vertex\nfn vs_main(\n @builtin(vertex_index) vi: u32,\n @builtin(instance_index) ii: u32,\n) -> VertOut {\n // Vertex-shader decimation: skip expensive projection for excess points.\n // Early exit before the ND projection loop saves ~50 cycles per rejected point.\n if (uni.max_points > 0u && uni.num_points > uni.max_points) {\n let threshold = f32(uni.max_points) / f32(uni.num_points);\n if (random_float(ii) > threshold) {\n var out: VertOut;\n out.clip_pos = vec4f(0.0, 0.0, 0.0, 0.0);\n out.uv = vec2f(0.0);\n out.point_color = vec4f(0.0);\n out.selected = 0u;\n out.effective_opacity = 0.0;\n return out;\n }\n }\n\n // Inline ND → 2D projection with pre-adjusted basis.\n // CPU folds normalization into the basis weights and biases so the\n // hot loop is just: x += raw * adjBasisX[d], y += raw * adjBasisY[d]\n // Must produce the same result as compute-projection.wgsl (used for lasso).\n let N = uni.num_points;\n let p = uni.num_dims;\n var x = uni.bias.x;\n var y = uni.bias.y;\n for (var d = 0u; d < p; d++) {\n let raw = data[d * N + ii];\n x += raw * adj_basis[d];\n y += raw * adj_basis[p + d];\n }\n\n // 3D camera rotation: project z-axis, rotate, then discard z (orthographic).\n if (camera.use_3d > 0.5) {\n var z = uni.bias_z;\n for (var d = 0u; d < p; d++) {\n z += data[d * N + ii] * adj_basis[2u * p + d];\n }\n let pos3 = vec3f(x, y, z);\n let rot = mat3x3f(camera.rot_col0, camera.rot_col1, camera.rot_col2);\n let rotated = rot * pos3;\n x = rotated.x;\n y = rotated.y;\n // z discarded — orthographic projection\n }\n\n // Apply camera: translate then scale, with aspect correction on x.\n // inset_zoom shrinks the scene to fit below the toolbar;\n // inset_offset_y shifts it down so it's centered in the visible area.\n let iz = camera.inset_zoom;\n let center = vec2f(\n (x + camera.pan_x) * camera.zoom * iz / camera.aspect,\n (y + camera.pan_y) * camera.zoom * iz + camera.inset_offset_y,\n );\n\n let q = quad_vertex(vi);\n // Clamp point size to [2, 32] pixels (physical).\n // min_ndc ensures points are always at least 2px — 1px quads can miss pixel\n // centers entirely depending on sub-pixel alignment, producing no fragments.\n let min_ndc = 4.0 / camera.viewport_height;\n let max_ndc = 64.0 / camera.viewport_height;\n let point_size = clamp(uni.point_size, min_ndc, max_ndc);\n // Aspect-correct the offset so circles stay circular on non-square canvases\n let offset = vec2f(q.x / camera.aspect, q.y) * (point_size * 0.5);\n\n // Scale opacity by zoom² to keep constant visual fill density (Reusser).\n // Only applied for auto-computed opacity; manual values are used as-is.\n let z = camera.zoom * camera.inset_zoom;\n let zoom_scale = select(1.0, z * z, uni.scale_opacity_by_zoom > 0.5);\n let eff_opacity = uni.opacity * zoom_scale;\n\n // Resolve per-point color from LUT\n var col: vec4f;\n if uni.color_mode == 1u {\n // Continuous: read raw value from color column, normalize, interpolate LUT\n let raw = data[uni.color_column_offset + ii];\n let inv_range = select(0.0, 1.0 / uni.color_range, uni.color_range > 0.0);\n let t = clamp((raw - uni.color_min) * inv_range, 0.0, 1.0);\n let stops = f32(uni.color_num_stops - 1u);\n let pos = t * stops;\n let idx = min(u32(floor(pos)), uni.color_num_stops - 2u);\n let frac = pos - f32(idx);\n let c0 = unpackColor(colorLut[idx]);\n let c1 = unpackColor(colorLut[idx + 1u]);\n col = mix(c0, c1, frac);\n } else if uni.color_mode == 2u {\n // Categorical: read category index, direct palette lookup\n let catIdx = catIndices[ii];\n col = unpackColor(colorLut[catIdx % uni.color_num_stops]);\n } else if uni.color_mode == 3u {\n // 2D colormap: read two columns, normalize to UV, look up from 2D colormap\n let raw_u = data[uni.color_column_offset + ii];\n let raw_v = data[uni.color_column_offset_v + ii];\n let inv_range_u = select(0.0, 1.0 / uni.color_range, uni.color_range > 0.0);\n let inv_range_v = select(0.0, 1.0 / uni.color_range_v, uni.color_range_v > 0.0);\n let u_val = clamp((raw_u - uni.color_min) * inv_range_u, 0.0, 1.0);\n let v_val = clamp((raw_v - uni.color_min_v) * inv_range_v, 0.0, 1.0);\n let rgb = colormap2d(uni.color2d_map_index, u_val, v_val);\n col = vec4f(rgb, 1.0);\n } else {\n col = uni.color;\n }\n\n // Read selection\n var sel: u32 = 1u;\n if uni.useSelectionMask > 0.5 {\n sel = (selectionMask[ii / 32u] >> (ii % 32u)) & 1u;\n }\n\n var out: VertOut;\n out.clip_pos = vec4f(center + offset, 0.0, 1.0);\n out.uv = q;\n out.point_color = col;\n out.selected = sel;\n out.effective_opacity = eff_opacity;\n return out;\n}\n\n// Shared fragment helper — computes edge, selection factor, and intensity.\nfn fragment_intensity(\n uv: vec2f,\n point_alpha: f32,\n selected: u32,\n effective_opacity: f32,\n) -> f32 {\n let dist = length(uv);\n // Smooth anti-aliased edge — naturally 0 for dist >= 1.0, so no discard needed.\n // Avoiding discard preserves early-Z and SIMD wavefront occupancy.\n let edge = 1.0 - smoothstep(0.75, 1.0, dist);\n\n // Selection: boost selected, dim unselected\n var sel_factor = 1.0;\n if uni.useSelectionMask > 0.5 {\n if selected == 0u {\n sel_factor = 0.1;\n } else {\n sel_factor = 1.0 / max(effective_opacity, 0.01);\n }\n }\n\n return edge * effective_opacity * point_alpha * sel_factor;\n}\n\n// ─── Fragment: single-target (additive / subtractive) ──────────────────────\n\n@fragment\nfn fs_main(\n @location(0) uv: vec2f,\n @location(1) point_color: vec4f,\n @location(2) @interpolate(flat) selected: u32,\n @location(3) @interpolate(flat) effective_opacity: f32,\n) -> @location(0) vec4f {\n let intensity = fragment_intensity(uv, point_color.a, selected, effective_opacity);\n // Subtractive mode (Reusser): output complement color so that\n // reverse-subtract blend (dst - src) on a white bg yields the correct hue.\n let rgb = select(point_color.rgb, vec3f(1.0) - point_color.rgb, uni.useSubtractive > 0.5);\n return vec4f(rgb * intensity, intensity);\n}\n`,C=`// Fullscreen tone-map pass — reads an HDR float texture and maps to [0,1]\n// for the canvas. Uses a single oversized triangle (3 vertices, no vertex\n// buffer) to cover the viewport.\n//\n// Mode 0 (additive): per-channel exponential compression 1-exp(-x).\n// Preserves hue ratios, dense regions desaturate toward white.\n// Mode 1 (normal / subtractive): simple clamp to [0,1].\n// Values are already bounded; tone mapping is identity.\n\nstruct Params {\n mode: f32,\n}\n\n@group(0) @binding(0) var hdrTexture: texture_2d<f32>;\n@group(0) @binding(1) var<uniform> params: Params;\n\n@vertex\nfn vs(@builtin(vertex_index) vi: u32) -> @builtin(position) vec4f {\n // Oversized triangle covering the full clip space:\n // vi=0 → (-1, -1) vi=1 → (3, -1) vi=2 → (-1, 3)\n let x = f32(i32(vi) / 2) * 4.0 - 1.0;\n let y = f32(i32(vi) % 2) * 4.0 - 1.0;\n return vec4f(x, y, 0.0, 1.0);\n}\n\n@fragment\nfn fs(@builtin(position) pos: vec4f) -> @location(0) vec4f {\n let hdr = textureLoad(hdrTexture, vec2u(pos.xy), 0);\n\n var mapped: vec3f;\n if params.mode < 0.5 {\n // Additive: exponential compression preserving density structure\n mapped = 1.0 - exp(-hdr.rgb);\n } else {\n // Normal / Subtractive: values already in [0,1], just clamp\n mapped = clamp(hdr.rgb, vec3f(0.0), vec3f(1.0));\n }\n return vec4f(mapped, 1.0);\n}\n`;const w={pointSize:.012,opacity:.7,color:[.25,.5,.9],scaleOpacityByZoom:!0},T={useSelectionMask:!1},E={mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},te={panX:0,panY:0,zoom:1,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1,use3d:!1,rotation:null},D=new ArrayBuffer(96),O=new Float32Array(D),k=new Uint32Array(D),A=new Float32Array(80/4),j=new Float32Array(4),ne=(e,t=`rgba32float`)=>{let n=e.createShaderModule({label:`point-shader`,code:S}),r=e.createBindGroupLayout({label:`point-bgl`,entries:[{binding:0,visibility:GPUShaderStage.VERTEX|GPUShaderStage.FRAGMENT,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.VERTEX,buffer:{type:`uniform`}},{binding:3,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:4,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:5,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}},{binding:6,visibility:GPUShaderStage.VERTEX,buffer:{type:`read-only-storage`}}]}),i=e.createPipelineLayout({bindGroupLayouts:[r]}),a=e.createRenderPipeline({label:`point-pipeline-additive`,layout:i,vertex:{module:n,entryPoint:`vs_main`},fragment:{module:n,entryPoint:`fs_main`,targets:[{format:t,blend:{color:{srcFactor:`one`,dstFactor:`one`},alpha:{srcFactor:`zero`,dstFactor:`one`}}}]},primitive:{topology:`triangle-strip`}}),o=e.createRenderPipeline({label:`point-pipeline-normal`,layout:i,vertex:{module:n,entryPoint:`vs_main`},fragment:{module:n,entryPoint:`fs_main`,targets:[{format:t,blend:{color:{srcFactor:`one`,dstFactor:`one-minus-src-alpha`},alpha:{srcFactor:`one`,dstFactor:`one-minus-src-alpha`}}}]},primitive:{topology:`triangle-strip`}}),s=e.createRenderPipeline({label:`point-pipeline-subtractive`,layout:i,vertex:{module:n,entryPoint:`vs_main`},fragment:{module:n,entryPoint:`fs_main`,targets:[{format:t,blend:{color:{operation:`reverse-subtract`,srcFactor:`one`,dstFactor:`one`},alpha:{srcFactor:`zero`,dstFactor:`one`}}}]},primitive:{topology:`triangle-strip`}}),c=e.createBuffer({label:`point-uniforms`,size:96,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),l=e.createBuffer({label:`camera-uniforms`,size:80,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST}),u=e.createBuffer({label:`default-color-lut-buf`,size:4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),d=e.createBuffer({label:`default-selection-buf`,size:4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),f=e.createBuffer({label:`default-cat-indices-buf`,size:4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});return M(e,c,w,T,E),re(e,l,te),{additivePipeline:a,normalPipeline:o,subtractivePipeline:s,bindGroupLayout:r,uniformBuffer:c,cameraBuffer:l,defaultColorLutBuffer:u,defaultSelectionBuffer:d,defaultCatIndicesBuffer:f}},M=(e,t,n,r=T,i=E,a=!1,o=0,s=0,c=0,l=0,u=0,d=0)=>{O[0]=n.pointSize,O[1]=n.opacity,k[2]=i.mode,O[3]=r.useSelectionMask?1:0,O[4]=n.color[0],O[5]=n.color[1],O[6]=n.color[2],O[7]=1,O[8]=a?1:0,k[9]=o,k[10]=s,k[11]=u,O[12]=c,O[13]=l,k[14]=i.columnOffset,O[15]=i.min,O[16]=i.range,k[17]=i.numStops,O[18]=d,k[19]=i.columnOffsetV,O[20]=i.minV,O[21]=i.rangeV,k[22]=i.mapIndex,O[23]=n.scaleOpacityByZoom?1:0,e.queue.writeBuffer(t,0,D)},re=(e,t,n)=>{A[0]=n.panX,A[1]=n.panY,A[2]=n.zoom,A[3]=n.aspect,A[4]=n.viewportHeight,A[5]=n.insetOffsetY,A[6]=n.insetZoom,A[7]=n.use3d?1:0;let r=n.rotation;A[8]=r?r[0]:1,A[9]=r?r[1]:0,A[10]=r?r[2]:0,A[11]=0,A[12]=r?r[3]:0,A[13]=r?r[4]:1,A[14]=r?r[5]:0,A[15]=0,A[16]=r?r[6]:0,A[17]=r?r[7]:0,A[18]=r?r[8]:1,A[19]=0,e.queue.writeBuffer(t,0,A)},ie=(e,t,n,r,i,a,o,s,c)=>e.createBindGroup({label:`point-bg`,layout:t,entries:[{binding:0,resource:{buffer:n}},{binding:1,resource:{buffer:r}},{binding:2,resource:{buffer:i}},{binding:3,resource:{buffer:a}},{binding:4,resource:{buffer:o}},{binding:5,resource:{buffer:s}},{binding:6,resource:{buffer:c}}]}),ae=(e,t,n)=>{j[0]=n,e.queue.writeBuffer(t,0,j,0,1)},oe=(e,t)=>{let n=e.createShaderModule({label:`tonemap-shader`,code:C}),r=e.createBindGroupLayout({label:`tonemap-bgl`,entries:[{binding:0,visibility:GPUShaderStage.FRAGMENT,texture:{sampleType:`unfilterable-float`}},{binding:1,visibility:GPUShaderStage.FRAGMENT,buffer:{type:`uniform`}}]});return{pipeline:e.createRenderPipeline({label:`tonemap-pipeline`,layout:e.createPipelineLayout({bindGroupLayouts:[r]}),vertex:{module:n,entryPoint:`vs`},fragment:{module:n,entryPoint:`fs`,targets:[{format:t}]},primitive:{topology:`triangle-list`}}),bindGroupLayout:r,paramsBuffer:e.createBuffer({label:`tonemap-params`,size:4,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},se=(e,t,n,r)=>e.createBindGroup({label:`tonemap-bg`,layout:t,entries:[{binding:0,resource:n},{binding:1,resource:{buffer:r}}]});var ce=`// ND → 2D projection compute shader (used only for lasso selection readback)\n//\n// Reads p-dimensional point data from a single concatenated storage buffer\n// (column-major: dim0[0..N], dim1[0..N], ..., dimP[0..N]),\n// normalizes each dimension using per-dim [center, range] params,\n// and projects to 2D using the supplied p×2 basis matrix.\n//\n// Output: N interleaved [x, y] pairs in the projected buffer.\n//\n// IMPORTANT: This must stay mathematically equivalent to the render-path\n// projection in point.wgsl + computeAdjustedBasis() in worker.ts.\n// The render path folds normalization into the basis on CPU (adj_basis, bias)\n// while this shader normalizes per-point on GPU. Both produce identical\n// projected coordinates: ((raw - center) / range) * basis * viewport_scale.\n\nstruct Params {\n num_points: u32,\n num_dims: u32,\n viewport_scale: f32,\n _pad: f32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n// norm_params[d] = vec2f(center, range) for dimension d\n@group(0) @binding(2) var<storage, read> norm_params: array<vec2f>;\n// basis: column-major p×2 — first p floats = x-weights, next p = y-weights\n@group(0) @binding(3) var<storage, read> basis: array<f32>;\n@group(0) @binding(4) var<storage, read_write> projected: array<f32>;\n\n@compute @workgroup_size(256)\nfn project(@builtin(global_invocation_id) gid: vec3u) {\n let i = gid.x;\n if (i >= params.num_points) { return; }\n\n let N = params.num_points;\n let p = params.num_dims;\n\n var x = 0.0;\n var y = 0.0;\n\n for (var d = 0u; d < p; d++) {\n let raw = data[d * N + i];\n let np = norm_params[d];\n let range = max(np.y, 1e-6);\n let v = (raw - np.x) / range;\n\n x += v * basis[d];\n y += v * basis[p + d];\n }\n\n projected[i * 2u] = x * params.viewport_scale;\n projected[i * 2u + 1u] = y * params.viewport_scale;\n}\n`;const le=e=>{let t=e.createShaderModule({label:`compute-projection`,code:ce}),n=e.createBindGroupLayout({label:`projection-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}}]});return{pipeline:e.createComputePipeline({label:`projection-pipeline`,layout:e.createPipelineLayout({bindGroupLayouts:[n]}),compute:{module:t,entryPoint:`project`}}),bindGroupLayout:n,paramsBuffer:e.createBuffer({label:`projection-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},ue=(e,t,n,r,i)=>{let a=new ArrayBuffer(16),o=new Uint32Array(a),s=new Float32Array(a);o[0]=n,o[1]=r,s[2]=i,s[3]=0,e.queue.writeBuffer(t,0,a)},de=(e,t,n)=>({dataBuffer:e.createBuffer({label:`nd-data`,size:t*n*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC}),normParamsBuffer:e.createBuffer({label:`norm-params`,size:n*8,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),basisBuffer:e.createBuffer({label:`basis`,size:n*2*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),projectedBuffer:e.createBuffer({label:`projected`,size:t*2*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC}),bindGroup:null}),fe=(e,t,n,r)=>e.createBindGroup({label:`projection-bg`,layout:t,entries:[{binding:0,resource:{buffer:n}},{binding:1,resource:{buffer:r.dataBuffer}},{binding:2,resource:{buffer:r.normParamsBuffer}},{binding:3,resource:{buffer:r.basisBuffer}},{binding:4,resource:{buffer:r.projectedBuffer}}]}),pe=(e,t,n,r)=>{let i=e.createCommandEncoder({label:`projection-dispatch`}),a=i.beginComputePass({label:`project-points`});return a.setPipeline(t.pipeline),a.setBindGroup(0,n),a.dispatchWorkgroups(Math.ceil(r/256)),a.end(),i.finish()};var me=`// Residual-PC compute shader — two-pass reduction.\n//\n// Computes the covariance matrix of the residual after projecting out\n// the current p×2 tour basis. The top eigenvector of this covariance\n// is the \"most informative direction not currently visible.\"\n//\n// Pass 1 (reduce_partial): each workgroup reduces its chunk of N points\n// into partial sums for p means + p*(p+1)/2 upper-triangle cross-products\n// of the per-point residuals.\n//\n// Pass 2 (final_reduce): one workgroup sums all partial sums → final\n// mean-corrected covariance. Result layout:\n// [mean_0 .. mean_{p-1}, cov_00, cov_01, ..., cov_{p-1,p-1}]\n// where the covariance entries are the upper triangle packed as:\n// index(d1,d2) = d1*p - d1*(d1-1)/2 + (d2-d1) (d2 >= d1)\n//\n// Power iteration (15 steps) is performed on the CPU after readback;\n// p is always small (≤64) so this is negligible.\n\nstruct Params {\n num_points: u32,\n num_dims: u32,\n num_workgroups: u32,\n _pad: u32,\n}\n\n@group(0) @binding(0) var<uniform> params: Params;\n@group(0) @binding(1) var<storage, read> data: array<f32>;\n@group(0) @binding(2) var<storage, read> norm_params: array<vec2f>;\n// basis: column-major p×2 — first p floats = x-weights, next p = y-weights\n@group(0) @binding(3) var<storage, read> basis: array<f32>;\n@group(0) @binding(4) var<storage, read_write> partials: array<f32>;\n@group(0) @binding(5) var<storage, read_write> result: array<f32>;\n\nconst WG: u32 = 256u;\nconst MAX_DIMS: u32 = 64u;\n\nvar<workgroup> wg_reduce: array<f32, 256>;\n\nfn cov_idx(d: u32, d1: u32, d2: u32) -> u32 {\n return d1 * d - d1 * (d1 - 1u) / 2u + (d2 - d1);\n}\n\n// ─── Pass 1: per-workgroup partial sums ─────────────────────────────────────\n\n@compute @workgroup_size(256)\nfn reduce_partial(\n @builtin(global_invocation_id) gid: vec3u,\n @builtin(local_invocation_id) lid: vec3u,\n @builtin(workgroup_id) wid: vec3u,\n) {\n let N = params.num_points;\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let i = gid.x;\n let valid = i < N;\n\n // Load normalized values into registers, then project out the 2D basis.\n var vals: array<f32, 64>;\n if (valid) {\n var dot_x = 0.0;\n var dot_y = 0.0;\n for (var dim = 0u; dim < d; dim++) {\n let raw = data[dim * N + i];\n let np = norm_params[dim];\n let range = max(np.y, 1e-6);\n let norm = (raw - np.x) / range;\n vals[dim] = norm;\n dot_x += norm * basis[dim];\n dot_y += norm * basis[d + dim];\n }\n // Subtract projection onto span{bx, by} → residual is orthogonal to basis\n for (var dim = 0u; dim < d; dim++) {\n vals[dim] -= dot_x * basis[dim] + dot_y * basis[d + dim];\n }\n }\n\n // Reduce mean terms (used for mean-correction in pass 2)\n for (var dim = 0u; dim < d; dim++) {\n wg_reduce[lid.x] = select(0.0, vals[dim], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) { wg_reduce[lid.x] += wg_reduce[lid.x + s]; }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n partials[wid.x * num_terms + dim] = wg_reduce[0];\n }\n }\n\n // Reduce upper-triangle cross-products\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n wg_reduce[lid.x] = select(0.0, vals[d1] * vals[d2], valid);\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) { wg_reduce[lid.x] += wg_reduce[lid.x + s]; }\n workgroupBarrier();\n }\n if (lid.x == 0u) {\n partials[wid.x * num_terms + d + cov_idx(d, d1, d2)] = wg_reduce[0];\n }\n }\n }\n}\n\n// ─── Pass 2: reduce across workgroups → final mean-corrected covariance ─────\n\n@compute @workgroup_size(256)\nfn final_reduce(@builtin(local_invocation_id) lid: vec3u) {\n let d = min(params.num_dims, MAX_DIMS);\n let num_cov = d * (d + 1u) / 2u;\n let num_terms = d + num_cov;\n let nwg = params.num_workgroups;\n let N_f32 = f32(params.num_points);\n\n for (var t = 0u; t < num_terms; t++) {\n var sum = 0.0;\n var w = lid.x;\n while (w < nwg) {\n sum += partials[w * num_terms + t];\n w += WG;\n }\n wg_reduce[lid.x] = sum;\n workgroupBarrier();\n for (var s = WG / 2u; s > 0u; s >>= 1u) {\n if (lid.x < s) { wg_reduce[lid.x] += wg_reduce[lid.x + s]; }\n workgroupBarrier();\n }\n if (lid.x == 0u) { result[t] = wg_reduce[0] / N_f32; }\n workgroupBarrier();\n }\n\n // Mean-corrected covariance: cov(X,Y) = E[XY] - E[X]*E[Y]\n if (lid.x == 0u) {\n for (var d1 = 0u; d1 < d; d1++) {\n for (var d2 = d1; d2 < d; d2++) {\n let ci = cov_idx(d, d1, d2);\n result[d + ci] -= result[d1] * result[d2];\n }\n }\n }\n}\n`;const he=e=>{let t=e.createShaderModule({label:`compute-residual-pc`,code:me}),n=e.createBindGroupLayout({label:`residual-pc-bgl`,entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:`uniform`}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:3,visibility:GPUShaderStage.COMPUTE,buffer:{type:`read-only-storage`}},{binding:4,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}},{binding:5,visibility:GPUShaderStage.COMPUTE,buffer:{type:`storage`}}]}),r=e.createPipelineLayout({bindGroupLayouts:[n]});return{partialPipeline:e.createComputePipeline({label:`residual-pc-partial`,layout:r,compute:{module:t,entryPoint:`reduce_partial`}}),finalPipeline:e.createComputePipeline({label:`residual-pc-final`,layout:r,compute:{module:t,entryPoint:`final_reduce`}}),bindGroupLayout:n,paramsBuffer:e.createBuffer({label:`residual-pc-params`,size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST})}},ge=async(e,t,n,r,i,a,o,s)=>{let c=Math.min(s,64),l=c+c*(c+1)/2,u=Math.ceil(o/256),d=new ArrayBuffer(16),f=new Uint32Array(d);f[0]=o,f[1]=c,f[2]=u,f[3]=0,e.queue.writeBuffer(t.paramsBuffer,0,d);let p=e.createBuffer({label:`residual-pc-partials`,size:u*l*4,usage:GPUBufferUsage.STORAGE}),m=e.createBuffer({label:`residual-pc-result`,size:l*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC}),h=e.createBuffer({label:`residual-pc-readback`,size:l*4,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),g=e.createBindGroup({label:`residual-pc-bg`,layout:t.bindGroupLayout,entries:[{binding:0,resource:{buffer:t.paramsBuffer}},{binding:1,resource:{buffer:n}},{binding:2,resource:{buffer:r}},{binding:3,resource:{buffer:i}},{binding:4,resource:{buffer:p}},{binding:5,resource:{buffer:m}}]}),_=e.createCommandEncoder({label:`residual-pc`}),v=_.beginComputePass({label:`residual-pc-partial`});v.setPipeline(t.partialPipeline),v.setBindGroup(0,g),v.dispatchWorkgroups(u),v.end();let y=_.beginComputePass({label:`residual-pc-final`});y.setPipeline(t.finalPipeline),y.setBindGroup(0,g),y.dispatchWorkgroups(1),y.end(),_.copyBufferToBuffer(m,0,h,0,l*4),e.queue.submit([_.finish()]),await h.mapAsync(GPUMapMode.READ);let b=new Float32Array(h.getMappedRange()).slice();return h.unmap(),p.destroy(),m.destroy(),h.destroy(),_e(b,c,s,a)},_e=(e,t,n,r)=>{let i=new Float64Array(t*t);for(let n=0;n<t;n++)for(let r=n;r<t;r++){let a=e[t+(n*t-n*(n-1)/2+(r-n))];i[n*t+r]=a,i[r*t+n]=a}let a=new Float64Array(t),o=new Float64Array(t);for(let e=0;e<t;e++)a[e]=r[e],o[e]=r[n+e];let s=new Float64Array(t);for(let e=0;e<t;e++)s[e]=1;N(s,a,o,t),P(s,t);let c=new Float64Array(t);for(let e=0;e<15;e++){for(let e=0;e<t;e++){let n=0;for(let r=0;r<t;r++)n+=i[e*t+r]*s[r];c[e]=n}N(c,a,o,t),P(c,t);let e=s;s=c,c=e}let l=new Float32Array(n);for(let e=0;e<t;e++)l[e]=s[e];return l},N=(e,t,n,r)=>{let i=0,a=0;for(let o=0;o<r;o++)i+=e[o]*t[o],a+=e[o]*n[o];for(let o=0;o<r;o++)e[o]=e[o]-(i*t[o]+a*n[o])},P=(e,t)=>{let n=0;for(let r=0;r<t;r++)n+=e[r]*e[r];if(n=Math.sqrt(n),n>1e-12)for(let r=0;r<t;r++)e[r]=e[r]/n};let F=null,I=[],L=null;const R=(e,t)=>{self.postMessage(e,t??[])},ve=(e,t,n,r,i,a)=>{if(e===0||t===0||n===0)return{pointSize:.012,opacity:.7};let o=n*r,s=o*o*i/e,c=Math.sqrt(s/Math.PI),l=a*r*.5,u,d;return c>=l?(u=c,d=1):(u=l,d=Math.max(.01,s/(Math.PI*l*l))),{pointSize:2*u/o,opacity:d}},ye=(e,t)=>e*F.dpr/t,be=e=>{let{style:t,numPoints:n,dpr:r}=F;if(t.pointSize!==`auto`&&t.opacity!==`auto`)return{pointSize:ye(t.pointSize,e.height),opacity:t.opacity,color:t.color,scaleOpacityByZoom:!1};let i=ve(n,e.width/r,e.height/r,r,t.fillTarget,t.minPointSize),a=t.opacity===`auto`;return{pointSize:t.pointSize===`auto`?i.pointSize:ye(t.pointSize,e.height),opacity:a?i.opacity:t.opacity,color:t.color,scaleOpacityByZoom:a}},xe=()=>{let{device:e,mainView:t,tonemapPipeline:n}=F,{width:r,height:i}=t.canvas,a=F.mainHdrTexture;if(a&&a.width===r&&a.height===i)return;a?.destroy();let o=e.createTexture({label:`hdr-main`,size:[r,i],format:F.hdrFormat,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),s=o.createView();F.mainHdrTexture=o,F.mainHdrTextureView=s,F.mainTonemapBindGroup=se(e,n.bindGroupLayout,s,n.paramsBuffer)},Se=()=>{if(!F.previewGpuCanvas)return;let{device:e,tonemapPipeline:t}=F,{width:n,height:r}=F.previewGpuCanvas,i=F.previewHdrTexture;if(i&&i.width===n&&i.height===r)return;i?.destroy();let a=e.createTexture({label:`hdr-preview`,size:[n,r],format:F.hdrFormat,usage:GPUTextureUsage.RENDER_ATTACHMENT|GPUTextureUsage.TEXTURE_BINDING}),o=a.createView();F.previewHdrTexture=a,F.previewHdrTextureView=o,F.previewTonemapBindGroup=se(e,t.bindGroupLayout,o,t.paramsBuffer)},Ce=e=>Math.ceil(e/32)*4,we=()=>{if(!F)throw Error(`state not initialized`);let e=Ce(F.numPoints);return(!F.selectionBuffer||F.selectionBuffer.size!==e)&&(F.selectionBuffer?.destroy(),F.selectionBuffer=F.device.createBuffer({label:`selection-mask`,size:e,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST})),F.selectionBuffer};let z=null;const Te=(e,t,n,r,i=2)=>{let a=r*i;(!z||z.length!==a)&&(z=new Float32Array(a));let o=0,s=0,c=0;for(let a=0;a<r;a++){let l=Math.max(n[a],1e-6),u=e[a],d=e[r+a],f=2/l;z[a]=u*f,z[r+a]=d*f;let p=-t[a]/l;if(o+=p*u*2,s+=p*d*2,i===3){let t=e[2*r+a];z[2*r+a]=t*f,c+=p*t*2}}return{weights:z,biasX:o,biasY:s,biasZ:c}},Ee=()=>{if(!F?.normMins||!F?.normRanges)return null;let e=F.numDims,t=new Float32Array(e);if(F.centering===`mean`&&F.normMeans)t.set(F.normMeans);else for(let n=0;n<e;n++)t[n]=F.normMins[n]+F.normRanges[n]*.5;return t},B=()=>{if(!F?.projectionResources||!F.normRanges)return;let e=F.numDims,t=Ee();if(!t)return;let n=new Float32Array(e*2);for(let r=0;r<e;r++)n[r*2]=t[r],n[r*2+1]=F.normRanges[r];F.device.queue.writeBuffer(F.projectionResources.normParamsBuffer,0,n)};let V=null,De=0,H=!0,U=!1;const W=()=>H?(H=!1,U=!1,J(),F.device.queue.onSubmittedWorkDone().then(()=>{H=!0,U&&F&&!V&&(U=!1,W())},()=>{H=!0,R({type:`error`,message:`WebGPU device lost during rendering`})}),!0):(U=!0,!1),Oe=e=>{if(!V||!F?.tour)return;let t=V.prevTime===null?void 0:e-V.prevTime;if(t!==void 0){let e=t/1e3*V.speed*V.direction/20,n=F.tour.position+e;n-=Math.floor(n),F.tour.position=n}V.prevTime=e,F.directBasis=null,W()&&(V.lastRenderedRafTime!==null&&V.frameTimes.push(e-V.lastRenderedRafTime),V.lastRenderedRafTime=e),e-De>=33&&(R({type:`playbackTick`,position:F.tour.position,frameMs:t}),De=e),V.rafId=requestAnimationFrame(Oe)},ke=(e,t)=>{V?(V.speed=e,V.direction=t):(H=!0,V={speed:e,direction:t,prevTime:null,rafId:0,frameTimes:[],lastRenderedRafTime:null},V.rafId=requestAnimationFrame(Oe))},Ae=()=>{if(V){cancelAnimationFrame(V.rafId),F?.tour&&R({type:`playbackTick`,position:F.tour.position});let e=Float64Array.from(V.frameTimes);R({type:`playbackStopped`,frameTimes:e},[e.buffer]),V=null}},G=(e,t,n,r,i,a)=>{if(!F?.projectionResources||!F.adjBasisBuffer||!F.normRanges)return;let{device:c,numPoints:l,numDims:u}=F,d=F.is3dActive&&F.residualPC,f=d?3:2,p;d?(p=new Float32Array(u*3),p.set(e.subarray(0,u*2)),p.set(F.residualPC,u*2)):p=e;let m=Ee();if(!m)return;let{weights:h,biasX:g,biasY:_,biasZ:v}=Te(p,m,F.normRanges,u,f),y=u*f*4;F.adjBasisBuffer.size<y&&(F.adjBasisBuffer.destroy(),F.adjBasisBuffer=c.createBuffer({label:`adj-basis`,size:y,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),Y()),c.queue.writeBuffer(F.adjBasisBuffer,0,h);let b=be(t.canvas),x=F.backgroundColor,ee=.2126*x[0]+.7152*x[1]+.0722*x[2],S=F.colorState.mode>0,C=!S&&ee>.5,w,T;S?(w=F.pointPipeline.normalPipeline,T=1):C?(w=F.pointPipeline.subtractivePipeline,T=1):(w=F.pointPipeline.additivePipeline,T=0),M(c,F.pointPipeline.uniformBuffer,b,F.styleFlags,F.colorState,C,l,u,g,_,F.maxPoints,v),ae(c,F.tonemapPipeline.paramsBuffer,T);let E=t.canvas,te=E.width/E.height||1;re(c,F.pointPipeline.cameraBuffer,{...i,aspect:te,viewportHeight:E.height});let D=c.createCommandEncoder({label:`render-frame`});o(D,n,w,a,l,x),s(D,t,F.tonemapPipeline.pipeline,r),c.queue.submit([D.finish()])},je={panX:0,panY:0,zoom:1,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1,use3d:!1,rotation:null},Me=()=>{!F||F.previewGpuCanvas||(F.previewGpuCanvas=new OffscreenCanvas(300,300),F.previewGpuView=i(F.previewGpuCanvas,F.device))};let K=!1;const Ne=(e,t)=>{if(!F?.projectionResources||!F.renderBindGroup)return;let n=F.previewCanvases.get(e);n&&(Me(),Se(),!(!F.previewGpuView||!F.previewHdrTextureView||!F.previewTonemapBindGroup)&&((F.previewGpuCanvas.width!==n.canvas.width||F.previewGpuCanvas.height!==n.canvas.height)&&(F.previewGpuCanvas.width=n.canvas.width,F.previewGpuCanvas.height=n.canvas.height,Se(),!F.previewHdrTextureView||!F.previewTonemapBindGroup)||(G(t,F.previewGpuView,F.previewHdrTextureView,F.previewTonemapBindGroup,je,F.renderBindGroup),n.ctx2d.drawImage(F.previewGpuCanvas,0,0),R({type:`rendered`,viewIndex:e}))))},Pe=()=>{if(!F?.projectionResources||!F.tour)return;let{tour:e,previewCanvases:t}=F;for(let[n]of t)n<e.bases.length&&Ne(n,e.bases[n])},Fe=()=>{K||(K=!0,setTimeout(()=>{K=!1,Pe()},0))},q=()=>{F?.projectionResources&&(J(),Fe())},J=()=>{if(!F?.projectionResources||!F.renderBindGroup||(xe(),!F.mainHdrTextureView||!F.mainTonemapBindGroup))return;if(F.directBasis){G(F.directBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),R({type:`rendered`,viewIndex:0});return}if(!F.tour)return;let{tour:e}=F;h(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),G(e.interpolatedBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),R({type:`rendered`,viewIndex:0})},Y=()=>{if(!F?.projectionResources||!F.adjBasisBuffer)return;let{device:e,projectionPipeline:t,pointPipeline:n,projectionResources:r}=F,i=F.colorLutBuffer??n.defaultColorLutBuffer,a=F.selectionBuffer??n.defaultSelectionBuffer,o=n.defaultCatIndicesBuffer;F.colorState.mode===2&&F.activeCatColumnName&&(o=F.categoricalBuffers.get(F.activeCatColumnName)??o),F.projectionBindGroup=fe(e,t.bindGroupLayout,t.paramsBuffer,r),F.renderBindGroup=ie(e,n.bindGroupLayout,n.uniformBuffer,r.dataBuffer,n.cameraBuffer,i,a,F.adjBasisBuffer,o)},X=()=>{F&&(Y(),(F.tour||F.directBasis)&&F.projectionResources&&q())},Ie=()=>{F&&(F.styleFlags.useSelectionMask=!0,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q())};let Z=0;const Le=e=>{if(!F)return;let{device:t}=F;if(e.data.type===`setColorContinuous`){if(e.data.dataVersion!==Z)return;let{columnIndex:n,min:r,range:i,colormap:a}=e.data;F.colorLutBuffer?.destroy(),F.colorLutBuffer=t.createBuffer({label:`color-lut`,size:a.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.colorLutBuffer,0,a),F.colorState={mode:1,columnOffset:n*F.numPoints,min:r,range:i,numStops:a.length,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=null,X();return}if(e.data.type===`setColorCategorical`){if(e.data.dataVersion!==Z)return;let{catColumnName:n,palette:r}=e.data;if(!F.categoricalBuffers.has(n))return;F.colorLutBuffer?.destroy(),F.colorLutBuffer=t.createBuffer({label:`color-lut`,size:r.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.colorLutBuffer,0,r),F.colorState={mode:2,columnOffset:0,min:0,range:1,numStops:r.length,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=n,X();return}if(e.data.type===`setColor2D`){if(e.data.dataVersion!==Z)return;let{columnIndexX:n,columnIndexY:r,minX:i,rangeX:a,minY:o,rangeY:s,lut:c,mapIndex:l}=e.data;c&&(F.colorLutBuffer?.destroy(),F.colorLutBuffer=t.createBuffer({label:`color-lut-2d`,size:c.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.colorLutBuffer,0,c)),F.colorState={mode:3,columnOffset:n*F.numPoints,min:i,range:a,numStops:0,columnOffsetV:r*F.numPoints,minV:o,rangeV:s,mapIndex:l},F.activeCatColumnName=null,X();return}if(e.data.type===`selectContinuous`){if(e.data.dataVersion!==Z||!F.projectionResources)return;let{columnIndex:n,ranges:r}=e.data,{numPoints:i,colorPipelines:a}=F,o=we(),s=t.createBuffer({label:`select-ranges`,size:Math.max(r.byteLength,4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});t.queue.writeBuffer(s,0,r),b(t,a.paramsBuffer,i,n*i,r.length/2);let c=y(t,a.bindGroupLayout,F.projectionResources.dataBuffer,s,o,a.paramsBuffer),l=t.createCommandEncoder({label:`select-continuous`});l.clearBuffer(o,0,o.size);let u=l.beginComputePass({label:`select-continuous`});u.setPipeline(a.selectContinuous),u.setBindGroup(0,c),u.dispatchWorkgroups(Math.ceil(i/256)),u.end(),t.queue.submit([l.finish()]),s.destroy(),Ie();return}if(e.data.type===`selectCategorical`){if(e.data.dataVersion!==Z)return;let{catColumnName:n,selectedLabels:r}=e.data,i=F.categoricalBuffers.get(n);if(!i)return;let{numPoints:a,colorPipelines:o}=F,s=we(),c=t.createBuffer({label:`selected-labels`,size:Math.max(r.byteLength,4),usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST});t.queue.writeBuffer(c,0,r),x(t,o.paramsBuffer,a,r.length);let l=y(t,o.bindGroupLayout,i,c,s,o.paramsBuffer),u=t.createCommandEncoder({label:`select-categorical`});u.clearBuffer(s,0,s.size);let d=u.beginComputePass({label:`select-categorical`});d.setPipeline(o.selectCategorical),d.setBindGroup(0,l),d.dispatchWorkgroups(Math.ceil(a/256)),d.end(),t.queue.submit([u.finish()]),c.destroy(),Ie();return}if(e.data.type!==`data`)return;let{dataVersion:n,dims:r,rows:i,buffers:a,mins:o,ranges:s,means:c,categoricalColumns:l}=e.data;if(Z=n,r<2||i===0||a.length<2){R({type:`error`,message:`Need at least 2 dimensions to render`});return}F.projectionResources&&(F.projectionResources.dataBuffer.destroy(),F.projectionResources.normParamsBuffer.destroy(),F.projectionResources.basisBuffer.destroy(),F.projectionResources.projectedBuffer.destroy()),F.adjBasisBuffer&&(F.adjBasisBuffer.destroy(),F.adjBasisBuffer=null),F.colorLutBuffer&&(F.colorLutBuffer.destroy(),F.colorLutBuffer=null),F.selectionBuffer&&(F.selectionBuffer.destroy(),F.selectionBuffer=null);for(let e of F.categoricalBuffers.values())e.destroy();F.categoricalBuffers.clear(),F.styleFlags={useSelectionMask:!1},F.colorState={mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=null,F.is3dActive=!1,F.rotation3d=null,F.residualPC=null,F.camera.use3d=!1,F.camera.rotation=null;let{projectionPipeline:u}=F,d=de(t,i,r);for(let e=0;e<r;e++)t.queue.writeBuffer(d.dataBuffer,e*i*4,a[e]);for(let e of l){let n=t.createBuffer({label:`cat-indices-${e.name}`,size:e.indices.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST|GPUBufferUsage.COPY_SRC});t.queue.writeBuffer(n,0,e.indices),F.categoricalBuffers.set(e.name,n)}if(ue(t,u.paramsBuffer,i,r,2),F.adjBasisBuffer=t.createBuffer({label:`adj-basis`,size:r*2*4,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),F.normMins=new Float32Array(o),F.normRanges=new Float32Array(s),F.normMeans=new Float32Array(c),F.projectionResources={...d,bindGroup:null},F.numPoints=i,F.numDims=r,B(),Y(),F.tour&&F.tour.dims===r)q();else{let e=Math.max(1,F.previewCanvases.size),t=[];for(let n=0;n<e;n++){let i=new Float32Array(r*2),a=Math.floor(n/e*r);i[a]=1,i[r+(a+1)%r]=1,t.push(i)}F.tour={bases:t,arcLengths:p(t,r),dims:r,orthonormalize:!0,position:0,interpolatedBasis:new Float32Array(r*2)},q()}},Q=(e,t)=>{e.projectionResources&&(e.residualPcPipeline||=he(e.device),e.device.queue.writeBuffer(e.projectionResources.basisBuffer,0,t),ge(e.device,e.residualPcPipeline,e.projectionResources.dataBuffer,e.projectionResources.normParamsBuffer,e.projectionResources.basisBuffer,t,e.numPoints,e.numDims).then(e=>{F?.is3dActive&&(F.residualPC=e,R({type:`residualPC`,residualPC:new Float32Array(e)}),J())}))},$=e=>{if(F){if(e.type===`setBases`){let{bases:t}=e;if(t.length===0)return;let n=t[0].length/2,r=e.tourFamily!==`sequential`,i=p(t,n,r);F.tour={bases:t,arcLengths:i,dims:n,orthonormalize:r,position:F.tour?.position??0,interpolatedBasis:new Float32Array(n*2)},F.projectionResources&&q();return}if(e.type===`setTourPosition`){if(!F.tour)return;F.tour.position=e.position,F.directBasis=null,W();return}if(e.type===`setStyle`){F.style={pointSize:e.pointSize,opacity:e.opacity,color:e.color,minPointSize:e.minPointSize??F.style.minPointSize,fillTarget:e.fillTarget??F.style.fillTarget},(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setCamera`){F.camera={panX:e.pan[0],panY:e.pan[1],zoom:e.zoom,aspect:F.camera.aspect,viewportHeight:F.camera.viewportHeight,insetOffsetY:e.insetOffsetY,insetZoom:e.insetZoom,use3d:F.camera.use3d,rotation:F.camera.rotation},W();return}if(e.type===`setCentering`){if(F.centering=e.centering,B(),(F.tour||F.directBasis)&&F.projectionResources){if(F.is3dActive){let e=F.directBasis??F.tour?.interpolatedBasis;e&&Q(F,e)}q()}return}if(e.type===`resize`){e.dpr!==void 0&&(F.dpr=e.dpr),e.viewIndex===0&&(F.mainView.canvas.width=e.width,F.mainView.canvas.height=e.height,(F.tour||F.directBasis)&&F.projectionResources&&J());return}if(e.type===`addPreviewCanvas`){let t=e.canvas.getContext(`2d`);if(!t)return;F.previewCanvases.set(e.id,{canvas:e.canvas,ctx2d:t}),F.tour&&F.projectionResources&&e.id<F.tour.bases.length&&Ne(e.id,F.tour.bases[e.id]);return}if(e.type===`removePreviewCanvas`){F.previewCanvases.delete(e.id);return}if(e.type===`resizePreview`){let t=F.previewCanvases.get(e.id);if(!t)return;t.canvas.width=e.width,t.canvas.height=e.height,F.tour&&F.projectionResources&&Fe();return}if(e.type===`render`){(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setDirectBasis`){if(!F.projectionResources||!F.renderBindGroup)return;F.directBasis=e.basis,W(),F.is3dActive&&F.projectionResources&&Q(F,e.basis);return}if(e.type===`clearColors`){F.colorLutBuffer&&(F.colorLutBuffer.destroy(),F.colorLutBuffer=null),F.colorState={mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},F.activeCatColumnName=null,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setBackgroundColor`){F.backgroundColor=e.color,(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setMaxPoints`){F.maxPoints=e.maxPoints,(F.tour||F.directBasis)&&F.projectionResources&&q();return}if(e.type===`setSelectionMask`){let{mask:t}=e;F.selectionBuffer&&F.selectionBuffer.destroy(),F.selectionBuffer=F.device.createBuffer({label:`selection-mask`,size:t.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),F.device.queue.writeBuffer(F.selectionBuffer,0,t),F.styleFlags.useSelectionMask=!0,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q();let n=new Uint32Array(t);R({type:`selectionResult`,mask:n},[n.buffer]);return}if(e.type===`clearSelectionMask`){F.selectionBuffer&&(F.selectionBuffer.destroy(),F.selectionBuffer=null),F.styleFlags.useSelectionMask=!1,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q(),R({type:`selectionResult`,mask:new Uint32Array});return}if(e.type===`enable3d`){if(!F.projectionResources)return;let e=F.directBasis??F.tour?.interpolatedBasis;if(!e)return;if(F.is3dActive=!0,F.camera.use3d=!0,F.adjBasisBuffer){let e=F.numDims*3*4;F.adjBasisBuffer.size<e&&(F.adjBasisBuffer.destroy(),F.adjBasisBuffer=F.device.createBuffer({label:`adj-basis`,size:e,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),Y())}J(),Q(F,e);return}if(e.type===`disable3d`){F.is3dActive=!1,F.residualPC=null,F.rotation3d=null,F.camera.use3d=!1,F.camera.rotation=null,J();return}if(e.type===`set3dRotation`){F.rotation3d=e.matrix,F.camera.rotation=e.matrix,W();return}if(e.type===`startPlayback`){if(!F.tour)return;ke(e.speed,e.direction);return}if(e.type===`stopPlayback`){Ae();return}if(e.type===`computePCA`){if(!F.projectionResources||F.numDims<2){R({type:`error`,message:`No data loaded for PCA`});return}let{device:e,projectionResources:t,numPoints:i,numDims:a}=F;L||=n(e),r(e,L,t.dataBuffer,t.normParamsBuffer,i,a).then(({eigenvalues:e,eigenvectors:t})=>{R({type:`pcaResult`,eigenvectors:t,eigenvalues:e,numDims:t.length})}).catch(e=>{R({type:`error`,message:`PCA failed: ${e instanceof Error?e.message:String(e)}`})});return}if(e.type===`lassoSelect`){if(!F.projectionResources||!F.projectionBindGroup||F.numPoints===0)return;let{device:t,numPoints:n,projectionPipeline:r,projectionResources:i,camera:a}=F,{polygon:o}=e,s=o.length/2;if(s<3)return;let c=null;if(F.directBasis)c=F.directBasis;else if(F.tour){let{tour:e}=F;h(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),c=e.interpolatedBasis}if(!c)return;t.queue.writeBuffer(i.basisBuffer,0,c);let l=pe(t,r,F.projectionBindGroup,n);t.queue.submit([l]);let u=F.mainView.canvas,d=u.width/u.height||1,f=a.insetZoom,p=a.zoom*f,m=new Float32Array(o.length);for(let e=0;e<s;e++)m[e*2]=o[e*2]*d/p-a.panX,m[e*2+1]=(o[e*2+1]-a.insetOffsetY)/p-a.panY;let g=n*2*4,_=t.createBuffer({label:`proj-readback`,size:g,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),v=t.createCommandEncoder();v.copyBufferToBuffer(i.projectedBuffer,0,_,0,g),t.queue.submit([v.finish()]),_.mapAsync(GPUMapMode.READ).then(()=>{let e=new Float32Array(_.getMappedRange()),r=new Uint32Array(Math.ceil(n/32));for(let t=0;t<n;t++){let n=e[t*2],i=e[t*2+1];if(Ve(n,i,m,s)){let e=t>>5;r[e]=(r[e]??0)|1<<(t&31)}}_.unmap(),_.destroy(),F.selectionBuffer&&F.selectionBuffer.destroy(),F.selectionBuffer=t.createBuffer({label:`selection-mask`,size:r.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST}),t.queue.writeBuffer(F.selectionBuffer,0,r),F.styleFlags.useSelectionMask=!0,Y(),(F.tour||F.directBasis)&&F.projectionResources&&q();let i=new Uint32Array(r);R({type:`selectionResult`,mask:i},[i.buffer])});return}if(e.type===`getProjectedPositions`){if(!F.projectionResources||!F.projectionBindGroup||F.numPoints===0){R({type:`projectedPositions`,positions:new Float32Array},[]);return}let{device:e,numPoints:t,projectionPipeline:n,projectionResources:r}=F,i=null;if(F.directBasis)i=F.directBasis;else if(F.tour){let{tour:e}=F;h(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),i=e.interpolatedBasis}if(!i){R({type:`projectedPositions`,positions:new Float32Array},[]);return}e.queue.writeBuffer(r.basisBuffer,0,i);let a=pe(e,n,F.projectionBindGroup,t);e.queue.submit([a]);let o=t*2*4,s=e.createBuffer({label:`proj-positions-readback`,size:o,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),c=e.createCommandEncoder();c.copyBufferToBuffer(r.projectedBuffer,0,s,0,o),e.queue.submit([c.finish()]),s.mapAsync(GPUMapMode.READ).then(()=>{let e=new Float32Array(s.getMappedRange()).slice();s.unmap(),s.destroy(),R({type:`projectedPositions`,positions:e},[e.buffer])});return}if(e.type===`getPointData`){if(!F.projectionResources||F.numPoints===0){R({type:`pointData`,pointIndex:e.pointIndex,numericValues:{},categoricalValues:{}});return}let{device:t,numPoints:n,numDims:r,projectionResources:i}=F,a=e.pointIndex;if(a<0||a>=n){R({type:`pointData`,pointIndex:a,numericValues:{},categoricalValues:{}});return}let o=r*4,s=t.createBuffer({label:`point-data-readback`,size:o,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ}),c=t.createCommandEncoder();for(let e=0;e<r;e++)c.copyBufferToBuffer(i.dataBuffer,(e*n+a)*4,s,e*4,4);let l=Array.from(F.categoricalBuffers.keys()),u=l.length*4,d=null;if(u>0){d=t.createBuffer({label:`point-cat-readback`,size:u,usage:GPUBufferUsage.COPY_DST|GPUBufferUsage.MAP_READ});let e=0;for(let t of l){let n=F.categoricalBuffers.get(t);c.copyBufferToBuffer(n,a*4,d,e,4),e+=4}}t.queue.submit([c.finish()]);let f=s.mapAsync(GPUMapMode.READ).then(()=>{let e=new Float32Array(s.getMappedRange()).slice();return s.unmap(),s.destroy(),e}),p=d?d.mapAsync(GPUMapMode.READ).then(()=>{let e=new Uint32Array(d.getMappedRange()).slice();return d.unmap(),d.destroy(),e}):Promise.resolve(new Uint32Array);Promise.all([f,p]).then(([e,t])=>{let n={};for(let t=0;t<r;t++)n[String(t)]=e[t];let i={};for(let e=0;e<l.length;e++)i[l[e]]=t[e];R({type:`pointData`,pointIndex:a,numericValues:n,categoricalValues:i})});return}if(e.type===`benchmark`){Be(e.numFrames);return}if(e.type===`getMetrics`){ze();return}}},Re=()=>{if(!F)return 0;let e=0;if(F.projectionResources){let t=F.projectionResources;e+=t.dataBuffer.size+t.normParamsBuffer.size+t.basisBuffer.size+t.projectedBuffer.size}F.adjBasisBuffer&&(e+=F.adjBasisBuffer.size),F.selectionBuffer&&(e+=F.selectionBuffer.size),F.colorLutBuffer&&(e+=F.colorLutBuffer.size);for(let t of F.categoricalBuffers.values())e+=t.size;F.mainHdrTexture&&(e+=F.mainHdrTexture.width*F.mainHdrTexture.height*16),F.previewHdrTexture&&(e+=F.previewHdrTexture.width*F.previewHdrTexture.height*16);let t=F.pointPipeline;return e+=t.uniformBuffer.size+t.cameraBuffer.size,e+=t.defaultColorLutBuffer.size+t.defaultSelectionBuffer.size+t.defaultCatIndicesBuffer.size,e+=F.projectionPipeline.paramsBuffer.size,e+=F.tonemapPipeline.paramsBuffer.size,e},ze=()=>{if(!F)return;let e=0;if(F.tour){for(let t of F.tour.bases)e+=t.byteLength;e+=F.tour.interpolatedBasis.byteLength}R({type:`metricsResult`,gpuMemoryBytes:Re(),numPoints:F.numPoints,numDims:F.numDims,workerJsHeapBytes:e})},Be=async e=>{if(!F?.tour||!F.renderBindGroup||F.numPoints===0){R({type:`error`,message:`No data/tour loaded for benchmark`});return}let{device:t,numPoints:n,numDims:r,tour:i}=F,a=i.position;F.directBasis=null;for(let e=0;e<5;e++)i.position=e/5,h(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize),xe(),G(i.interpolatedBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),await t.queue.onSubmittedWorkDone();let o=new Float64Array(e);for(let n=0;n<e;n++){i.position=n/e,h(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize);let r=performance.now();G(i.interpolatedBasis,F.mainView,F.mainHdrTextureView,F.mainTonemapBindGroup,F.camera,F.renderBindGroup),await t.queue.onSubmittedWorkDone(),o[n]=performance.now()-r}i.position=a,R({type:`benchmarkResult`,frameTimes:o,numPoints:n,numDims:r},[o.buffer])},Ve=(e,t,n,r)=>{let i=!1;for(let a=0,o=r-1;a<r;o=a++){let r=n[a*2],s=n[a*2+1],c=n[o*2],l=n[o*2+1];s>t!=l>t&&e<(c-r)*(t-s)/(l-s)+r&&(i=!i)}return i};self.onmessage=async e=>{let t=e.data;if(t.type===`init`){try{let{device:e}=await ee(),n=i(t.canvas,e),r=navigator.gpu.getPreferredCanvasFormat(),a=`rgba32float`,o=ne(e,a),s=le(e),c=v(e),l=oe(e,r);F={device:e,mainView:n,previewGpuCanvas:null,previewGpuView:null,previewCanvases:new Map,pointPipeline:o,renderBindGroup:null,projectionPipeline:s,projectionResources:null,projectionBindGroup:null,colorPipelines:c,numPoints:0,numDims:0,categoricalBuffers:new Map,tour:null,style:{pointSize:`auto`,opacity:`auto`,color:[.25,.5,.9],minPointSize:2,fillTarget:.5},styleFlags:{useSelectionMask:!1},camera:{panX:0,panY:0,zoom:t.zoom,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1,use3d:!1,rotation:null},colorLutBuffer:null,colorState:{mode:0,columnOffset:0,min:0,range:1,numStops:0,columnOffsetV:0,minV:0,rangeV:1,mapIndex:0},activeCatColumnName:null,selectionBuffer:null,directBasis:null,backgroundColor:[0,0,0],dpr:t.dpr,hdrFormat:a,tonemapPipeline:l,mainHdrTexture:null,mainHdrTextureView:null,mainTonemapBindGroup:null,previewHdrTexture:null,previewHdrTextureView:null,previewTonemapBindGroup:null,maxPoints:0,adjBasisBuffer:null,normMins:null,normRanges:null,normMeans:null,centering:`midrange`,is3dActive:!1,rotation3d:null,residualPC:null,residualPcPipeline:null},t.dataPort.onmessage=Le;let u=I;if(I=null,u)for(let e of u)$(e);R({type:`ready`})}catch(e){R({type:`error`,message:e instanceof Error?e.message:String(e)})}return}if(I){I.push(t);return}$(t)};", h = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", m], { type: "text/javascript;charset=utf-8" });
2639
2639
  function g(e) {
2640
2640
  let t;
2641
2641
  try {
@@ -3023,7 +3023,7 @@ var _ = (e) => {
3023
3023
  }, A = (e, t, n, r, i, a = !0) => {
3024
3024
  let o = t.length, { segment: s, localT: c } = k(n, i), l = (s - 1 + o) % o, u = s, d = (s + 1) % o, f = (s + 2) % o;
3025
3025
  return T(e, t[l], t[u], t[d], t[f], r, c, a);
3026
- }, j = "var e=`#version 300 es\n// Point fragment shader — SDF circle anti-aliasing\n//\n// WebGL2 equivalent of point.wgsl fs_main. Uses gl_PointCoord for the\n// SDF distance (maps [0,1] in gl.POINTS, remap to [-1,1]).\n\nprecision highp float;\nprecision highp int;\n\nflat in float v_effOpacity;\nflat in vec4 v_color;\nflat in uint v_selected;\nflat in float v_useSubtractive;\nflat in float v_useSelectionMask;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main() {\n // gl_PointCoord is [0,1], remap to [-1,1] for SDF\n vec2 uv = gl_PointCoord * 2.0 - 1.0;\n float dist = length(uv);\n\n // Smooth anti-aliased edge — naturally 0 for dist >= 1.0, so no discard needed.\n // Avoiding discard preserves early-Z and SIMD wavefront occupancy.\n float edge = 1.0 - smoothstep(0.75, 1.0, dist);\n\n // Selection: boost selected, dim unselected\n float sel_factor = 1.0;\n if (v_useSelectionMask > 0.5) {\n if (v_selected == 0u) {\n sel_factor = 0.1;\n } else {\n sel_factor = 1.0 / max(v_effOpacity, 0.01);\n }\n }\n\n float intensity = edge * v_effOpacity * v_color.a * sel_factor;\n\n // Subtractive mode (Reusser): output complement color so that\n // reverse-subtract blend (dst - src) on a white bg yields the correct hue.\n vec3 rgb = v_color.rgb;\n if (v_useSubtractive > 0.5) {\n rgb = vec3(1.0) - rgb;\n }\n\n fragColor = vec4(rgb * intensity, intensity);\n}\n`,t=`#version 300 es\n// Point renderer — gl.POINTS with SDF circle anti-aliasing\n//\n// WebGL2 equivalent of point.wgsl. Projects raw ND data to 2D inline using\n// pre-adjusted basis weights and biases (normalization folded into the basis\n// on CPU). Uses gl.POINTS with gl_PointSize for native point expansion —\n// 1 vertex per point instead of 4 (instanced quads in WebGPU).\n//\n// Data is stored in a tiled R32F texture to handle point counts exceeding\n// MAX_TEXTURE_SIZE. Layout: width=texWidth, height=numDims*tileRows.\n// For point idx: col = idx % texWidth, row = d * tileRows + idx / texWidth.\n//\n// Color mapping: instead of a pre-computed N-element color texture, the shader\n// reads raw data values and looks up colors from a tiny LUT at render time.\n\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp usampler2D;\n\n// Raw ND data (column-major, tiled).\n// R32F texture: width = texWidth, height = numDims * tileRows.\nuniform sampler2D u_data;\n\n// Adjusted basis weights (normalization folded in).\n// Max 128 dims — for higher, fall back to a basis texture.\nuniform float u_adjBasisX[128];\nuniform float u_adjBasisY[128];\nuniform vec2 u_bias;\n\n// Style\nuniform float u_pointSize; // NDC units\nuniform float u_opacity;\nuniform vec4 u_color; // uniform point color (RGBA 0-1)\nuniform float u_useSubtractive;\nuniform float u_scaleOpacityByZoom; // 1.0 = auto (zoom² density scaling), 0.0 = manual\n\n// Camera\nuniform vec2 u_pan;\nuniform float u_zoom;\nuniform float u_aspect;\nuniform float u_viewportHeight;\nuniform float u_insetOffsetY;\nuniform float u_insetZoom;\n\n// Data dimensions\nuniform int u_numPoints;\nuniform int u_numDims;\n\n// Decimation: 0 = disabled\nuniform int u_maxPoints;\n\n// Tiling: texture width (min(numPoints, MAX_TEXTURE_SIZE))\nuniform int u_texWidth;\n\n// Color mapping — tiny LUT replaces N-element packed color texture\n// 0 = uniform color, 1 = continuous, 2 = categorical, 3 = 2D colormap\nuniform int u_colorMode;\nuniform int u_colorColumnIndex; // which data column to read (continuous only)\nuniform float u_colorMin;\nuniform float u_colorRange;\nuniform int u_colorNumStops; // LUT size\nuniform usampler2D u_colorLutTex; // packed RGBA u32 LUT (width=numStops, height=1)\nuniform usampler2D u_catIndexTex; // categorical indices (R32UI, tiled like data tex)\n\n// 2D colormap params (used when u_colorMode == 3)\nuniform int u_colorColumnIndexV; // second data column\nuniform float u_colorMinV;\nuniform float u_colorRangeV;\nuniform int u_color2dMapIndex; // 0-5 = LUT-based, 6 = oklab_polar\nuniform float u_color2dLut[128]; // SVD rank-1 LUT curves (8 × 16 entries)\n\n// Selection mask (bit-packed u32 in R32UI texture, width = ceil(N/32), height = 1)\nuniform float u_useSelectionMask;\nuniform usampler2D u_selectionTex;\n\nflat out float v_effOpacity;\nflat out vec4 v_color;\nflat out uint v_selected;\nflat out float v_useSubtractive;\nflat out float v_useSelectionMask;\n\n// Unpack RGBA8 from u32 (0xAABBGGRR little-endian)\nvec4 unpackColor(uint packed) {\n float r = float(packed & 0xFFu) / 255.0;\n float g = float((packed >> 8u) & 0xFFu) / 255.0;\n float b = float((packed >> 16u) & 0xFFu) / 255.0;\n float a = float((packed >> 24u) & 0xFFu) / 255.0;\n return vec4(r, g, b, a);\n}\n\n// PCG hash — fast, high-quality deterministic random from vertex index.\n// Used for vertex-shader decimation when numPoints > maxPoints.\nuint pcg_hash(uint v) {\n uint state = v * 747796405u + 2891336453u;\n uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\n\nfloat random_float(uint seed) {\n return float(pcg_hash(seed)) / 4294967295.0;\n}\n\n// ── 2D colormap helpers ──\n\n// Linearly interpolate within a 16-entry LUT curve starting at base offset.\nfloat lut2d_interp(int base, float t) {\n float pos = t * 15.0;\n int lo = min(int(floor(pos)), 14);\n int hi = lo + 1;\n return mix(u_color2dLut[base + lo], u_color2dLut[base + hi], fract(pos));\n}\n\n// Reconstruct 2D color from SVD rank-1 LUT curves.\n// Layout: R_X[16] R_Y[16] G_X[16] G_Y[16] B_X[16] B_Y[16] B_X2[16] B_Y2[16]\nvec3 colormap2d_lut(float u, float v) {\n float r = lut2d_interp(0, u) * lut2d_interp(16, v);\n float g = lut2d_interp(32, u) * lut2d_interp(48, v);\n float b = lut2d_interp(64, u) * lut2d_interp(80, v)\n + lut2d_interp(96, u) * lut2d_interp(112, v);\n return clamp(vec3(r, g, b), vec3(0.0), vec3(1.0));\n}\n\n// Oklab → linear sRGB conversion\nvec3 oklabToLinear(vec3 lab) {\n float l_ = lab.x + 0.3963377774 * lab.y + 0.2158037573 * lab.z;\n float m_ = lab.x - 0.1055613458 * lab.y - 0.0638541728 * lab.z;\n float s_ = lab.x - 0.0894841775 * lab.y - 1.2914855480 * lab.z;\n float l = l_ * l_ * l_;\n float m = m_ * m_ * m_;\n float s = s_ * s_ * s_;\n return vec3(\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s\n );\n}\n\n// Oklab polar 2D colormap: hue = angle, chroma = radius, lightness varies\nvec3 colormap2d_oklab_polar(float u, float v) {\n float cx = u - 0.5;\n float cy = v - 0.5;\n float radius = min(length(vec2(cx, cy)) * 2.0, 1.0);\n float angle = atan(cy, cx);\n float chroma = radius * 0.25;\n float L = mix(0.3, 0.85, 1.0 - radius);\n float a = chroma * cos(angle);\n float b = chroma * sin(angle);\n vec3 linear = oklabToLinear(vec3(L, a, b));\n return pow(clamp(linear, vec3(0.0), vec3(1.0)), vec3(1.0 / 2.2));\n}\n\nvec3 colormap2d(int mapIndex, float u, float v) {\n if (mapIndex == 6) {\n return colormap2d_oklab_polar(u, v);\n }\n return colormap2d_lut(u, v);\n}\n\nvoid main() {\n int idx = gl_VertexID;\n\n // Vertex-shader decimation: skip expensive projection for excess points.\n if (u_maxPoints > 0 && u_numPoints > u_maxPoints) {\n float threshold = float(u_maxPoints) / float(u_numPoints);\n if (random_float(uint(idx)) > threshold) {\n gl_Position = vec4(0.0, 0.0, 0.0, 0.0);\n gl_PointSize = 0.0;\n v_effOpacity = 0.0;\n v_color = vec4(0.0);\n v_selected = 0u;\n v_useSubtractive = 0.0;\n v_useSelectionMask = 0.0;\n return;\n }\n }\n\n // Tiled texture coordinates: col = idx % texWidth, tileRow = idx / texWidth\n int tileRows = (u_numPoints + u_texWidth - 1) / u_texWidth;\n int tx = idx % u_texWidth;\n int tileRow = idx / u_texWidth;\n\n // Inline ND -> 2D projection with pre-adjusted basis.\n // CPU folds normalization into the basis weights and biases so the\n // hot loop is just: x += raw * adjBasisX[d], y += raw * adjBasisY[d]\n float x = u_bias.x;\n float y = u_bias.y;\n for (int d = 0; d < u_numDims; d++) {\n float raw = texelFetch(u_data, ivec2(tx, d * tileRows + tileRow), 0).r;\n x += raw * u_adjBasisX[d];\n y += raw * u_adjBasisY[d];\n }\n\n // Apply camera: translate then scale, with aspect correction on x.\n float iz = u_insetZoom;\n vec2 center = vec2(\n (x + u_pan.x) * u_zoom * iz / u_aspect,\n (y + u_pan.y) * u_zoom * iz + u_insetOffsetY\n );\n\n gl_Position = vec4(center, 0.0, 1.0);\n\n // Point size: clamp to [2, 32] physical pixels (same as WGSL)\n float min_ndc = 4.0 / u_viewportHeight;\n float max_ndc = 64.0 / u_viewportHeight;\n float ps = clamp(u_pointSize, min_ndc, max_ndc);\n // gl_PointSize is in physical pixels, convert from NDC\n gl_PointSize = ps * u_viewportHeight * 0.5;\n\n // Scale opacity by zoom^2 for constant visual fill density (Reusser).\n // Only applied for auto-computed opacity; manual values are used as-is.\n float z = u_zoom * u_insetZoom;\n float zoomScale = u_scaleOpacityByZoom > 0.5 ? z * z : 1.0;\n v_effOpacity = u_opacity * zoomScale;\n\n // Resolve per-point color from LUT\n if (u_colorMode == 1) {\n // Continuous: read raw value from color column, normalize, interpolate LUT\n float raw = texelFetch(u_data, ivec2(tx, u_colorColumnIndex * tileRows + tileRow), 0).r;\n float invRange = u_colorRange > 0.0 ? 1.0 / u_colorRange : 0.0;\n float t = clamp((raw - u_colorMin) * invRange, 0.0, 1.0);\n float stops = float(u_colorNumStops - 1);\n float pos = t * stops;\n int idx0 = min(int(floor(pos)), u_colorNumStops - 2);\n float frac = pos - float(idx0);\n vec4 c0 = unpackColor(texelFetch(u_colorLutTex, ivec2(idx0, 0), 0).r);\n vec4 c1 = unpackColor(texelFetch(u_colorLutTex, ivec2(idx0 + 1, 0), 0).r);\n v_color = mix(c0, c1, frac);\n } else if (u_colorMode == 2) {\n // Categorical: read category index from tiled texture, direct palette lookup\n uint catIdx = texelFetch(u_catIndexTex, ivec2(tx, tileRow), 0).r;\n int lutIdx = int(catIdx) % u_colorNumStops;\n v_color = unpackColor(texelFetch(u_colorLutTex, ivec2(lutIdx, 0), 0).r);\n } else if (u_colorMode == 3) {\n // 2D colormap: read two columns, normalize to UV, look up from 2D colormap\n float rawU = texelFetch(u_data, ivec2(tx, u_colorColumnIndex * tileRows + tileRow), 0).r;\n float rawV = texelFetch(u_data, ivec2(tx, u_colorColumnIndexV * tileRows + tileRow), 0).r;\n float invRangeU = u_colorRange > 0.0 ? 1.0 / u_colorRange : 0.0;\n float invRangeV = u_colorRangeV > 0.0 ? 1.0 / u_colorRangeV : 0.0;\n float uVal = clamp((rawU - u_colorMin) * invRangeU, 0.0, 1.0);\n float vVal = clamp((rawV - u_colorMinV) * invRangeV, 0.0, 1.0);\n vec3 rgb2d = colormap2d(u_color2dMapIndex, uVal, vVal);\n v_color = vec4(rgb2d, 1.0);\n } else {\n v_color = u_color;\n }\n\n // Read selection bit (not tiled — bit-packed width is small)\n v_selected = 1u;\n if (u_useSelectionMask > 0.5) {\n int word_idx = idx / 32;\n uint word = texelFetch(u_selectionTex, ivec2(word_idx, 0), 0).r;\n v_selected = (word >> uint(idx % 32)) & 1u;\n }\n\n v_useSubtractive = u_useSubtractive;\n v_useSelectionMask = u_useSelectionMask;\n}\n`,n=`#version 300 es\n// Fullscreen tone-map pass — reads HDR float texture and maps to [0,1].\n// Same logic as tonemap.wgsl.\n//\n// Mode 0 (additive): per-channel exponential compression 1-exp(-x).\n// Preserves hue ratios, dense regions desaturate toward white.\n// Mode 1 (normal / subtractive): simple clamp to [0,1].\n\nprecision highp float;\n\nuniform sampler2D u_hdrTexture;\nuniform float u_mode;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main() {\n vec4 hdr = texelFetch(u_hdrTexture, ivec2(gl_FragCoord.xy), 0);\n\n vec3 mapped;\n if (u_mode < 0.5) {\n // Additive: exponential compression preserving density structure\n mapped = 1.0 - exp(-hdr.rgb);\n } else {\n // Normal / Subtractive: values already in [0,1], just clamp\n mapped = clamp(hdr.rgb, vec3(0.0), vec3(1.0));\n }\n fragColor = vec4(mapped, 1.0);\n}\n`,r=`#version 300 es\n// Fullscreen tone-map pass — oversized triangle (3 vertices, no vertex buffer).\n// Same geometry as tonemap.wgsl.\n\nvoid main() {\n // Oversized triangle covering the full clip space:\n // vi=0 -> (-1, -1) vi=1 -> (3, -1) vi=2 -> (-1, 3)\n float x = float(gl_VertexID / 2) * 4.0 - 1.0;\n float y = float(gl_VertexID % 2) * 4.0 - 1.0;\n gl_Position = vec4(x, y, 0.0, 1.0);\n}\n`;const i=1e-10,a=e=>{let t=e[0],n=e[1],r=e[2],a=e[3],o=t*t+r*r,s=t*n+r*a,c=n*n+a*a,l=o+c,u=o*c-s*s,d=Math.sqrt(Math.max(0,l*l*.25-u)),f=l*.5+d,p=l*.5-d,m=Math.sqrt(Math.max(0,f)),h=Math.sqrt(Math.max(0,p)),g=new Float32Array([m,h]),_=new Float32Array(4);if(Math.abs(s)>i){let e=f-c,t=Math.sqrt(e*e+s*s);_[0]=e/t,_[1]=s/t;let n=p-c,r=Math.sqrt(n*n+s*s);_[2]=n/r,_[3]=s/r}else _[0]=1,_[1]=0,_[2]=0,_[3]=1;let v=new Float32Array(4);return m>i&&(v[0]=(t*_[0]+n*_[1])/m,v[2]=(r*_[0]+a*_[1])/m),h>i&&(v[1]=(t*_[2]+n*_[3])/h,v[3]=(r*_[2]+a*_[3])/h),m<=i&&h>i?(v[0]=-v[3],v[2]=v[1]):h<=i&&m>i?(v[1]=-v[2],v[3]=v[0]):m<=i&&h<=i&&(v[0]=1,v[1]=0,v[2]=0,v[3]=1),{U:v,S:g,V:_}},o=(e,t)=>{let n=0;for(let r=0;r<t;r++)n+=e[r]*e[r];if(n=Math.sqrt(n),n>1e-10)for(let r=0;r<t;r++)e[r]=e[r]/n;let r=0;for(let n=0;n<t;n++)r+=e[n]*e[t+n];for(let n=0;n<t;n++)e[t+n]=e[t+n]-r*e[n];let i=0;for(let n=0;n<t;n++)i+=e[t+n]*e[t+n];if(i=Math.sqrt(i),i>1e-10)for(let n=0;n<t;n++)e[t+n]=e[t+n]/i},s=(e,t,n,r,i,a,s,c=!0)=>{let l=s*s,u=l*s,d=.5*(-s+2*l-u),f=.5*(2-5*l+3*u),p=.5*(s+4*l-3*u),m=.5*(-l+u);for(let o=0;o<a*2;o++)e[o]=d*t[o]+f*n[o]+p*r[o]+m*i[o];return c&&o(e,a),e},c=(e,t,n)=>{let r=new Float32Array(4);for(let i=0;i<2;i++)for(let a=0;a<2;a++){let o=0;for(let r=0;r<n;r++)o+=e[i*n+r]*t[a*n+r];r[i*2+a]=o}let{S:i}=a(r),o=Math.acos(Math.max(-1,Math.min(1,i[0]))),s=Math.acos(Math.max(-1,Math.min(1,i[1])));return Math.sqrt(o*o+s*s)},l=(e,t,n=!0)=>{let r=e.length,i=new Float32Array(r+1);if(i[0]=0,!n){for(let e=1;e<=r;e++)i[e]=e/r;return i}let a=new Float32Array(t*2),o=new Float32Array(t*2);for(let n=0;n<r;n++){let l=(n-1+r)%r,u=n,d=(n+1)%r,f=(n+2)%r,p=0;s(a,e[l],e[u],e[d],e[f],t,0);for(let n=1;n<=8;n++){let r=n/8;s(o,e[l],e[u],e[d],e[f],t,r),p+=c(a,o,t),a.set(o)}i[n+1]=i[n]+p}let l=i[r];if(l>1e-10)for(let e=1;e<=r;e++)i[e]=i[e]/l;return i},u=(e,t)=>{let n=e.length-1,r=Math.max(0,Math.min(1,t)),i=0,a=n-1;for(;i<a;){let t=i+a>>>1;e[t+1]<=r?i=t+1:a=t}let o=e[i],s=e[i+1]-o,c=s>1e-10?(r-o)/s:0;return{segment:i,localT:c}},d=(e,t,n,r,i,a=!0)=>{let o=t.length,{segment:c,localT:l}=u(n,i),d=(c-1+o)%o,f=c,p=(c+1)%o,m=(c+2)%o;return s(e,t[d],t[f],t[p],t[m],r,l,a)};let f=null,p=[];const m=(e,t)=>{self.postMessage(e,t??[])},h=(e,t,n)=>{let r=e.createShader(t);if(e.shaderSource(r,n),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){let t=e.getShaderInfoLog(r);throw e.deleteShader(r),Error(`Shader compile error: ${t}`)}return r},g=(e,t,n)=>{let r=h(e,e.VERTEX_SHADER,t),i=h(e,e.FRAGMENT_SHADER,n),a=e.createProgram();if(e.attachShader(a,r),e.attachShader(a,i),e.linkProgram(a),!e.getProgramParameter(a,e.LINK_STATUS)){let t=e.getProgramInfoLog(a);throw e.deleteProgram(a),Error(`Program link error: ${t}`)}return e.deleteShader(r),e.deleteShader(i),a},_=(e,t,n)=>e.getUniformLocation(t,n),v=(e,t,n,r,i,a)=>{if(e===0||t===0||n===0)return{pointSize:.012,opacity:.7};let o=t*r,s=n*r,c=o*s*i/e,l=Math.sqrt(c/Math.PI),u=a*r*.5,d,f;return l>=u?(d=l,f=1):(d=u,f=Math.max(.01,c/(Math.PI*u*u))),{pointSize:2*d/s,opacity:f}},y=(e,t)=>e*f.dpr/t,b=(e,t)=>{let{style:n,numPoints:r,dpr:i}=f;if(n.pointSize!==`auto`&&n.opacity!==`auto`)return{pointSize:y(n.pointSize,t),opacity:n.opacity,color:n.color,scaleOpacityByZoom:!1};let a=v(r,e/i,t/i,i,n.fillTarget,n.minPointSize),o=n.opacity===`auto`;return{pointSize:n.pointSize===`auto`?a.pointSize:y(n.pointSize,t),opacity:o?a.opacity:n.opacity,color:n.color,scaleOpacityByZoom:o}};let x=null;const S=(e,t,n,r)=>{(!x||x.length!==r*2)&&(x=new Float32Array(r*2));let i=0,a=0;for(let o=0;o<r;o++){let s=Math.max(n[o],1e-6),c=e[o],l=e[r+o],u=2/s;x[o]=c*u,x[r+o]=l*u;let d=-t[o]/s;i+=d*c*2,a+=d*l*2}return{weights:x,biasX:i,biasY:a}},C=()=>{if(!f?.normMins||!f.normRanges)return null;let e=f.numDims,t=new Float32Array(e);if(f.centering===`mean`&&f.normMeans)t.set(f.normMeans);else for(let n=0;n<e;n++)t[n]=f.normMins[n]+f.normRanges[n]*.5;return t},w=(e,t,n)=>{if(!f)return;let{hdrFbos:r,hdrTextures:i,hdrSizes:a}=f,o=f.mainView.gl,s=a[e];if(s&&s[0]===t&&s[1]===n)return;r[e]&&o.deleteFramebuffer(r[e]),i[e]&&o.deleteTexture(i[e]);let c=o.createTexture();o.bindTexture(o.TEXTURE_2D,c),o.texImage2D(o.TEXTURE_2D,0,o.RGBA32F,t,n,0,o.RGBA,o.FLOAT,null),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MIN_FILTER,o.NEAREST),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MAG_FILTER,o.NEAREST),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE);let l=o.createFramebuffer();o.bindFramebuffer(o.FRAMEBUFFER,l),o.framebufferTexture2D(o.FRAMEBUFFER,o.COLOR_ATTACHMENT0,o.TEXTURE_2D,c,0),o.bindFramebuffer(o.FRAMEBUFFER,null),o.bindTexture(o.TEXTURE_2D,null),f.hdrFbos[e]=l,f.hdrTextures[e]=c,f.hdrSizes[e]=[t,n]},T=(e,t)=>{if(!f)return;let n=f.blitSize;if(n&&n[0]===e&&n[1]===t)return;let r=f.mainView.gl;f.blitFbo&&r.deleteFramebuffer(f.blitFbo),f.blitTexture&&r.deleteTexture(f.blitTexture);let i=r.createTexture();r.bindTexture(r.TEXTURE_2D,i),r.texImage2D(r.TEXTURE_2D,0,r.RGBA8,e,t,0,r.RGBA,r.UNSIGNED_BYTE,null),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.NEAREST),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.NEAREST);let a=r.createFramebuffer();r.bindFramebuffer(r.FRAMEBUFFER,a),r.framebufferTexture2D(r.FRAMEBUFFER,r.COLOR_ATTACHMENT0,r.TEXTURE_2D,i,0),r.bindFramebuffer(r.FRAMEBUFFER,null),r.bindTexture(r.TEXTURE_2D,null),f.blitFbo=a,f.blitTexture=i,f.blitSize=[e,t]};let E=null,D=0,O=null,k=!1,A=null;const j=()=>{if(!O||!f)return!0;let e=f.mainView.gl;return e.clientWaitSync(O,0,0)===e.TIMEOUT_EXPIRED?!1:(e.deleteSync(O),O=null,!0)},M=()=>{if(!j())return k=!0,A||=setTimeout(function(){A=null,k&&f&&!E&&M()},2),!1;if(k=!1,U(),f){let e=f.mainView.gl;O=e.fenceSync(e.SYNC_GPU_COMMANDS_COMPLETE,0),e.flush()}return!0},N=e=>{if(!E||!f?.tour)return;let t=E.prevTime===null?void 0:e-E.prevTime;if(t!==void 0){let e=t/1e3*E.speed*E.direction/20,n=f.tour.position+e;n-=Math.floor(n),f.tour.position=n}E.prevTime=e,f.directBasis=null,M()&&(E.lastRenderedRafTime!==null&&E.frameTimes.push(e-E.lastRenderedRafTime),E.lastRenderedRafTime=e),e-D>=33&&(m({type:`playbackTick`,position:f.tour.position,frameMs:t}),D=e),E.rafId=requestAnimationFrame(N)},P=(e,t)=>{E?(E.speed=e,E.direction=t):(O=null,k=!1,E={speed:e,direction:t,prevTime:null,rafId:0,frameTimes:[],lastRenderedRafTime:null},E.rafId=requestAnimationFrame(N))},ee=()=>{if(E){cancelAnimationFrame(E.rafId),f?.tour&&m({type:`playbackTick`,position:f.tour.position});let e=Float64Array.from(E.frameTimes);m({type:`playbackStopped`,frameTimes:e},[e.buffer]),E=null}},F=(e,t,n,r,i,a)=>{if(!f?.dataTexture||!f.normRanges)return;let{numPoints:o,numDims:s,pointProgram:c,pointLocs:l,tonemapProgram:u,tonemapLocs:d}=f,p=f.mainView.gl,m=C();if(!m)return;let{weights:h,biasX:g,biasY:_}=S(e,m,f.normRanges,s),v=b(t,n),y=f.backgroundColor,x=.2126*y[0]+.7152*y[1]+.0722*y[2],T=f.colorMode>0,E=!T&&x>.5,D;D=T||E?1:0;let O=t/n||1;if(w(r,t,n),p.bindFramebuffer(p.FRAMEBUFFER,f.hdrFbos[r]),p.viewport(0,0,t,n),p.clearColor(y[0],y[1],y[2],1),p.clear(p.COLOR_BUFFER_BIT),o>0){if(p.useProgram(c),p.enable(p.BLEND),T?(p.blendEquation(p.FUNC_ADD),p.blendFunc(p.ONE,p.ONE_MINUS_SRC_ALPHA)):E?(p.blendEquationSeparate(p.FUNC_REVERSE_SUBTRACT,p.FUNC_ADD),p.blendFunc(p.ONE,p.ONE)):(p.blendEquation(p.FUNC_ADD),p.blendFunc(p.ONE,p.ONE)),p.activeTexture(p.TEXTURE0),p.bindTexture(p.TEXTURE_2D,f.dataTexture),p.uniform1i(l.u_data,0),p.activeTexture(p.TEXTURE1),f.colorLutTexture&&p.bindTexture(p.TEXTURE_2D,f.colorLutTexture),p.uniform1i(l.u_colorLutTex,1),p.activeTexture(p.TEXTURE2),f.selectionTexture&&p.bindTexture(p.TEXTURE_2D,f.selectionTexture),p.uniform1i(l.u_selectionTex,2),p.activeTexture(p.TEXTURE3),f.colorMode===2&&f.activeCatColumnName){let e=f.catIndexTextures.get(f.activeCatColumnName);e&&p.bindTexture(p.TEXTURE_2D,e)}p.uniform1i(l.u_catIndexTex,3);let e=new Float32Array(128),t=new Float32Array(128);for(let n=0;n<s&&n<128;n++)e[n]=h[n],t[n]=h[s+n];p.uniform1fv(l.u_adjBasisX,e),p.uniform1fv(l.u_adjBasisY,t),p.uniform2f(l.u_bias,g,_),p.uniform1f(l.u_pointSize,v.pointSize),p.uniform1f(l.u_opacity,v.opacity),p.uniform4f(l.u_color,v.color[0],v.color[1],v.color[2],1),p.uniform1f(l.u_useSubtractive,E?1:0),p.uniform1f(l.u_scaleOpacityByZoom,v.scaleOpacityByZoom?1:0),p.uniform2f(l.u_pan,i.panX,i.panY),p.uniform1f(l.u_zoom,i.zoom),p.uniform1f(l.u_aspect,O),p.uniform1f(l.u_viewportHeight,n),p.uniform1f(l.u_insetOffsetY,i.insetOffsetY),p.uniform1f(l.u_insetZoom,i.insetZoom),p.uniform1i(l.u_numPoints,o),p.uniform1i(l.u_numDims,s),p.uniform1i(l.u_maxPoints,f.maxPoints),p.uniform1i(l.u_texWidth,f.texWidth),p.uniform1i(l.u_colorMode,f.colorMode),p.uniform1i(l.u_colorColumnIndex,f.colorColumnIndex),p.uniform1f(l.u_colorMin,f.colorMin),p.uniform1f(l.u_colorRange,f.colorRange),p.uniform1i(l.u_colorNumStops,f.colorNumStops),p.uniform1i(l.u_colorColumnIndexV,f.colorColumnIndexV),p.uniform1f(l.u_colorMinV,f.colorMinV),p.uniform1f(l.u_colorRangeV,f.colorRangeV),p.uniform1i(l.u_color2dMapIndex,f.color2dMapIndex),p.uniform1fv(l.u_color2dLut,f.color2dLut),p.uniform1f(l.u_useSelectionMask,f.styleFlags.useSelectionMask?1:0),p.drawArrays(p.POINTS,0,o),p.disable(p.BLEND)}p.bindFramebuffer(p.FRAMEBUFFER,a??null),p.viewport(0,0,t,n),p.clearColor(0,0,0,1),p.clear(p.COLOR_BUFFER_BIT),p.useProgram(u),p.activeTexture(p.TEXTURE0),p.bindTexture(p.TEXTURE_2D,f.hdrTextures[r]),p.uniform1i(d.u_hdrTexture,0),p.uniform1f(d.u_mode,D),p.drawArrays(p.TRIANGLES,0,3),p.finish()};let I=!1,L=null,R=null;const z=(e,t)=>{if(!f?.dataTexture)return;let n=f.previewCanvases.get(e);if(!n){m({type:`rendered`,viewIndex:e});return}let r=n.canvas.width,i=n.canvas.height;if(r===0||i===0){m({type:`rendered`,viewIndex:e});return}let a=f.mainView.gl;T(r,i),F(t,r,i,1,{panX:0,panY:0,zoom:1,aspect:1,viewportHeight:i,insetOffsetY:0,insetZoom:1},f.blitFbo);let o=r*i*4,s=r*4;(!L||L.length<o)&&(L=new Uint8Array(o)),(!R||R.length<s)&&(R=new Uint8Array(s)),a.bindFramebuffer(a.FRAMEBUFFER,f.blitFbo),a.readPixels(0,0,r,i,a.RGBA,a.UNSIGNED_BYTE,L),a.bindFramebuffer(a.FRAMEBUFFER,null);for(let e=0;e<i>>1;e++){let t=e*s,n=(i-1-e)*s;R.set(L.subarray(t,t+s)),L.copyWithin(t,n,n+s),L.set(R,n)}let c=new ImageData(new Uint8ClampedArray(L.buffer,0,o),r,i);n.ctx2d.putImageData(c,0,0),m({type:`rendered`,viewIndex:e})},B=()=>{if(!f?.dataTexture||!f.tour)return;let{tour:e}=f;for(let[t]of f.previewCanvases)t>=e.bases.length||z(t,e.bases[t])},V=()=>{I||(I=!0,setTimeout(()=>{I=!1,B()},0))},H=()=>{f?.dataTexture&&(U(),V())},U=()=>{if(!f?.dataTexture)return;let{canvas:e}=f.mainView;if(f.directBasis){F(f.directBasis,e.width,e.height,0,f.camera),m({type:`rendered`,viewIndex:0});return}if(!f.tour)return;let{tour:t}=f;d(t.interpolatedBasis,t.bases,t.arcLengths,t.dims,t.position,t.orthonormalize),F(t.interpolatedBasis,e.width,e.height,0,f.camera),m({type:`rendered`,viewIndex:0})},W=()=>{f&&(f.tour||f.directBasis)&&f.dataTexture&&H()},G=()=>{f&&(f.styleFlags.useSelectionMask=!0,(f.tour||f.directBasis)&&f.dataTexture&&H())};let K=0;const q=e=>{if(!f)return;let t=f.mainView.gl;if(e.data.type===`setColorContinuous`){if(e.data.dataVersion!==K)return;let{columnIndex:n,min:r,range:i,colormap:a}=e.data;f.colorLutTexture&&t.deleteTexture(f.colorLutTexture),f.colorLutTexture=J(t,a),f.colorMode=1,f.colorColumnIndex=n,f.colorMin=r,f.colorRange=i,f.colorNumStops=a.length,f.activeCatColumnName=null,W();return}if(e.data.type===`setColorCategorical`){if(e.data.dataVersion!==K)return;let{catColumnName:n,palette:r}=e.data;if(!f.catIndexTextures.has(n))return;f.colorLutTexture&&t.deleteTexture(f.colorLutTexture),f.colorLutTexture=J(t,r),f.colorMode=2,f.colorNumStops=r.length,f.activeCatColumnName=n,W();return}if(e.data.type===`setColor2D`){if(e.data.dataVersion!==K)return;let{columnIndexX:t,columnIndexY:n,minX:r,rangeX:i,minY:a,rangeY:o,lut:s,mapIndex:c}=e.data;f.colorMode=3,f.colorColumnIndex=t,f.colorMin=r,f.colorRange=i,f.colorColumnIndexV=n,f.colorMinV=a,f.colorRangeV=o,f.color2dMapIndex=c,f.activeCatColumnName=null,s&&f.color2dLut.set(s.subarray(0,128)),W();return}if(e.data.type===`selectContinuous`){if(e.data.dataVersion!==K)return;let{columnIndex:n,ranges:r}=e.data,{numPoints:i,dataBuffers:a}=f,o=a[n];if(!o)return;let s=new Uint32Array(Math.ceil(i/32)),c=r.length/2;for(let e=0;e<i;e++){let t=o[e],n=!1;for(let e=0;e<c;e++)if(t>=r[e*2]&&t<=r[e*2+1]){n=!0;break}n&&(s[e>>5]=s[e>>5]|0|1<<(e&31))}X(t,s),G();return}if(e.data.type===`selectCategorical`){if(e.data.dataVersion!==K)return;let{catColumnName:n,selectedLabels:r}=e.data,i=f.categoricalBuffers.get(n);if(!i)return;let{numPoints:a}=f,o=new Uint32Array(Math.ceil(a/32));for(let e=0;e<a;e++){let t=i[e];t<r.length&&r[t]>0&&(o[e>>5]=o[e>>5]|0|1<<(e&31))}X(t,o),G();return}if(e.data.type!==`data`)return;let{dataVersion:n,dims:r,rows:i,buffers:a,mins:o,ranges:s,means:c,categoricalColumns:u}=e.data;if(K=n,r<2||i===0||a.length<2){m({type:`error`,message:`Need at least 2 dimensions to render`});return}f.dataTexture&&(t.deleteTexture(f.dataTexture),f.dataTexture=null),f.colorLutTexture&&(t.deleteTexture(f.colorLutTexture),f.colorLutTexture=null);for(let e of f.catIndexTextures.values())t.deleteTexture(e);f.catIndexTextures.clear(),f.selectionTexture&&(t.deleteTexture(f.selectionTexture),f.selectionTexture=null),f.styleFlags={useSelectionMask:!1},f.colorMode=0,f.colorColumnIndex=0,f.colorMin=0,f.colorRange=1,f.colorNumStops=0,f.activeCatColumnName=null,f.categoricalBuffers.clear(),f.dataBuffers=a,f.normMins=new Float32Array(o),f.normRanges=new Float32Array(s),f.normMeans=new Float32Array(c),f.numPoints=i,f.numDims=r;let d=t.getParameter(t.MAX_TEXTURE_SIZE),p=Math.min(i,d),h=Math.ceil(i/p);f.texWidth=p;let g=t.createTexture();t.bindTexture(t.TEXTURE_2D,g);let _=r*h,v=new Float32Array(p*_);for(let e=0;e<r;e++){let t=a[e],n=e*h;for(let e=0;e<i;e++){let r=e%p,i=n+Math.floor(e/p);v[i*p+r]=t[e]}}t.texImage2D(t.TEXTURE_2D,0,t.R32F,p,_,0,t.RED,t.FLOAT,v),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),t.bindTexture(t.TEXTURE_2D,null),f.dataTexture=g;for(let e of u)f.categoricalBuffers.set(e.name,e.indices),f.catIndexTextures.set(e.name,Y(t,e.indices,i,p));if(f.tour&&f.tour.dims===r)H();else{let e=Math.max(1,f.previewCanvases.size),t=[];for(let n=0;n<e;n++){let i=new Float32Array(r*2),a=Math.floor(n/e*r);i[a]=1,i[r+(a+1)%r]=1,t.push(i)}f.tour={bases:t,arcLengths:l(t,r),dims:r,orthonormalize:!0,position:0,interpolatedBasis:new Float32Array(r*2)},H()}},J=(e,t)=>{let n=e.createTexture();return e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.R32UI,t.length,1,0,e.RED_INTEGER,e.UNSIGNED_INT,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),n},Y=(e,t,n,r)=>{let i=Math.ceil(n/r),a;if(i===1)a=t;else{a=new Uint32Array(r*i);for(let e=0;e<n;e++){let n=e%r,i=Math.floor(e/r);a[i*r+n]=t[e]}}let o=e.createTexture();return e.bindTexture(e.TEXTURE_2D,o),e.texImage2D(e.TEXTURE_2D,0,e.R32UI,r,i,0,e.RED_INTEGER,e.UNSIGNED_INT,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),o},X=(e,t)=>{if(!f)return;f.selectionTexture&&e.deleteTexture(f.selectionTexture);let n=e.createTexture();e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.R32UI,t.length,1,0,e.RED_INTEGER,e.UNSIGNED_INT,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),f.selectionTexture=n},Z=(e,t,n,r)=>{let i=!1;for(let a=0,o=r-1;a<r;o=a++){let r=n[a*2],s=n[a*2+1],c=n[o*2],l=n[o*2+1];s>t!=l>t&&e<(c-r)*(t-s)/(l-s)+r&&(i=!i)}return i},Q=e=>{if(!f?.normRanges||f.numPoints===0)return;let{numPoints:t,numDims:n,dataBuffers:r,camera:i}=f,a=e.length/2;if(a<3)return;let o=null;if(f.directBasis)o=f.directBasis;else if(f.tour){let{tour:e}=f;d(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),o=e.interpolatedBasis}if(!o)return;let s=C();if(!s)return;let{weights:c,biasX:l,biasY:u}=S(o,s,f.normRanges,n),p=f.mainView.canvas,h=p.width/p.height||1,g=i.insetZoom,_=i.zoom*g,v=new Float32Array(e.length);for(let t=0;t<a;t++)v[t*2]=e[t*2]*h/_-i.panX,v[t*2+1]=(e[t*2+1]-i.insetOffsetY)/_-i.panY;let y=new Uint32Array(Math.ceil(t/32));for(let e=0;e<t;e++){let t=l,i=u;for(let a=0;a<n;a++){let o=r[a][e];t+=o*c[a],i+=o*c[n+a]}Z(t,i,v,a)&&(y[e>>5]=y[e>>5]|0|1<<(e&31))}let b=f.mainView.gl;X(b,y),G();let x=new Uint32Array(y);m({type:`selectionResult`,mask:x},[x.buffer])},te=()=>{if(!f)return 0;let e=0;if(f.dataTexture){let t=Math.ceil(f.numPoints/f.texWidth);e+=f.texWidth*f.numDims*t*4}for(let t=0;t<f.hdrSizes.length;t++){let n=f.hdrSizes[t];n&&(e+=n[0]*n[1]*16)}f.blitSize&&(e+=f.blitSize[0]*f.blitSize[1]*4),f.colorLutTexture&&(e+=f.colorNumStops*4);for(let t of f.categoricalBuffers.values())e+=t.byteLength;return f.selectionTexture&&(e+=Math.ceil(f.numPoints/32)*4),e},ne=()=>{if(!f)return;let e=0;for(let t of f.dataBuffers)e+=t.byteLength;for(let t of f.categoricalBuffers.values())e+=t.byteLength;if(f.normMins&&(e+=f.normMins.byteLength),f.normRanges&&(e+=f.normRanges.byteLength),f.tour){for(let t of f.tour.bases)e+=t.byteLength;e+=f.tour.interpolatedBasis.byteLength}m({type:`metricsResult`,gpuMemoryBytes:te(),numPoints:f.numPoints,numDims:f.numDims,workerJsHeapBytes:e})},re=e=>{if(!f?.tour||!f.dataTexture||f.numPoints===0){m({type:`error`,message:`No data/tour loaded for benchmark`});return}let t=f.mainView.gl,{numPoints:n,numDims:r,tour:i}=f,a=i.position;f.directBasis=null;let o=new Uint8Array(4),s=()=>{t.readPixels(0,0,1,1,t.RGBA,t.UNSIGNED_BYTE,o)};for(let e=0;e<5;e++)i.position=e/5,d(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize),F(i.interpolatedBasis,f.mainView.canvas.width,f.mainView.canvas.height,0,f.camera),s();let c=new Float64Array(e);for(let t=0;t<e;t++){i.position=t/e,d(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize);let n=performance.now();F(i.interpolatedBasis,f.mainView.canvas.width,f.mainView.canvas.height,0,f.camera),s(),c[t]=performance.now()-n}i.position=a,m({type:`benchmarkResult`,frameTimes:c,numPoints:n,numDims:r},[c.buffer])},$=e=>{if(f){if(e.type===`setBases`){let{bases:t}=e;if(t.length===0)return;let n=t[0].length/2,r=e.tourFamily!==`sequential`;f.tour={bases:t,arcLengths:l(t,n,r),dims:n,orthonormalize:r,position:f.tour?.position??0,interpolatedBasis:new Float32Array(n*2)},f.dataTexture&&H();return}if(e.type===`addPreviewCanvas`){let t=e.canvas.getContext(`2d`);if(!t)return;f.previewCanvases.set(e.id,{canvas:e.canvas,ctx2d:t}),f.dataTexture&&f.tour&&e.id<f.tour.bases.length&&z(e.id,f.tour.bases[e.id]);return}if(e.type===`removePreviewCanvas`){f.previewCanvases.delete(e.id);return}if(e.type===`resizePreview`){let t=f.previewCanvases.get(e.id);if(!t)return;t.canvas.width=e.width,t.canvas.height=e.height,f.dataTexture&&f.tour&&e.id<f.tour.bases.length&&V();return}if(e.type===`setTourPosition`){if(!f.tour)return;f.tour.position=e.position,f.directBasis=null,M();return}if(e.type===`setStyle`){f.style={pointSize:e.pointSize,opacity:e.opacity,color:e.color,minPointSize:e.minPointSize??f.style.minPointSize,fillTarget:e.fillTarget??f.style.fillTarget},(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setCamera`){f.camera={panX:e.pan[0],panY:e.pan[1],zoom:e.zoom,aspect:f.camera.aspect,viewportHeight:f.camera.viewportHeight,insetOffsetY:e.insetOffsetY,insetZoom:e.insetZoom},U();return}if(e.type===`setCentering`){f.centering=e.centering,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`resize`){e.dpr!==void 0&&(f.dpr=e.dpr),e.viewIndex===0&&(f.mainView.canvas.width=e.width,f.mainView.canvas.height=e.height,(f.tour||f.directBasis)&&f.dataTexture&&U());return}if(e.type===`render`){(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setDirectBasis`){if(!f.dataTexture)return;f.directBasis=e.basis,F(e.basis,f.mainView.canvas.width,f.mainView.canvas.height,0,f.camera),m({type:`rendered`,viewIndex:0});return}if(e.type===`clearColors`){f.colorLutTexture&&(f.mainView.gl.deleteTexture(f.colorLutTexture),f.colorLutTexture=null),f.colorMode=0,f.colorColumnIndex=0,f.colorMin=0,f.colorRange=1,f.colorNumStops=0,f.activeCatColumnName=null,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setBackgroundColor`){f.backgroundColor=e.color,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setMaxPoints`){f.maxPoints=e.maxPoints,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setSelectionMask`){let t=f.mainView.gl;X(t,e.mask),f.styleFlags.useSelectionMask=!0,(f.tour||f.directBasis)&&f.dataTexture&&H();let n=new Uint32Array(e.mask);m({type:`selectionResult`,mask:n},[n.buffer]);return}if(e.type===`clearSelectionMask`){f.selectionTexture&&(f.mainView.gl.deleteTexture(f.selectionTexture),f.selectionTexture=null),f.styleFlags.useSelectionMask=!1,(f.tour||f.directBasis)&&f.dataTexture&&H(),m({type:`selectionResult`,mask:new Uint32Array});return}if(e.type===`startPlayback`){if(!f.tour)return;P(e.speed,e.direction);return}if(e.type===`stopPlayback`){ee();return}if(e.type===`computePCA`){m({type:`error`,message:`PCA not yet implemented in WebGL backend`});return}if(e.type===`lassoSelect`){Q(e.polygon);return}if(e.type===`getProjectedPositions`){if(!f?.normRanges||f.numPoints===0){m({type:`projectedPositions`,positions:new Float32Array},[]);return}let{numPoints:e,numDims:t,dataBuffers:n}=f,r=null;if(f.directBasis)r=f.directBasis;else if(f.tour){let{tour:e}=f;d(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),r=e.interpolatedBasis}if(!r){m({type:`projectedPositions`,positions:new Float32Array},[]);return}let i=C();if(!i){m({type:`projectedPositions`,positions:new Float32Array},[]);return}let{weights:a,biasX:o,biasY:s}=S(r,i,f.normRanges,t),c=new Float32Array(e*2);for(let r=0;r<e;r++){let e=o,i=s;for(let o=0;o<t;o++){let s=n[o][r];e+=s*a[o],i+=s*a[t+o]}c[r*2]=e,c[r*2+1]=i}m({type:`projectedPositions`,positions:c},[c.buffer]);return}if(e.type===`getPointData`){if(!f||f.numPoints===0){m({type:`pointData`,pointIndex:e.pointIndex,numericValues:{},categoricalValues:{}});return}let{numDims:t,dataBuffers:n}=f,r=e.pointIndex;if(r<0||r>=f.numPoints){m({type:`pointData`,pointIndex:r,numericValues:{},categoricalValues:{}});return}let i={};for(let e=0;e<t;e++)i[String(e)]=n[e][r];let a={};for(let[e,t]of f.categoricalBuffers)a[e]=t[r];m({type:`pointData`,pointIndex:r,numericValues:i,categoricalValues:a});return}if(e.type===`benchmark`){re(e.numFrames);return}if(e.type===`getMetrics`){ne();return}}};self.onmessage=i=>{let a=i.data;if(a.type===`init`){try{let i=a.canvas,o=i.getContext(`webgl2`,{alpha:!0,premultipliedAlpha:!0,antialias:!1,preserveDrawingBuffer:!1});if(!o)throw Error(`WebGL2 not supported`);o.getExtension(`EXT_color_buffer_float`)||console.warn(`EXT_color_buffer_float not available — HDR rendering may fail`),o.getExtension(`OES_texture_float_linear`),o.getExtension(`EXT_float_blend`);let s=o,c=g(s,t,e),l={u_data:_(s,c,`u_data`),u_adjBasisX:_(s,c,`u_adjBasisX`),u_adjBasisY:_(s,c,`u_adjBasisY`),u_bias:_(s,c,`u_bias`),u_pointSize:_(s,c,`u_pointSize`),u_opacity:_(s,c,`u_opacity`),u_color:_(s,c,`u_color`),u_useSubtractive:_(s,c,`u_useSubtractive`),u_pan:_(s,c,`u_pan`),u_zoom:_(s,c,`u_zoom`),u_aspect:_(s,c,`u_aspect`),u_viewportHeight:_(s,c,`u_viewportHeight`),u_insetOffsetY:_(s,c,`u_insetOffsetY`),u_insetZoom:_(s,c,`u_insetZoom`),u_numPoints:_(s,c,`u_numPoints`),u_numDims:_(s,c,`u_numDims`),u_maxPoints:_(s,c,`u_maxPoints`),u_texWidth:_(s,c,`u_texWidth`),u_colorMode:_(s,c,`u_colorMode`),u_colorColumnIndex:_(s,c,`u_colorColumnIndex`),u_colorMin:_(s,c,`u_colorMin`),u_colorRange:_(s,c,`u_colorRange`),u_colorNumStops:_(s,c,`u_colorNumStops`),u_colorLutTex:_(s,c,`u_colorLutTex`),u_catIndexTex:_(s,c,`u_catIndexTex`),u_useSelectionMask:_(s,c,`u_useSelectionMask`),u_selectionTex:_(s,c,`u_selectionTex`),u_colorColumnIndexV:_(s,c,`u_colorColumnIndexV`),u_colorMinV:_(s,c,`u_colorMinV`),u_colorRangeV:_(s,c,`u_colorRangeV`),u_color2dMapIndex:_(s,c,`u_color2dMapIndex`),u_color2dLut:_(s,c,`u_color2dLut`),u_scaleOpacityByZoom:_(s,c,`u_scaleOpacityByZoom`)},u=g(s,r,n),d={u_hdrTexture:_(s,u,`u_hdrTexture`),u_mode:_(s,u,`u_mode`)};f={mainView:{canvas:i,gl:o},previewCanvases:new Map,pointProgram:c,pointLocs:l,tonemapProgram:u,tonemapLocs:d,hdrFbos:[],hdrTextures:[],hdrSizes:[],blitFbo:null,blitTexture:null,blitSize:null,dataTexture:null,colorLutTexture:null,catIndexTextures:new Map,selectionTexture:null,numPoints:0,numDims:0,dataBuffers:[],normMins:null,normRanges:null,normMeans:null,centering:`midrange`,categoricalBuffers:new Map,tour:null,style:{pointSize:`auto`,opacity:`auto`,color:[.25,.5,.9],minPointSize:2,fillTarget:.5},styleFlags:{useSelectionMask:!1},colorMode:0,colorColumnIndex:0,colorMin:0,colorRange:1,colorNumStops:0,activeCatColumnName:null,colorColumnIndexV:0,colorMinV:0,colorRangeV:1,color2dMapIndex:0,color2dLut:new Float32Array(128),camera:{panX:0,panY:0,zoom:a.zoom,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1},directBasis:null,backgroundColor:[0,0,0],dpr:a.dpr,maxPoints:0,texWidth:0},a.dataPort.onmessage=q;let h=p;if(p=null,h)for(let e of h)$(e);m({type:`ready`})}catch(e){m({type:`error`,message:e instanceof Error?e.message:String(e)})}return}if(p){p.push(a);return}$(a)};", M = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", j], { type: "text/javascript;charset=utf-8" });
3026
+ }, j = "var e=`#version 300 es\n// Point fragment shader — SDF circle anti-aliasing\n//\n// WebGL2 equivalent of point.wgsl fs_main. Uses gl_PointCoord for the\n// SDF distance (maps [0,1] in gl.POINTS, remap to [-1,1]).\n\nprecision highp float;\nprecision highp int;\n\nflat in float v_effOpacity;\nflat in vec4 v_color;\nflat in uint v_selected;\nflat in float v_useSubtractive;\nflat in float v_useSelectionMask;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main() {\n // gl_PointCoord is [0,1], remap to [-1,1] for SDF\n vec2 uv = gl_PointCoord * 2.0 - 1.0;\n float dist = length(uv);\n\n // Smooth anti-aliased edge — naturally 0 for dist >= 1.0, so no discard needed.\n // Avoiding discard preserves early-Z and SIMD wavefront occupancy.\n float edge = 1.0 - smoothstep(0.75, 1.0, dist);\n\n // Selection: boost selected, dim unselected\n float sel_factor = 1.0;\n if (v_useSelectionMask > 0.5) {\n if (v_selected == 0u) {\n sel_factor = 0.1;\n } else {\n sel_factor = 1.0 / max(v_effOpacity, 0.01);\n }\n }\n\n float intensity = edge * v_effOpacity * v_color.a * sel_factor;\n\n // Subtractive mode (Reusser): output complement color so that\n // reverse-subtract blend (dst - src) on a white bg yields the correct hue.\n vec3 rgb = v_color.rgb;\n if (v_useSubtractive > 0.5) {\n rgb = vec3(1.0) - rgb;\n }\n\n fragColor = vec4(rgb * intensity, intensity);\n}\n`,t=`#version 300 es\n// Point renderer — gl.POINTS with SDF circle anti-aliasing\n//\n// WebGL2 equivalent of point.wgsl. Projects raw ND data to 2D inline using\n// pre-adjusted basis weights and biases (normalization folded into the basis\n// on CPU). Uses gl.POINTS with gl_PointSize for native point expansion —\n// 1 vertex per point instead of 4 (instanced quads in WebGPU).\n//\n// Data is stored in a tiled R32F texture to handle point counts exceeding\n// MAX_TEXTURE_SIZE. Layout: width=texWidth, height=numDims*tileRows.\n// For point idx: col = idx % texWidth, row = d * tileRows + idx / texWidth.\n//\n// Color mapping: instead of a pre-computed N-element color texture, the shader\n// reads raw data values and looks up colors from a tiny LUT at render time.\n\nprecision highp float;\nprecision highp int;\nprecision highp sampler2D;\nprecision highp usampler2D;\n\n// Raw ND data (column-major, tiled).\n// R32F texture: width = texWidth, height = numDims * tileRows.\nuniform sampler2D u_data;\n\n// Adjusted basis weights (normalization folded in).\n// Max 128 dims — for higher, fall back to a basis texture.\nuniform float u_adjBasisX[128];\nuniform float u_adjBasisY[128];\nuniform vec2 u_bias;\n\n// Style\nuniform float u_pointSize; // NDC units\nuniform float u_opacity;\nuniform vec4 u_color; // uniform point color (RGBA 0-1)\nuniform float u_useSubtractive;\nuniform float u_scaleOpacityByZoom; // 1.0 = auto (zoom² density scaling), 0.0 = manual\n\n// Camera\nuniform vec2 u_pan;\nuniform float u_zoom;\nuniform float u_aspect;\nuniform float u_viewportHeight;\nuniform float u_insetOffsetY;\nuniform float u_insetZoom;\n\n// Data dimensions\nuniform int u_numPoints;\nuniform int u_numDims;\n\n// Decimation: 0 = disabled\nuniform int u_maxPoints;\n\n// Tiling: texture width (min(numPoints, MAX_TEXTURE_SIZE))\nuniform int u_texWidth;\n\n// Color mapping — tiny LUT replaces N-element packed color texture\n// 0 = uniform color, 1 = continuous, 2 = categorical, 3 = 2D colormap\nuniform int u_colorMode;\nuniform int u_colorColumnIndex; // which data column to read (continuous only)\nuniform float u_colorMin;\nuniform float u_colorRange;\nuniform int u_colorNumStops; // LUT size\nuniform usampler2D u_colorLutTex; // packed RGBA u32 LUT (width=numStops, height=1)\nuniform usampler2D u_catIndexTex; // categorical indices (R32UI, tiled like data tex)\n\n// 2D colormap params (used when u_colorMode == 3)\nuniform int u_colorColumnIndexV; // second data column\nuniform float u_colorMinV;\nuniform float u_colorRangeV;\nuniform int u_color2dMapIndex; // 0-5 = LUT-based, 6 = oklab_polar\nuniform float u_color2dLut[128]; // SVD rank-1 LUT curves (8 × 16 entries)\n\n// Selection mask (bit-packed u32 in R32UI texture, width = ceil(N/32), height = 1)\nuniform float u_useSelectionMask;\nuniform usampler2D u_selectionTex;\n\nflat out float v_effOpacity;\nflat out vec4 v_color;\nflat out uint v_selected;\nflat out float v_useSubtractive;\nflat out float v_useSelectionMask;\n\n// Unpack RGBA8 from u32 (0xAABBGGRR little-endian)\nvec4 unpackColor(uint packed) {\n float r = float(packed & 0xFFu) / 255.0;\n float g = float((packed >> 8u) & 0xFFu) / 255.0;\n float b = float((packed >> 16u) & 0xFFu) / 255.0;\n float a = float((packed >> 24u) & 0xFFu) / 255.0;\n return vec4(r, g, b, a);\n}\n\n// PCG hash — fast, high-quality deterministic random from vertex index.\n// Used for vertex-shader decimation when numPoints > maxPoints.\nuint pcg_hash(uint v) {\n uint state = v * 747796405u + 2891336453u;\n uint word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;\n return (word >> 22u) ^ word;\n}\n\nfloat random_float(uint seed) {\n return float(pcg_hash(seed)) / 4294967295.0;\n}\n\n// ── 2D colormap helpers ──\n\n// Linearly interpolate within a 16-entry LUT curve starting at base offset.\nfloat lut2d_interp(int base, float t) {\n float pos = t * 15.0;\n int lo = min(int(floor(pos)), 14);\n int hi = lo + 1;\n return mix(u_color2dLut[base + lo], u_color2dLut[base + hi], fract(pos));\n}\n\n// Reconstruct 2D color from SVD rank-1 LUT curves.\n// Layout: R_X[16] R_Y[16] G_X[16] G_Y[16] B_X[16] B_Y[16] B_X2[16] B_Y2[16]\nvec3 colormap2d_lut(float u, float v) {\n float r = lut2d_interp(0, u) * lut2d_interp(16, v);\n float g = lut2d_interp(32, u) * lut2d_interp(48, v);\n float b = lut2d_interp(64, u) * lut2d_interp(80, v)\n + lut2d_interp(96, u) * lut2d_interp(112, v);\n return clamp(vec3(r, g, b), vec3(0.0), vec3(1.0));\n}\n\n// Oklab → linear sRGB conversion\nvec3 oklabToLinear(vec3 lab) {\n float l_ = lab.x + 0.3963377774 * lab.y + 0.2158037573 * lab.z;\n float m_ = lab.x - 0.1055613458 * lab.y - 0.0638541728 * lab.z;\n float s_ = lab.x - 0.0894841775 * lab.y - 1.2914855480 * lab.z;\n float l = l_ * l_ * l_;\n float m = m_ * m_ * m_;\n float s = s_ * s_ * s_;\n return vec3(\n 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,\n -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,\n -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s\n );\n}\n\n// Oklab polar 2D colormap: hue = angle, chroma = radius, lightness varies\nvec3 colormap2d_oklab_polar(float u, float v) {\n float cx = u - 0.5;\n float cy = v - 0.5;\n float radius = min(length(vec2(cx, cy)) * 2.0, 1.0);\n float angle = atan(cy, cx);\n float chroma = radius * 0.25;\n float L = mix(0.3, 0.85, 1.0 - radius);\n float a = chroma * cos(angle);\n float b = chroma * sin(angle);\n vec3 linear = oklabToLinear(vec3(L, a, b));\n return pow(clamp(linear, vec3(0.0), vec3(1.0)), vec3(1.0 / 2.2));\n}\n\nvec3 colormap2d(int mapIndex, float u, float v) {\n if (mapIndex == 6) {\n return colormap2d_oklab_polar(u, v);\n }\n return colormap2d_lut(u, v);\n}\n\nvoid main() {\n int idx = gl_VertexID;\n\n // Vertex-shader decimation: skip expensive projection for excess points.\n if (u_maxPoints > 0 && u_numPoints > u_maxPoints) {\n float threshold = float(u_maxPoints) / float(u_numPoints);\n if (random_float(uint(idx)) > threshold) {\n gl_Position = vec4(0.0, 0.0, 0.0, 0.0);\n gl_PointSize = 0.0;\n v_effOpacity = 0.0;\n v_color = vec4(0.0);\n v_selected = 0u;\n v_useSubtractive = 0.0;\n v_useSelectionMask = 0.0;\n return;\n }\n }\n\n // Tiled texture coordinates: col = idx % texWidth, tileRow = idx / texWidth\n int tileRows = (u_numPoints + u_texWidth - 1) / u_texWidth;\n int tx = idx % u_texWidth;\n int tileRow = idx / u_texWidth;\n\n // Inline ND -> 2D projection with pre-adjusted basis.\n // CPU folds normalization into the basis weights and biases so the\n // hot loop is just: x += raw * adjBasisX[d], y += raw * adjBasisY[d]\n float x = u_bias.x;\n float y = u_bias.y;\n for (int d = 0; d < u_numDims; d++) {\n float raw = texelFetch(u_data, ivec2(tx, d * tileRows + tileRow), 0).r;\n x += raw * u_adjBasisX[d];\n y += raw * u_adjBasisY[d];\n }\n\n // Apply camera: translate then scale, with aspect correction on x.\n float iz = u_insetZoom;\n vec2 center = vec2(\n (x + u_pan.x) * u_zoom * iz / u_aspect,\n (y + u_pan.y) * u_zoom * iz + u_insetOffsetY\n );\n\n gl_Position = vec4(center, 0.0, 1.0);\n\n // Point size: clamp to [2, 32] physical pixels (same as WGSL)\n float min_ndc = 4.0 / u_viewportHeight;\n float max_ndc = 64.0 / u_viewportHeight;\n float ps = clamp(u_pointSize, min_ndc, max_ndc);\n // gl_PointSize is in physical pixels, convert from NDC\n gl_PointSize = ps * u_viewportHeight * 0.5;\n\n // Scale opacity by zoom^2 for constant visual fill density (Reusser).\n // Only applied for auto-computed opacity; manual values are used as-is.\n float z = u_zoom * u_insetZoom;\n float zoomScale = u_scaleOpacityByZoom > 0.5 ? z * z : 1.0;\n v_effOpacity = u_opacity * zoomScale;\n\n // Resolve per-point color from LUT\n if (u_colorMode == 1) {\n // Continuous: read raw value from color column, normalize, interpolate LUT\n float raw = texelFetch(u_data, ivec2(tx, u_colorColumnIndex * tileRows + tileRow), 0).r;\n float invRange = u_colorRange > 0.0 ? 1.0 / u_colorRange : 0.0;\n float t = clamp((raw - u_colorMin) * invRange, 0.0, 1.0);\n float stops = float(u_colorNumStops - 1);\n float pos = t * stops;\n int idx0 = min(int(floor(pos)), u_colorNumStops - 2);\n float frac = pos - float(idx0);\n vec4 c0 = unpackColor(texelFetch(u_colorLutTex, ivec2(idx0, 0), 0).r);\n vec4 c1 = unpackColor(texelFetch(u_colorLutTex, ivec2(idx0 + 1, 0), 0).r);\n v_color = mix(c0, c1, frac);\n } else if (u_colorMode == 2) {\n // Categorical: read category index from tiled texture, direct palette lookup\n uint catIdx = texelFetch(u_catIndexTex, ivec2(tx, tileRow), 0).r;\n int lutIdx = int(catIdx) % u_colorNumStops;\n v_color = unpackColor(texelFetch(u_colorLutTex, ivec2(lutIdx, 0), 0).r);\n } else if (u_colorMode == 3) {\n // 2D colormap: read two columns, normalize to UV, look up from 2D colormap\n float rawU = texelFetch(u_data, ivec2(tx, u_colorColumnIndex * tileRows + tileRow), 0).r;\n float rawV = texelFetch(u_data, ivec2(tx, u_colorColumnIndexV * tileRows + tileRow), 0).r;\n float invRangeU = u_colorRange > 0.0 ? 1.0 / u_colorRange : 0.0;\n float invRangeV = u_colorRangeV > 0.0 ? 1.0 / u_colorRangeV : 0.0;\n float uVal = clamp((rawU - u_colorMin) * invRangeU, 0.0, 1.0);\n float vVal = clamp((rawV - u_colorMinV) * invRangeV, 0.0, 1.0);\n vec3 rgb2d = colormap2d(u_color2dMapIndex, uVal, vVal);\n v_color = vec4(rgb2d, 1.0);\n } else {\n v_color = u_color;\n }\n\n // Read selection bit (not tiled — bit-packed width is small)\n v_selected = 1u;\n if (u_useSelectionMask > 0.5) {\n int word_idx = idx / 32;\n uint word = texelFetch(u_selectionTex, ivec2(word_idx, 0), 0).r;\n v_selected = (word >> uint(idx % 32)) & 1u;\n }\n\n v_useSubtractive = u_useSubtractive;\n v_useSelectionMask = u_useSelectionMask;\n}\n`,n=`#version 300 es\n// Fullscreen tone-map pass — reads HDR float texture and maps to [0,1].\n// Same logic as tonemap.wgsl.\n//\n// Mode 0 (additive): per-channel exponential compression 1-exp(-x).\n// Preserves hue ratios, dense regions desaturate toward white.\n// Mode 1 (normal / subtractive): simple clamp to [0,1].\n\nprecision highp float;\n\nuniform sampler2D u_hdrTexture;\nuniform float u_mode;\n\nlayout(location = 0) out vec4 fragColor;\n\nvoid main() {\n vec4 hdr = texelFetch(u_hdrTexture, ivec2(gl_FragCoord.xy), 0);\n\n vec3 mapped;\n if (u_mode < 0.5) {\n // Additive: exponential compression preserving density structure\n mapped = 1.0 - exp(-hdr.rgb);\n } else {\n // Normal / Subtractive: values already in [0,1], just clamp\n mapped = clamp(hdr.rgb, vec3(0.0), vec3(1.0));\n }\n fragColor = vec4(mapped, 1.0);\n}\n`,r=`#version 300 es\n// Fullscreen tone-map pass — oversized triangle (3 vertices, no vertex buffer).\n// Same geometry as tonemap.wgsl.\n\nvoid main() {\n // Oversized triangle covering the full clip space:\n // vi=0 -> (-1, -1) vi=1 -> (3, -1) vi=2 -> (-1, 3)\n float x = float(gl_VertexID / 2) * 4.0 - 1.0;\n float y = float(gl_VertexID % 2) * 4.0 - 1.0;\n gl_Position = vec4(x, y, 0.0, 1.0);\n}\n`;const i=1e-10,a=e=>{let t=e[0],n=e[1],r=e[2],a=e[3],o=t*t+r*r,s=t*n+r*a,c=n*n+a*a,l=o+c,u=o*c-s*s,d=Math.sqrt(Math.max(0,l*l*.25-u)),f=l*.5+d,p=l*.5-d,m=Math.sqrt(Math.max(0,f)),h=Math.sqrt(Math.max(0,p)),g=new Float32Array([m,h]),_=new Float32Array(4);if(Math.abs(s)>i){let e=f-c,t=Math.sqrt(e*e+s*s);_[0]=e/t,_[1]=s/t;let n=p-c,r=Math.sqrt(n*n+s*s);_[2]=n/r,_[3]=s/r}else _[0]=1,_[1]=0,_[2]=0,_[3]=1;let v=new Float32Array(4);return m>i&&(v[0]=(t*_[0]+n*_[1])/m,v[2]=(r*_[0]+a*_[1])/m),h>i&&(v[1]=(t*_[2]+n*_[3])/h,v[3]=(r*_[2]+a*_[3])/h),m<=i&&h>i?(v[0]=-v[3],v[2]=v[1]):h<=i&&m>i?(v[1]=-v[2],v[3]=v[0]):m<=i&&h<=i&&(v[0]=1,v[1]=0,v[2]=0,v[3]=1),{U:v,S:g,V:_}},o=(e,t)=>{let n=0;for(let r=0;r<t;r++)n+=e[r]*e[r];if(n=Math.sqrt(n),n>1e-10)for(let r=0;r<t;r++)e[r]=e[r]/n;let r=0;for(let n=0;n<t;n++)r+=e[n]*e[t+n];for(let n=0;n<t;n++)e[t+n]=e[t+n]-r*e[n];let i=0;for(let n=0;n<t;n++)i+=e[t+n]*e[t+n];if(i=Math.sqrt(i),i>1e-10)for(let n=0;n<t;n++)e[t+n]=e[t+n]/i},s=(e,t,n,r,i,a,s,c=!0)=>{let l=s*s,u=l*s,d=.5*(-s+2*l-u),f=.5*(2-5*l+3*u),p=.5*(s+4*l-3*u),m=.5*(-l+u);for(let o=0;o<a*2;o++)e[o]=d*t[o]+f*n[o]+p*r[o]+m*i[o];return c&&o(e,a),e},c=(e,t,n)=>{let r=new Float32Array(4);for(let i=0;i<2;i++)for(let a=0;a<2;a++){let o=0;for(let r=0;r<n;r++)o+=e[i*n+r]*t[a*n+r];r[i*2+a]=o}let{S:i}=a(r),o=Math.acos(Math.max(-1,Math.min(1,i[0]))),s=Math.acos(Math.max(-1,Math.min(1,i[1])));return Math.sqrt(o*o+s*s)},l=(e,t,n=!0)=>{let r=e.length,i=new Float32Array(r+1);if(i[0]=0,!n){for(let e=1;e<=r;e++)i[e]=e/r;return i}let a=new Float32Array(t*2),o=new Float32Array(t*2);for(let n=0;n<r;n++){let l=(n-1+r)%r,u=n,d=(n+1)%r,f=(n+2)%r,p=0;s(a,e[l],e[u],e[d],e[f],t,0);for(let n=1;n<=8;n++){let r=n/8;s(o,e[l],e[u],e[d],e[f],t,r),p+=c(a,o,t),a.set(o)}i[n+1]=i[n]+p}let l=i[r];if(l>1e-10)for(let e=1;e<=r;e++)i[e]=i[e]/l;return i},u=(e,t)=>{let n=e.length-1,r=Math.max(0,Math.min(1,t)),i=0,a=n-1;for(;i<a;){let t=i+a>>>1;e[t+1]<=r?i=t+1:a=t}let o=e[i],s=e[i+1]-o,c=s>1e-10?(r-o)/s:0;return{segment:i,localT:c}},d=(e,t,n,r,i,a=!0)=>{let o=t.length,{segment:c,localT:l}=u(n,i),d=(c-1+o)%o,f=c,p=(c+1)%o,m=(c+2)%o;return s(e,t[d],t[f],t[p],t[m],r,l,a)};let f=null,p=[];const m=(e,t)=>{self.postMessage(e,t??[])},h=(e,t,n)=>{let r=e.createShader(t);if(e.shaderSource(r,n),e.compileShader(r),!e.getShaderParameter(r,e.COMPILE_STATUS)){let t=e.getShaderInfoLog(r);throw e.deleteShader(r),Error(`Shader compile error: ${t}`)}return r},g=(e,t,n)=>{let r=h(e,e.VERTEX_SHADER,t),i=h(e,e.FRAGMENT_SHADER,n),a=e.createProgram();if(e.attachShader(a,r),e.attachShader(a,i),e.linkProgram(a),!e.getProgramParameter(a,e.LINK_STATUS)){let t=e.getProgramInfoLog(a);throw e.deleteProgram(a),Error(`Program link error: ${t}`)}return e.deleteShader(r),e.deleteShader(i),a},_=(e,t,n)=>e.getUniformLocation(t,n),v=(e,t,n,r,i,a)=>{if(e===0||t===0||n===0)return{pointSize:.012,opacity:.7};let o=n*r,s=o*o*i/e,c=Math.sqrt(s/Math.PI),l=a*r*.5,u,d;return c>=l?(u=c,d=1):(u=l,d=Math.max(.01,s/(Math.PI*l*l))),{pointSize:2*u/o,opacity:d}},y=(e,t)=>e*f.dpr/t,b=(e,t)=>{let{style:n,numPoints:r,dpr:i}=f;if(n.pointSize!==`auto`&&n.opacity!==`auto`)return{pointSize:y(n.pointSize,t),opacity:n.opacity,color:n.color,scaleOpacityByZoom:!1};let a=v(r,e/i,t/i,i,n.fillTarget,n.minPointSize),o=n.opacity===`auto`;return{pointSize:n.pointSize===`auto`?a.pointSize:y(n.pointSize,t),opacity:o?a.opacity:n.opacity,color:n.color,scaleOpacityByZoom:o}};let x=null;const S=(e,t,n,r)=>{(!x||x.length!==r*2)&&(x=new Float32Array(r*2));let i=0,a=0;for(let o=0;o<r;o++){let s=Math.max(n[o],1e-6),c=e[o],l=e[r+o],u=2/s;x[o]=c*u,x[r+o]=l*u;let d=-t[o]/s;i+=d*c*2,a+=d*l*2}return{weights:x,biasX:i,biasY:a}},C=()=>{if(!f?.normMins||!f.normRanges)return null;let e=f.numDims,t=new Float32Array(e);if(f.centering===`mean`&&f.normMeans)t.set(f.normMeans);else for(let n=0;n<e;n++)t[n]=f.normMins[n]+f.normRanges[n]*.5;return t},w=(e,t,n)=>{if(!f)return;let{hdrFbos:r,hdrTextures:i,hdrSizes:a}=f,o=f.mainView.gl,s=a[e];if(s&&s[0]===t&&s[1]===n)return;r[e]&&o.deleteFramebuffer(r[e]),i[e]&&o.deleteTexture(i[e]);let c=o.createTexture();o.bindTexture(o.TEXTURE_2D,c),o.texImage2D(o.TEXTURE_2D,0,o.RGBA32F,t,n,0,o.RGBA,o.FLOAT,null),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MIN_FILTER,o.NEAREST),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_MAG_FILTER,o.NEAREST),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_S,o.CLAMP_TO_EDGE),o.texParameteri(o.TEXTURE_2D,o.TEXTURE_WRAP_T,o.CLAMP_TO_EDGE);let l=o.createFramebuffer();o.bindFramebuffer(o.FRAMEBUFFER,l),o.framebufferTexture2D(o.FRAMEBUFFER,o.COLOR_ATTACHMENT0,o.TEXTURE_2D,c,0),o.bindFramebuffer(o.FRAMEBUFFER,null),o.bindTexture(o.TEXTURE_2D,null),f.hdrFbos[e]=l,f.hdrTextures[e]=c,f.hdrSizes[e]=[t,n]},T=(e,t)=>{if(!f)return;let n=f.blitSize;if(n&&n[0]===e&&n[1]===t)return;let r=f.mainView.gl;f.blitFbo&&r.deleteFramebuffer(f.blitFbo),f.blitTexture&&r.deleteTexture(f.blitTexture);let i=r.createTexture();r.bindTexture(r.TEXTURE_2D,i),r.texImage2D(r.TEXTURE_2D,0,r.RGBA8,e,t,0,r.RGBA,r.UNSIGNED_BYTE,null),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MIN_FILTER,r.NEAREST),r.texParameteri(r.TEXTURE_2D,r.TEXTURE_MAG_FILTER,r.NEAREST);let a=r.createFramebuffer();r.bindFramebuffer(r.FRAMEBUFFER,a),r.framebufferTexture2D(r.FRAMEBUFFER,r.COLOR_ATTACHMENT0,r.TEXTURE_2D,i,0),r.bindFramebuffer(r.FRAMEBUFFER,null),r.bindTexture(r.TEXTURE_2D,null),f.blitFbo=a,f.blitTexture=i,f.blitSize=[e,t]};let E=null,D=0,O=null,k=!1,A=null;const j=()=>{if(!O||!f)return!0;let e=f.mainView.gl;return e.clientWaitSync(O,0,0)===e.TIMEOUT_EXPIRED?!1:(e.deleteSync(O),O=null,!0)},M=()=>{if(!j())return k=!0,A||=setTimeout(function(){A=null,k&&f&&!E&&M()},2),!1;if(k=!1,U(),f){let e=f.mainView.gl;O=e.fenceSync(e.SYNC_GPU_COMMANDS_COMPLETE,0),e.flush()}return!0},N=e=>{if(!E||!f?.tour)return;let t=E.prevTime===null?void 0:e-E.prevTime;if(t!==void 0){let e=t/1e3*E.speed*E.direction/20,n=f.tour.position+e;n-=Math.floor(n),f.tour.position=n}E.prevTime=e,f.directBasis=null,M()&&(E.lastRenderedRafTime!==null&&E.frameTimes.push(e-E.lastRenderedRafTime),E.lastRenderedRafTime=e),e-D>=33&&(m({type:`playbackTick`,position:f.tour.position,frameMs:t}),D=e),E.rafId=requestAnimationFrame(N)},P=(e,t)=>{E?(E.speed=e,E.direction=t):(O=null,k=!1,E={speed:e,direction:t,prevTime:null,rafId:0,frameTimes:[],lastRenderedRafTime:null},E.rafId=requestAnimationFrame(N))},ee=()=>{if(E){cancelAnimationFrame(E.rafId),f?.tour&&m({type:`playbackTick`,position:f.tour.position});let e=Float64Array.from(E.frameTimes);m({type:`playbackStopped`,frameTimes:e},[e.buffer]),E=null}},F=(e,t,n,r,i,a)=>{if(!f?.dataTexture||!f.normRanges)return;let{numPoints:o,numDims:s,pointProgram:c,pointLocs:l,tonemapProgram:u,tonemapLocs:d}=f,p=f.mainView.gl,m=C();if(!m)return;let{weights:h,biasX:g,biasY:_}=S(e,m,f.normRanges,s),v=b(t,n),y=f.backgroundColor,x=.2126*y[0]+.7152*y[1]+.0722*y[2],T=f.colorMode>0,E=!T&&x>.5,D;D=T||E?1:0;let O=t/n||1;if(w(r,t,n),p.bindFramebuffer(p.FRAMEBUFFER,f.hdrFbos[r]),p.viewport(0,0,t,n),p.clearColor(y[0],y[1],y[2],1),p.clear(p.COLOR_BUFFER_BIT),o>0){if(p.useProgram(c),p.enable(p.BLEND),T?(p.blendEquation(p.FUNC_ADD),p.blendFunc(p.ONE,p.ONE_MINUS_SRC_ALPHA)):E?(p.blendEquationSeparate(p.FUNC_REVERSE_SUBTRACT,p.FUNC_ADD),p.blendFunc(p.ONE,p.ONE)):(p.blendEquation(p.FUNC_ADD),p.blendFunc(p.ONE,p.ONE)),p.activeTexture(p.TEXTURE0),p.bindTexture(p.TEXTURE_2D,f.dataTexture),p.uniform1i(l.u_data,0),p.activeTexture(p.TEXTURE1),f.colorLutTexture&&p.bindTexture(p.TEXTURE_2D,f.colorLutTexture),p.uniform1i(l.u_colorLutTex,1),p.activeTexture(p.TEXTURE2),f.selectionTexture&&p.bindTexture(p.TEXTURE_2D,f.selectionTexture),p.uniform1i(l.u_selectionTex,2),p.activeTexture(p.TEXTURE3),f.colorMode===2&&f.activeCatColumnName){let e=f.catIndexTextures.get(f.activeCatColumnName);e&&p.bindTexture(p.TEXTURE_2D,e)}p.uniform1i(l.u_catIndexTex,3);let e=new Float32Array(128),t=new Float32Array(128);for(let n=0;n<s&&n<128;n++)e[n]=h[n],t[n]=h[s+n];p.uniform1fv(l.u_adjBasisX,e),p.uniform1fv(l.u_adjBasisY,t),p.uniform2f(l.u_bias,g,_),p.uniform1f(l.u_pointSize,v.pointSize),p.uniform1f(l.u_opacity,v.opacity),p.uniform4f(l.u_color,v.color[0],v.color[1],v.color[2],1),p.uniform1f(l.u_useSubtractive,E?1:0),p.uniform1f(l.u_scaleOpacityByZoom,v.scaleOpacityByZoom?1:0),p.uniform2f(l.u_pan,i.panX,i.panY),p.uniform1f(l.u_zoom,i.zoom),p.uniform1f(l.u_aspect,O),p.uniform1f(l.u_viewportHeight,n),p.uniform1f(l.u_insetOffsetY,i.insetOffsetY),p.uniform1f(l.u_insetZoom,i.insetZoom),p.uniform1i(l.u_numPoints,o),p.uniform1i(l.u_numDims,s),p.uniform1i(l.u_maxPoints,f.maxPoints),p.uniform1i(l.u_texWidth,f.texWidth),p.uniform1i(l.u_colorMode,f.colorMode),p.uniform1i(l.u_colorColumnIndex,f.colorColumnIndex),p.uniform1f(l.u_colorMin,f.colorMin),p.uniform1f(l.u_colorRange,f.colorRange),p.uniform1i(l.u_colorNumStops,f.colorNumStops),p.uniform1i(l.u_colorColumnIndexV,f.colorColumnIndexV),p.uniform1f(l.u_colorMinV,f.colorMinV),p.uniform1f(l.u_colorRangeV,f.colorRangeV),p.uniform1i(l.u_color2dMapIndex,f.color2dMapIndex),p.uniform1fv(l.u_color2dLut,f.color2dLut),p.uniform1f(l.u_useSelectionMask,f.styleFlags.useSelectionMask?1:0),p.drawArrays(p.POINTS,0,o),p.disable(p.BLEND)}p.bindFramebuffer(p.FRAMEBUFFER,a??null),p.viewport(0,0,t,n),p.clearColor(0,0,0,1),p.clear(p.COLOR_BUFFER_BIT),p.useProgram(u),p.activeTexture(p.TEXTURE0),p.bindTexture(p.TEXTURE_2D,f.hdrTextures[r]),p.uniform1i(d.u_hdrTexture,0),p.uniform1f(d.u_mode,D),p.drawArrays(p.TRIANGLES,0,3),p.finish()};let I=!1,L=null,R=null;const z=(e,t)=>{if(!f?.dataTexture)return;let n=f.previewCanvases.get(e);if(!n){m({type:`rendered`,viewIndex:e});return}let r=n.canvas.width,i=n.canvas.height;if(r===0||i===0){m({type:`rendered`,viewIndex:e});return}let a=f.mainView.gl;T(r,i),F(t,r,i,1,{panX:0,panY:0,zoom:1,aspect:1,viewportHeight:i,insetOffsetY:0,insetZoom:1},f.blitFbo);let o=r*i*4,s=r*4;(!L||L.length<o)&&(L=new Uint8Array(o)),(!R||R.length<s)&&(R=new Uint8Array(s)),a.bindFramebuffer(a.FRAMEBUFFER,f.blitFbo),a.readPixels(0,0,r,i,a.RGBA,a.UNSIGNED_BYTE,L),a.bindFramebuffer(a.FRAMEBUFFER,null);for(let e=0;e<i>>1;e++){let t=e*s,n=(i-1-e)*s;R.set(L.subarray(t,t+s)),L.copyWithin(t,n,n+s),L.set(R,n)}let c=new ImageData(new Uint8ClampedArray(L.buffer,0,o),r,i);n.ctx2d.putImageData(c,0,0),m({type:`rendered`,viewIndex:e})},B=()=>{if(!f?.dataTexture||!f.tour)return;let{tour:e}=f;for(let[t]of f.previewCanvases)t>=e.bases.length||z(t,e.bases[t])},V=()=>{I||(I=!0,setTimeout(()=>{I=!1,B()},0))},H=()=>{f?.dataTexture&&(U(),V())},U=()=>{if(!f?.dataTexture)return;let{canvas:e}=f.mainView;if(f.directBasis){F(f.directBasis,e.width,e.height,0,f.camera),m({type:`rendered`,viewIndex:0});return}if(!f.tour)return;let{tour:t}=f;d(t.interpolatedBasis,t.bases,t.arcLengths,t.dims,t.position,t.orthonormalize),F(t.interpolatedBasis,e.width,e.height,0,f.camera),m({type:`rendered`,viewIndex:0})},W=()=>{f&&(f.tour||f.directBasis)&&f.dataTexture&&H()},G=()=>{f&&(f.styleFlags.useSelectionMask=!0,(f.tour||f.directBasis)&&f.dataTexture&&H())};let K=0;const q=e=>{if(!f)return;let t=f.mainView.gl;if(e.data.type===`setColorContinuous`){if(e.data.dataVersion!==K)return;let{columnIndex:n,min:r,range:i,colormap:a}=e.data;f.colorLutTexture&&t.deleteTexture(f.colorLutTexture),f.colorLutTexture=J(t,a),f.colorMode=1,f.colorColumnIndex=n,f.colorMin=r,f.colorRange=i,f.colorNumStops=a.length,f.activeCatColumnName=null,W();return}if(e.data.type===`setColorCategorical`){if(e.data.dataVersion!==K)return;let{catColumnName:n,palette:r}=e.data;if(!f.catIndexTextures.has(n))return;f.colorLutTexture&&t.deleteTexture(f.colorLutTexture),f.colorLutTexture=J(t,r),f.colorMode=2,f.colorNumStops=r.length,f.activeCatColumnName=n,W();return}if(e.data.type===`setColor2D`){if(e.data.dataVersion!==K)return;let{columnIndexX:t,columnIndexY:n,minX:r,rangeX:i,minY:a,rangeY:o,lut:s,mapIndex:c}=e.data;f.colorMode=3,f.colorColumnIndex=t,f.colorMin=r,f.colorRange=i,f.colorColumnIndexV=n,f.colorMinV=a,f.colorRangeV=o,f.color2dMapIndex=c,f.activeCatColumnName=null,s&&f.color2dLut.set(s.subarray(0,128)),W();return}if(e.data.type===`selectContinuous`){if(e.data.dataVersion!==K)return;let{columnIndex:n,ranges:r}=e.data,{numPoints:i,dataBuffers:a}=f,o=a[n];if(!o)return;let s=new Uint32Array(Math.ceil(i/32)),c=r.length/2;for(let e=0;e<i;e++){let t=o[e],n=!1;for(let e=0;e<c;e++)if(t>=r[e*2]&&t<=r[e*2+1]){n=!0;break}n&&(s[e>>5]=s[e>>5]|0|1<<(e&31))}X(t,s),G();return}if(e.data.type===`selectCategorical`){if(e.data.dataVersion!==K)return;let{catColumnName:n,selectedLabels:r}=e.data,i=f.categoricalBuffers.get(n);if(!i)return;let{numPoints:a}=f,o=new Uint32Array(Math.ceil(a/32));for(let e=0;e<a;e++){let t=i[e];t<r.length&&r[t]>0&&(o[e>>5]=o[e>>5]|0|1<<(e&31))}X(t,o),G();return}if(e.data.type!==`data`)return;let{dataVersion:n,dims:r,rows:i,buffers:a,mins:o,ranges:s,means:c,categoricalColumns:u}=e.data;if(K=n,r<2||i===0||a.length<2){m({type:`error`,message:`Need at least 2 dimensions to render`});return}f.dataTexture&&(t.deleteTexture(f.dataTexture),f.dataTexture=null),f.colorLutTexture&&(t.deleteTexture(f.colorLutTexture),f.colorLutTexture=null);for(let e of f.catIndexTextures.values())t.deleteTexture(e);f.catIndexTextures.clear(),f.selectionTexture&&(t.deleteTexture(f.selectionTexture),f.selectionTexture=null),f.styleFlags={useSelectionMask:!1},f.colorMode=0,f.colorColumnIndex=0,f.colorMin=0,f.colorRange=1,f.colorNumStops=0,f.activeCatColumnName=null,f.categoricalBuffers.clear(),f.dataBuffers=a,f.normMins=new Float32Array(o),f.normRanges=new Float32Array(s),f.normMeans=new Float32Array(c),f.numPoints=i,f.numDims=r;let d=t.getParameter(t.MAX_TEXTURE_SIZE),p=Math.min(i,d),h=Math.ceil(i/p);f.texWidth=p;let g=t.createTexture();t.bindTexture(t.TEXTURE_2D,g);let _=r*h,v=new Float32Array(p*_);for(let e=0;e<r;e++){let t=a[e],n=e*h;for(let e=0;e<i;e++){let r=e%p,i=n+Math.floor(e/p);v[i*p+r]=t[e]}}t.texImage2D(t.TEXTURE_2D,0,t.R32F,p,_,0,t.RED,t.FLOAT,v),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MIN_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_MAG_FILTER,t.NEAREST),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_S,t.CLAMP_TO_EDGE),t.texParameteri(t.TEXTURE_2D,t.TEXTURE_WRAP_T,t.CLAMP_TO_EDGE),t.bindTexture(t.TEXTURE_2D,null),f.dataTexture=g;for(let e of u)f.categoricalBuffers.set(e.name,e.indices),f.catIndexTextures.set(e.name,Y(t,e.indices,i,p));if(f.tour&&f.tour.dims===r)H();else{let e=Math.max(1,f.previewCanvases.size),t=[];for(let n=0;n<e;n++){let i=new Float32Array(r*2),a=Math.floor(n/e*r);i[a]=1,i[r+(a+1)%r]=1,t.push(i)}f.tour={bases:t,arcLengths:l(t,r),dims:r,orthonormalize:!0,position:0,interpolatedBasis:new Float32Array(r*2)},H()}},J=(e,t)=>{let n=e.createTexture();return e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.R32UI,t.length,1,0,e.RED_INTEGER,e.UNSIGNED_INT,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),n},Y=(e,t,n,r)=>{let i=Math.ceil(n/r),a;if(i===1)a=t;else{a=new Uint32Array(r*i);for(let e=0;e<n;e++){let n=e%r,i=Math.floor(e/r);a[i*r+n]=t[e]}}let o=e.createTexture();return e.bindTexture(e.TEXTURE_2D,o),e.texImage2D(e.TEXTURE_2D,0,e.R32UI,r,i,0,e.RED_INTEGER,e.UNSIGNED_INT,a),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),o},X=(e,t)=>{if(!f)return;f.selectionTexture&&e.deleteTexture(f.selectionTexture);let n=e.createTexture();e.bindTexture(e.TEXTURE_2D,n),e.texImage2D(e.TEXTURE_2D,0,e.R32UI,t.length,1,0,e.RED_INTEGER,e.UNSIGNED_INT,t),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MIN_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_MAG_FILTER,e.NEAREST),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_S,e.CLAMP_TO_EDGE),e.texParameteri(e.TEXTURE_2D,e.TEXTURE_WRAP_T,e.CLAMP_TO_EDGE),e.bindTexture(e.TEXTURE_2D,null),f.selectionTexture=n},Z=(e,t,n,r)=>{let i=!1;for(let a=0,o=r-1;a<r;o=a++){let r=n[a*2],s=n[a*2+1],c=n[o*2],l=n[o*2+1];s>t!=l>t&&e<(c-r)*(t-s)/(l-s)+r&&(i=!i)}return i},Q=e=>{if(!f?.normRanges||f.numPoints===0)return;let{numPoints:t,numDims:n,dataBuffers:r,camera:i}=f,a=e.length/2;if(a<3)return;let o=null;if(f.directBasis)o=f.directBasis;else if(f.tour){let{tour:e}=f;d(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),o=e.interpolatedBasis}if(!o)return;let s=C();if(!s)return;let{weights:c,biasX:l,biasY:u}=S(o,s,f.normRanges,n),p=f.mainView.canvas,h=p.width/p.height||1,g=i.insetZoom,_=i.zoom*g,v=new Float32Array(e.length);for(let t=0;t<a;t++)v[t*2]=e[t*2]*h/_-i.panX,v[t*2+1]=(e[t*2+1]-i.insetOffsetY)/_-i.panY;let y=new Uint32Array(Math.ceil(t/32));for(let e=0;e<t;e++){let t=l,i=u;for(let a=0;a<n;a++){let o=r[a][e];t+=o*c[a],i+=o*c[n+a]}Z(t,i,v,a)&&(y[e>>5]=y[e>>5]|0|1<<(e&31))}let b=f.mainView.gl;X(b,y),G();let x=new Uint32Array(y);m({type:`selectionResult`,mask:x},[x.buffer])},te=()=>{if(!f)return 0;let e=0;if(f.dataTexture){let t=Math.ceil(f.numPoints/f.texWidth);e+=f.texWidth*f.numDims*t*4}for(let t=0;t<f.hdrSizes.length;t++){let n=f.hdrSizes[t];n&&(e+=n[0]*n[1]*16)}f.blitSize&&(e+=f.blitSize[0]*f.blitSize[1]*4),f.colorLutTexture&&(e+=f.colorNumStops*4);for(let t of f.categoricalBuffers.values())e+=t.byteLength;return f.selectionTexture&&(e+=Math.ceil(f.numPoints/32)*4),e},ne=()=>{if(!f)return;let e=0;for(let t of f.dataBuffers)e+=t.byteLength;for(let t of f.categoricalBuffers.values())e+=t.byteLength;if(f.normMins&&(e+=f.normMins.byteLength),f.normRanges&&(e+=f.normRanges.byteLength),f.tour){for(let t of f.tour.bases)e+=t.byteLength;e+=f.tour.interpolatedBasis.byteLength}m({type:`metricsResult`,gpuMemoryBytes:te(),numPoints:f.numPoints,numDims:f.numDims,workerJsHeapBytes:e})},re=e=>{if(!f?.tour||!f.dataTexture||f.numPoints===0){m({type:`error`,message:`No data/tour loaded for benchmark`});return}let t=f.mainView.gl,{numPoints:n,numDims:r,tour:i}=f,a=i.position;f.directBasis=null;let o=new Uint8Array(4),s=()=>{t.readPixels(0,0,1,1,t.RGBA,t.UNSIGNED_BYTE,o)};for(let e=0;e<5;e++)i.position=e/5,d(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize),F(i.interpolatedBasis,f.mainView.canvas.width,f.mainView.canvas.height,0,f.camera),s();let c=new Float64Array(e);for(let t=0;t<e;t++){i.position=t/e,d(i.interpolatedBasis,i.bases,i.arcLengths,i.dims,i.position,i.orthonormalize);let n=performance.now();F(i.interpolatedBasis,f.mainView.canvas.width,f.mainView.canvas.height,0,f.camera),s(),c[t]=performance.now()-n}i.position=a,m({type:`benchmarkResult`,frameTimes:c,numPoints:n,numDims:r},[c.buffer])},$=e=>{if(f){if(e.type===`setBases`){let{bases:t}=e;if(t.length===0)return;let n=t[0].length/2,r=e.tourFamily!==`sequential`;f.tour={bases:t,arcLengths:l(t,n,r),dims:n,orthonormalize:r,position:f.tour?.position??0,interpolatedBasis:new Float32Array(n*2)},f.dataTexture&&H();return}if(e.type===`addPreviewCanvas`){let t=e.canvas.getContext(`2d`);if(!t)return;f.previewCanvases.set(e.id,{canvas:e.canvas,ctx2d:t}),f.dataTexture&&f.tour&&e.id<f.tour.bases.length&&z(e.id,f.tour.bases[e.id]);return}if(e.type===`removePreviewCanvas`){f.previewCanvases.delete(e.id);return}if(e.type===`resizePreview`){let t=f.previewCanvases.get(e.id);if(!t)return;t.canvas.width=e.width,t.canvas.height=e.height,f.dataTexture&&f.tour&&e.id<f.tour.bases.length&&V();return}if(e.type===`setTourPosition`){if(!f.tour)return;f.tour.position=e.position,f.directBasis=null,M();return}if(e.type===`setStyle`){f.style={pointSize:e.pointSize,opacity:e.opacity,color:e.color,minPointSize:e.minPointSize??f.style.minPointSize,fillTarget:e.fillTarget??f.style.fillTarget},(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setCamera`){f.camera={panX:e.pan[0],panY:e.pan[1],zoom:e.zoom,aspect:f.camera.aspect,viewportHeight:f.camera.viewportHeight,insetOffsetY:e.insetOffsetY,insetZoom:e.insetZoom},U();return}if(e.type===`setCentering`){f.centering=e.centering,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`resize`){e.dpr!==void 0&&(f.dpr=e.dpr),e.viewIndex===0&&(f.mainView.canvas.width=e.width,f.mainView.canvas.height=e.height,(f.tour||f.directBasis)&&f.dataTexture&&U());return}if(e.type===`render`){(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setDirectBasis`){if(!f.dataTexture)return;f.directBasis=e.basis,F(e.basis,f.mainView.canvas.width,f.mainView.canvas.height,0,f.camera),m({type:`rendered`,viewIndex:0});return}if(e.type===`clearColors`){f.colorLutTexture&&(f.mainView.gl.deleteTexture(f.colorLutTexture),f.colorLutTexture=null),f.colorMode=0,f.colorColumnIndex=0,f.colorMin=0,f.colorRange=1,f.colorNumStops=0,f.activeCatColumnName=null,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setBackgroundColor`){f.backgroundColor=e.color,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setMaxPoints`){f.maxPoints=e.maxPoints,(f.tour||f.directBasis)&&f.dataTexture&&H();return}if(e.type===`setSelectionMask`){let t=f.mainView.gl;X(t,e.mask),f.styleFlags.useSelectionMask=!0,(f.tour||f.directBasis)&&f.dataTexture&&H();let n=new Uint32Array(e.mask);m({type:`selectionResult`,mask:n},[n.buffer]);return}if(e.type===`clearSelectionMask`){f.selectionTexture&&(f.mainView.gl.deleteTexture(f.selectionTexture),f.selectionTexture=null),f.styleFlags.useSelectionMask=!1,(f.tour||f.directBasis)&&f.dataTexture&&H(),m({type:`selectionResult`,mask:new Uint32Array});return}if(e.type===`startPlayback`){if(!f.tour)return;P(e.speed,e.direction);return}if(e.type===`stopPlayback`){ee();return}if(e.type===`computePCA`){m({type:`error`,message:`PCA not yet implemented in WebGL backend`});return}if(e.type===`lassoSelect`){Q(e.polygon);return}if(e.type===`getProjectedPositions`){if(!f?.normRanges||f.numPoints===0){m({type:`projectedPositions`,positions:new Float32Array},[]);return}let{numPoints:e,numDims:t,dataBuffers:n}=f,r=null;if(f.directBasis)r=f.directBasis;else if(f.tour){let{tour:e}=f;d(e.interpolatedBasis,e.bases,e.arcLengths,e.dims,e.position,e.orthonormalize),r=e.interpolatedBasis}if(!r){m({type:`projectedPositions`,positions:new Float32Array},[]);return}let i=C();if(!i){m({type:`projectedPositions`,positions:new Float32Array},[]);return}let{weights:a,biasX:o,biasY:s}=S(r,i,f.normRanges,t),c=new Float32Array(e*2);for(let r=0;r<e;r++){let e=o,i=s;for(let o=0;o<t;o++){let s=n[o][r];e+=s*a[o],i+=s*a[t+o]}c[r*2]=e,c[r*2+1]=i}m({type:`projectedPositions`,positions:c},[c.buffer]);return}if(e.type===`getPointData`){if(!f||f.numPoints===0){m({type:`pointData`,pointIndex:e.pointIndex,numericValues:{},categoricalValues:{}});return}let{numDims:t,dataBuffers:n}=f,r=e.pointIndex;if(r<0||r>=f.numPoints){m({type:`pointData`,pointIndex:r,numericValues:{},categoricalValues:{}});return}let i={};for(let e=0;e<t;e++)i[String(e)]=n[e][r];let a={};for(let[e,t]of f.categoricalBuffers)a[e]=t[r];m({type:`pointData`,pointIndex:r,numericValues:i,categoricalValues:a});return}if(e.type===`benchmark`){re(e.numFrames);return}if(e.type===`getMetrics`){ne();return}}};self.onmessage=i=>{let a=i.data;if(a.type===`init`){try{let i=a.canvas,o=i.getContext(`webgl2`,{alpha:!0,premultipliedAlpha:!0,antialias:!1,preserveDrawingBuffer:!1});if(!o)throw Error(`WebGL2 not supported`);o.getExtension(`EXT_color_buffer_float`)||console.warn(`EXT_color_buffer_float not available — HDR rendering may fail`),o.getExtension(`OES_texture_float_linear`),o.getExtension(`EXT_float_blend`);let s=o,c=g(s,t,e),l={u_data:_(s,c,`u_data`),u_adjBasisX:_(s,c,`u_adjBasisX`),u_adjBasisY:_(s,c,`u_adjBasisY`),u_bias:_(s,c,`u_bias`),u_pointSize:_(s,c,`u_pointSize`),u_opacity:_(s,c,`u_opacity`),u_color:_(s,c,`u_color`),u_useSubtractive:_(s,c,`u_useSubtractive`),u_pan:_(s,c,`u_pan`),u_zoom:_(s,c,`u_zoom`),u_aspect:_(s,c,`u_aspect`),u_viewportHeight:_(s,c,`u_viewportHeight`),u_insetOffsetY:_(s,c,`u_insetOffsetY`),u_insetZoom:_(s,c,`u_insetZoom`),u_numPoints:_(s,c,`u_numPoints`),u_numDims:_(s,c,`u_numDims`),u_maxPoints:_(s,c,`u_maxPoints`),u_texWidth:_(s,c,`u_texWidth`),u_colorMode:_(s,c,`u_colorMode`),u_colorColumnIndex:_(s,c,`u_colorColumnIndex`),u_colorMin:_(s,c,`u_colorMin`),u_colorRange:_(s,c,`u_colorRange`),u_colorNumStops:_(s,c,`u_colorNumStops`),u_colorLutTex:_(s,c,`u_colorLutTex`),u_catIndexTex:_(s,c,`u_catIndexTex`),u_useSelectionMask:_(s,c,`u_useSelectionMask`),u_selectionTex:_(s,c,`u_selectionTex`),u_colorColumnIndexV:_(s,c,`u_colorColumnIndexV`),u_colorMinV:_(s,c,`u_colorMinV`),u_colorRangeV:_(s,c,`u_colorRangeV`),u_color2dMapIndex:_(s,c,`u_color2dMapIndex`),u_color2dLut:_(s,c,`u_color2dLut`),u_scaleOpacityByZoom:_(s,c,`u_scaleOpacityByZoom`)},u=g(s,r,n),d={u_hdrTexture:_(s,u,`u_hdrTexture`),u_mode:_(s,u,`u_mode`)};f={mainView:{canvas:i,gl:o},previewCanvases:new Map,pointProgram:c,pointLocs:l,tonemapProgram:u,tonemapLocs:d,hdrFbos:[],hdrTextures:[],hdrSizes:[],blitFbo:null,blitTexture:null,blitSize:null,dataTexture:null,colorLutTexture:null,catIndexTextures:new Map,selectionTexture:null,numPoints:0,numDims:0,dataBuffers:[],normMins:null,normRanges:null,normMeans:null,centering:`midrange`,categoricalBuffers:new Map,tour:null,style:{pointSize:`auto`,opacity:`auto`,color:[.25,.5,.9],minPointSize:2,fillTarget:.5},styleFlags:{useSelectionMask:!1},colorMode:0,colorColumnIndex:0,colorMin:0,colorRange:1,colorNumStops:0,activeCatColumnName:null,colorColumnIndexV:0,colorMinV:0,colorRangeV:1,color2dMapIndex:0,color2dLut:new Float32Array(128),camera:{panX:0,panY:0,zoom:a.zoom,aspect:1,viewportHeight:1,insetOffsetY:0,insetZoom:1},directBasis:null,backgroundColor:[0,0,0],dpr:a.dpr,maxPoints:0,texWidth:0},a.dataPort.onmessage=q;let h=p;if(p=null,h)for(let e of h)$(e);m({type:`ready`})}catch(e){m({type:`error`,message:e instanceof Error?e.message:String(e)})}return}if(p){p.push(a);return}$(a)};", M = typeof self < "u" && self.Blob && new Blob(["URL.revokeObjectURL(import.meta.url);", j], { type: "text/javascript;charset=utf-8" });
3027
3027
  function N(e) {
3028
3028
  let t;
3029
3029
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dtour/scatter",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "type": "module",
5
5
  "main": "./dist/scatter.js",
6
6
  "module": "./dist/scatter.js",