d3-force-webgpu
Version:
GPU-accelerated force-directed graph layout with adaptive CPU/GPU selection. Drop-in replacement for d3-force with WebGPU support.
3 lines (2 loc) • 26.6 kB
JavaScript
// https://github.com/jamescarruthers/d3-force-webgpu v1.0.2 Copyright 2010-2021 Mike Bostock
!function(e,n){"object"==typeof exports&&"undefined"!=typeof module?n(exports,require("d3-dispatch"),require("d3-timer"),require("d3-quadtree")):"function"==typeof define&&define.amd?define(["exports","d3-dispatch","d3-timer","d3-quadtree"],n):n((e="undefined"!=typeof globalThis?globalThis:e||self).d3=e.d3||{},e.d3,e.d3,e.d3)}(this,(function(e,n,t,r){"use strict";const i=4294967296;function o(){let e=1;return()=>(e=(1664525*e+1013904223)%i)/i}function a(e){return e.x}function u(e){return e.y}var s=Math.PI*(3-Math.sqrt(5));function f(e){var r,i=1,a=.001,u=1-Math.pow(a,1/300),f=0,c=.6,l=new Map,d=t.timer(p),h=n.dispatch("tick","end"),g=o();function p(){y(),h.call("tick",r),i<a&&(d.stop(),h.call("end",r))}function y(n){var t,o,a=e.length;void 0===n&&(n=1);for(var s=0;s<n;++s)for(i+=(f-i)*u,l.forEach((function(e){e(i)})),t=0;t<a;++t)null==(o=e[t]).fx?o.x+=o.vx*=c:(o.x=o.fx,o.vx=0),null==o.fy?o.y+=o.vy*=c:(o.y=o.fy,o.vy=0);return r}function v(){for(var n,t=0,r=e.length;t<r;++t){if((n=e[t]).index=t,null!=n.fx&&(n.x=n.fx),null!=n.fy&&(n.y=n.fy),isNaN(n.x)||isNaN(n.y)){var i=10*Math.sqrt(.5+t),o=t*s;n.x=i*Math.cos(o),n.y=i*Math.sin(o)}(isNaN(n.vx)||isNaN(n.vy))&&(n.vx=n.vy=0)}}function m(n){return n.initialize&&n.initialize(e,g),n}return null==e&&(e=[]),v(),r={tick:y,restart:function(){return d.restart(p),r},stop:function(){return d.stop(),r},nodes:function(n){return arguments.length?(e=n,v(),l.forEach(m),r):e},alpha:function(e){return arguments.length?(i=+e,r):i},alphaMin:function(e){return arguments.length?(a=+e,r):a},alphaDecay:function(e){return arguments.length?(u=+e,r):+u},alphaTarget:function(e){return arguments.length?(f=+e,r):f},velocityDecay:function(e){return arguments.length?(c=1-e,r):1-c},randomSource:function(e){return arguments.length?(g=e,l.forEach(m),r):g},force:function(e,n){return arguments.length>1?(null==n?l.delete(e):l.set(e,m(n)),r):l.get(e)},find:function(n,t,r){var i,o,a,u,s,f=0,c=e.length;for(null==r?r=1/0:r*=r,f=0;f<c;++f)(a=(i=n-(u=e[f]).x)*i+(o=t-u.y)*o)<r&&(s=u,r=a);return s},on:function(e,n){return arguments.length>1?(h.on(e,n),r):h.on(e)}}}class c{constructor(){this.device=null,this.adapter=null}async initialize(){if(!navigator.gpu)throw new Error("WebGPU is not supported in this browser");if(this.adapter=await navigator.gpu.requestAdapter({powerPreference:"high-performance"}),!this.adapter)throw new Error("Failed to get WebGPU adapter");return this.device=await this.adapter.requestDevice({requiredFeatures:[],requiredLimits:{maxStorageBufferBindingSize:this.adapter.limits.maxStorageBufferBindingSize,maxComputeWorkgroupStorageSize:this.adapter.limits.maxComputeWorkgroupStorageSize,maxComputeInvocationsPerWorkgroup:this.adapter.limits.maxComputeInvocationsPerWorkgroup}}),this.device.lost.then((e=>{console.error("WebGPU device was lost:",e.message),"destroyed"!==e.reason&&this.initialize()})),this.device}destroy(){this.device&&(this.device.destroy(),this.device=null),this.adapter=null}}let l=null;class d{constructor(e){this.device=e,this.buffers=new Map}createNodeBuffer(e){const n=e.length,t=8*n*4,r=new Float32Array(8*n);for(let t=0;t<n;t++){const n=e[t],i=8*t;r[i+0]=n.x||0,r[i+1]=n.y||0,r[i+2]=n.vx||0,r[i+3]=n.vy||0,r[i+4]=null!==n.fx&&void 0!==n.fx?n.fx:NaN,r[i+5]=null!==n.fy&&void 0!==n.fy?n.fy:NaN,r[i+6]=t,r[i+7]=0}const i=this.device.createBuffer({label:"Node Buffer",size:t,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_SRC|GPUBufferUsage.COPY_DST,mappedAtCreation:!0});return new Float32Array(i.getMappedRange()).set(r),i.unmap(),this.buffers.set("nodes",{buffer:i,count:n,floatsPerNode:8}),i}createSimulationParamsBuffer(e){const n=new Float32Array([e.alpha||1,e.alphaDecay||.0228,e.alphaTarget||0,e.velocityDecay||.6,e.nodeCount||0,0,0,0]),t=this.device.createBuffer({label:"Simulation Parameters",size:n.byteLength,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST,mappedAtCreation:!0});return new Float32Array(t.getMappedRange()).set(n),t.unmap(),this.buffers.set("simulationParams",{buffer:t}),t}createReadbackBuffer(e){const n=this.device.createBuffer({label:"Readback Buffer",size:e,usage:GPUBufferUsage.MAP_READ|GPUBufferUsage.COPY_DST});return this.buffers.set("readback",{buffer:n,size:e}),n}async readNodeData(e){const n=this.buffers.get("nodes");if(!n)return;const t=this.buffers.get("readback");(!t||t.size<n.buffer.size)&&this.createReadbackBuffer(n.buffer.size);const r=this.device.createCommandEncoder();r.copyBufferToBuffer(n.buffer,0,this.buffers.get("readback").buffer,0,n.buffer.size),this.device.queue.submit([r.finish()]),await this.buffers.get("readback").buffer.mapAsync(GPUMapMode.READ);const i=new Float32Array(this.buffers.get("readback").buffer.getMappedRange());for(let t=0;t<n.count;t++){const r=t*n.floatsPerNode,o=e[t];o.x=i[r+0],o.y=i[r+1],o.vx=i[r+2],o.vy=i[r+3]}this.buffers.get("readback").buffer.unmap()}updateSimulationParams(e){const n=new Float32Array([e.alpha,e.alphaDecay,e.alphaTarget,e.velocityDecay,e.nodeCount,0,0,0]);this.device.queue.writeBuffer(this.buffers.get("simulationParams").buffer,0,n)}destroy(){for(const[,e]of this.buffers)e.buffer.destroy();this.buffers.clear()}}class h{constructor(e){this.device=e,this.shaderModules=new Map,this.pipelines=new Map,this.bindGroupLayouts=new Map}createSimulationTickPipeline(){const e=this.device.createShaderModule({label:"Simulation Tick Shader",code:"\nstruct Node {\n position: vec2<f32>,\n velocity: vec2<f32>,\n fixedPosition: vec2<f32>,\n index: f32,\n _padding: f32,\n}\n\nstruct SimulationParams {\n alpha: f32,\n alphaDecay: f32,\n alphaTarget: f32,\n velocityDecay: f32,\n nodeCount: f32,\n _padding: vec3<f32>,\n}\n\n@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;\n@group(0) @binding(1) var<uniform> params: SimulationParams;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) global_id: vec3<u32>) {\n let idx = global_id.x;\n let nodeCount = u32(params.nodeCount);\n \n if (idx >= nodeCount) {\n return;\n }\n \n var node = nodes[idx];\n \n // Check if position is fixed\n let isFixedX = !isnan(node.fixedPosition.x);\n let isFixedY = !isnan(node.fixedPosition.y);\n \n // Update positions based on velocity\n if (!isFixedX) {\n node.position.x += node.velocity.x;\n node.velocity.x *= params.velocityDecay;\n } else {\n node.position.x = node.fixedPosition.x;\n node.velocity.x = 0.0;\n }\n \n if (!isFixedY) {\n node.position.y += node.velocity.y;\n node.velocity.y *= params.velocityDecay;\n } else {\n node.position.y = node.fixedPosition.y;\n node.velocity.y = 0.0;\n }\n \n nodes[idx] = node;\n}"}),n=this.device.createBindGroupLayout({label:"Simulation Tick Bind Group Layout",entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:"storage"}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]}),t=this.device.createPipelineLayout({bindGroupLayouts:[n]}),r=this.device.createComputePipeline({label:"Simulation Tick Pipeline",layout:t,compute:{module:e,entryPoint:"main"}});return this.pipelines.set("simulationTick",r),this.bindGroupLayouts.set("simulationTick",n),{pipeline:r,bindGroupLayout:n}}createBindGroup(e,n,t){const r=t.map(((e,n)=>({binding:n,resource:{buffer:e}})));return this.device.createBindGroup({label:`${e} Bind Group`,layout:n,entries:r})}getPipeline(e){return this.pipelines.get(e)}getBindGroupLayout(e){return this.bindGroupLayouts.get(e)}destroy(){this.shaderModules.clear(),this.pipelines.clear(),this.bindGroupLayouts.clear()}}var g=Math.PI*(3-Math.sqrt(5));function p(e){var r,i=1,a=.001,u=1-Math.pow(a,1/300),s=0,f=.6,p=new Map,y=t.timer(U),v=n.dispatch("tick","end"),m=o(),b=null,x=null,P=null,w=null,M=null,S=!1;async function C(){if(!S)try{const n=await async function(){return l||(l=new c,await l.initialize()),l}();b=n.device,x=new d(b),P=new h(b);const{pipeline:t,bindGroupLayout:r}=P.createSimulationTickPipeline();w=t;const o=x.createNodeBuffer(e),a=x.createSimulationParamsBuffer({alpha:i,alphaDecay:u,alphaTarget:s,velocityDecay:f,nodeCount:e.length});M=P.createBindGroup("simulationTick",r,[o,a]),S=!0}catch(e){throw console.error("Failed to initialize WebGPU:",e),e}}async function U(){await G(),v.call("tick",r),i<a&&(y.stop(),v.call("end",r))}async function G(n){void 0===n&&(n=1),S||await C();for(var t=0;t<n;++t){i+=(s-i)*u,x.updateSimulationParams({alpha:i,alphaDecay:u,alphaTarget:s,velocityDecay:f,nodeCount:e.length});for(const e of p.values())e&&"function"==typeof e&&await e(i);const n=b.createCommandEncoder(),t=n.beginComputePass();t.setPipeline(w),t.setBindGroup(0,M),t.dispatchWorkgroups(Math.ceil(e.length/64)),t.end(),b.queue.submit([n.finish()]),await b.queue.onSubmittedWorkDone(),await x.readNodeData(e)}return r}function _(){for(var n,t=0,r=e.length;t<r;++t){if((n=e[t]).index=t,null!=n.fx&&(n.x=n.fx),null!=n.fy&&(n.y=n.fy),isNaN(n.x)||isNaN(n.y)){var i=10*Math.sqrt(.5+t),o=t*g;n.x=i*Math.cos(o),n.y=i*Math.sin(o)}(isNaN(n.vx)||isNaN(n.vy))&&(n.vx=n.vy=0)}}function B(n){if(n.initialize){const t=x?x.buffers.get("nodes")?.buffer:null;n.initialize(e,m,b,t)}return n}return null==e&&(e=[]),_(),r={tick:G,restart:function(){return y.restart(U),r},stop:function(){return y.stop(),r},nodes:function(n){return arguments.length?(e=n,_(),p.forEach(B),S&&(x.destroy(),S=!1),r):e},alpha:function(e){return arguments.length?(i=+e,r):i},alphaMin:function(e){return arguments.length?(a=+e,r):a},alphaDecay:function(e){return arguments.length?(u=+e,r):+u},alphaTarget:function(e){return arguments.length?(s=+e,r):s},velocityDecay:function(e){return arguments.length?(f=1-e,r):1-f},randomSource:function(e){return arguments.length?(m=e,p.forEach(B),r):m},force:function(e,n){return arguments.length>1?(null==n?p.delete(e):p.set(e,B(n)),r):p.get(e)},find:function(n,t,r){var i,o,a,u,s,f=0,c=e.length;for(null==r?r=1/0:r*=r,f=0;f<c;++f)(a=(i=n-(u=e[f]).x)*i+(o=t-u.y)*o)<r&&(s=u,r=a);return s},on:function(e,n){return arguments.length>1?(v.on(e,n),r):v.on(e)},destroy:function(){y.stop(),x&&x.destroy(),P&&P.destroy(),S=!1}}}const y={MIN_GPU_NODES:750,MIN_CPU_FPS:30,MIN_GPU_LINKS:1e3,COMPLEXITY_THRESHOLD:5e5};function v(e,n=2*e.length){const t=e.length,r=t*n;return!!navigator.gpu&&(!(t<y.MIN_GPU_NODES)&&(r>y.COMPLEXITY_THRESHOLD||t>=y.MIN_GPU_NODES&&n>=y.MIN_GPU_LINKS))}class m{constructor(){this.fpsHistory=[],this.frameTimeHistory=[],this.lastTime=performance.now(),this.frameCount=0,this.avgFPS=0,this.avgFrameTime=0}recordFrame(e){this.frameTimeHistory.push(e),this.frameCount++;const n=performance.now();if(n-this.lastTime>=1e3){const e=Math.round(1e3*this.frameCount/(n-this.lastTime));this.fpsHistory.push(e),this.frameCount=0,this.lastTime=n;const t=this.fpsHistory.slice(-5),r=this.frameTimeHistory.slice(-300);this.avgFPS=t.reduce(((e,n)=>e+n),0)/t.length,this.avgFrameTime=r.reduce(((e,n)=>e+n),0)/r.length}}shouldSwitchToGPU(){return this.avgFPS<y.MIN_CPU_FPS&&this.fpsHistory.length>=3}reset(){this.fpsHistory=[],this.frameTimeHistory=[],this.frameCount=0,this.lastTime=performance.now()}}async function b(e,n={}){const{mode:t="auto",enableSwitching:r=!0,onModeChange:i=null,estimatedLinkCount:o=2*e?.length}=n;let a=t,u=null,s=new m,c=null;async function l(e,n=!1){if(!n)return{simulation:f(e),type:"cpu",initialized:!0};try{const n=p(e);return await(n.initialize?.()),{simulation:n,type:"gpu",initialized:!0}}catch(n){return console.warn("GPU simulation failed, falling back to CPU:",n.message),{simulation:f(e),type:"cpu",initialized:!0}}}"auto"===t&&(a=v(e,o)?"gpu":"cpu");const d=await l(e,"gpu"===a);u=d;const h=u.simulation.tick;u.simulation.tick=function(...e){const n=performance.now(),t=h.apply(this,e),r=performance.now()-n;return s.recordFrame(r),t},r&&"cpu"===a&&(c=setInterval((async()=>{s.shouldSwitchToGPU()&&(clearInterval(c),await async function(){if("gpu"===a||!r)return;console.log("🚀 Switching to GPU acceleration for better performance");const n=u.simulation,t=new Map;n.forces&&n.forces.forEach(((e,n)=>{t.set(n,e)})),n.stop();const o=await l(e,!0);return t.forEach(((e,n)=>{o.simulation.force(n,e)})),u=o,a="gpu",s.reset(),i&&i("gpu","Switched to GPU for better performance"),o.simulation}())}),2e3));const g=u.simulation;g.getCurrentMode=()=>a,g.getPerformanceStats=()=>({avgFPS:s.avgFPS,avgFrameTime:s.avgFrameTime,mode:a}),g.forceMode=async n=>{if(n===a)return g;const t=u.simulation,r=new Map;t.forces&&t.forces.forEach(((e,n)=>{r.set(n,e)})),t.stop();const o=await l(e,"gpu"===n);return r.forEach(((e,n)=>{o.simulation.force(n,e)})),u=o,a=n,s.reset(),i&&i(n,`Manually switched to ${n.toUpperCase()}`),o.simulation};const y=g.stop;return g.stop=function(){return c&&(clearInterval(c),c=null),y.call(this)},g._adaptiveMode=a,g._isAdaptive=!0,i&&i(a,`Started with ${a.toUpperCase()} mode`),g}const x={setThresholds:e=>{Object.assign(y,e)},getThresholds:()=>({...y}),shouldUseGPU:v,estimateComplexity:(e,n)=>e*n,recommendMode:(e,n=[])=>{const t=e.length*n.length;return navigator.gpu?e.length<200?"cpu":t>1e6||e.length>2e3?"gpu":"auto":"cpu"}},P=f,w=p,M=b;function S(e){return function(){return e}}function C(e){return 1e-6*(e()-.5)}function U(e){return e.x+e.vx}function G(e){return e.y+e.vy}function _(e){return e.index}function B(e,n){var t=e.get(n);if(!t)throw new Error("node not found: "+n);return t}function N(e,n=[]){const t=e.length,r=t*n.length;return navigator.gpu?t<200?{mode:"cpu",reason:"Small graph - CPU overhead lower"}:t<750?{mode:"cpu",reason:"Medium graph - CPU likely competitive"}:t>2e3?{mode:"gpu",reason:"Large graph - GPU parallelism beneficial"}:r>5e5?{mode:"gpu",reason:"High complexity - GPU acceleration beneficial"}:{mode:"auto",reason:"Let adaptive mode decide based on performance"}:{mode:"cpu",reason:"WebGPU not supported"}}e.AdaptiveConfig=x,e.createOptimalSimulation=async function(e,n={}){const t=N(e,n.links);return await b(e,{mode:t.mode,enableSwitching:!0,onModeChange:(e,t)=>{n.onModeChange?n.onModeChange(e,t):console.log(`🔄 D3-Force: ${t}`)},...n})},e.forceAuto=M,e.forceCPU=P,e.forceCenter=function(e,n){var t,r=1;function i(){var i,o,a=t.length,u=0,s=0;for(i=0;i<a;++i)u+=(o=t[i]).x,s+=o.y;for(u=(u/a-e)*r,s=(s/a-n)*r,i=0;i<a;++i)(o=t[i]).x-=u,o.y-=s}return null==e&&(e=0),null==n&&(n=0),i.initialize=function(e){t=e},i.x=function(n){return arguments.length?(e=+n,i):e},i.y=function(e){return arguments.length?(n=+e,i):n},i.strength=function(e){return arguments.length?(r=+e,i):r},i},e.forceCenterGPU=function(e,n){var t,r,i,o,a,u=1;async function s(){if(!r||!i||!o)return;const e=r.createCommandEncoder(),n=e.beginComputePass();n.setPipeline(i),n.setBindGroup(0,o),n.dispatchWorkgroups(1),n.end(),r.queue.submit([e.finish()]),await r.queue.onSubmittedWorkDone()}function f(){if(r&&a&&t){const i=new Float32Array([e,n,u,t.length]);r.queue.writeBuffer(a,0,i)}}return null==e&&(e=0),null==n&&(n=0),s.initialize=function(s,f,c,l){t=s;const d=(r=c).createShaderModule({label:"Center Force Shader",code:"\nstruct Node {\n position: vec2<f32>,\n velocity: vec2<f32>,\n fixedPosition: vec2<f32>,\n index: f32,\n _padding: f32,\n}\n\nstruct CenterParams {\n center: vec2<f32>,\n strength: f32,\n nodeCount: f32,\n}\n\n@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;\n@group(0) @binding(1) var<uniform> params: CenterParams;\n@group(0) @binding(2) var<storage, read_write> reduction: array<vec2<f32>>;\n\nvar<workgroup> shared_sum: array<vec2<f32>, 64>;\n\n@compute @workgroup_size(64)\nfn main(\n @builtin(global_invocation_id) global_id: vec3<u32>,\n @builtin(local_invocation_id) local_id: vec3<u32>,\n @builtin(workgroup_id) workgroup_id: vec3<u32>\n) {\n let tid = local_id.x;\n let gid = global_id.x;\n let nodeCount = u32(params.nodeCount);\n \n // First pass: compute center of mass\n if (workgroup_id.x == 0u) {\n var sum = vec2<f32>(0.0, 0.0);\n \n // Each thread sums multiple nodes\n for (var i = gid; i < nodeCount; i += 64u) {\n sum += nodes[i].position;\n }\n \n shared_sum[tid] = sum;\n workgroupBarrier();\n \n // Reduction in shared memory\n for (var s = 32u; s > 0u; s >>= 1u) {\n if (tid < s) {\n shared_sum[tid] += shared_sum[tid + s];\n }\n workgroupBarrier();\n }\n \n // Write result\n if (tid == 0u) {\n reduction[0] = shared_sum[0] / f32(nodeCount);\n }\n }\n \n workgroupBarrier();\n storageBarrier();\n \n // Second pass: apply centering force\n if (gid < nodeCount) {\n let center_of_mass = reduction[0];\n let delta = (params.center - center_of_mass) * params.strength;\n nodes[gid].position += delta;\n }\n}"});a=r.createBuffer({label:"Center Parameters",size:16,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST,mappedAtCreation:!0}),new Float32Array(a.getMappedRange()).set([e,n,u,t.length]),a.unmap();const h=r.createBuffer({label:"Reduction Buffer",size:8,usage:GPUBufferUsage.STORAGE}),g=r.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:"storage"}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:"storage"}}]});i=r.createComputePipeline({layout:r.createPipelineLayout({bindGroupLayouts:[g]}),compute:{module:d,entryPoint:"main"}}),o=r.createBindGroup({layout:g,entries:[{binding:0,resource:{buffer:l}},{binding:1,resource:{buffer:a}},{binding:2,resource:{buffer:h}}]})},s.x=function(n){return arguments.length?(e=+n,f(),s):e},s.y=function(e){return arguments.length?(n=+e,f(),s):n},s.strength=function(e){return arguments.length?(u=+e,f(),s):u},s},e.forceCollide=function(e){var n,t,i,o=1,a=1;function u(){for(var e,u,f,c,l,d,h,g=n.length,p=0;p<a;++p)for(u=r.quadtree(n,U,G).visitAfter(s),e=0;e<g;++e)f=n[e],d=t[f.index],h=d*d,c=f.x+f.vx,l=f.y+f.vy,u.visit(y);function y(e,n,t,r,a){var u=e.data,s=e.r,g=d+s;if(!u)return n>c+g||r<c-g||t>l+g||a<l-g;if(u.index>f.index){var p=c-u.x-u.vx,y=l-u.y-u.vy,v=p*p+y*y;v<g*g&&(0===p&&(v+=(p=C(i))*p),0===y&&(v+=(y=C(i))*y),v=(g-(v=Math.sqrt(v)))/v*o,f.vx+=(p*=v)*(g=(s*=s)/(h+s)),f.vy+=(y*=v)*g,u.vx-=p*(g=1-g),u.vy-=y*g)}}}function s(e){if(e.data)return e.r=t[e.data.index];for(var n=e.r=0;n<4;++n)e[n]&&e[n].r>e.r&&(e.r=e[n].r)}function f(){if(n){var r,i,o=n.length;for(t=new Array(o),r=0;r<o;++r)i=n[r],t[i.index]=+e(i,r,n)}}return"function"!=typeof e&&(e=S(null==e?1:+e)),u.initialize=function(e,t){n=e,i=t,f()},u.iterations=function(e){return arguments.length?(a=+e,u):a},u.strength=function(e){return arguments.length?(o=+e,u):o},u.radius=function(n){return arguments.length?(e="function"==typeof n?n:S(+n),f(),u):e},u},e.forceGPU=w,e.forceLink=function(e){var n,t,r,i,o,a,u=_,s=function(e){return 1/Math.min(i[e.source.index],i[e.target.index])},f=S(30),c=1;function l(r){for(var i=0,u=e.length;i<c;++i)for(var s,f,l,d,h,g,p,y=0;y<u;++y)f=(s=e[y]).source,d=(l=s.target).x+l.vx-f.x-f.vx||C(a),h=l.y+l.vy-f.y-f.vy||C(a),d*=g=((g=Math.sqrt(d*d+h*h))-t[y])/g*r*n[y],h*=g,l.vx-=d*(p=o[y]),l.vy-=h*p,f.vx+=d*(p=1-p),f.vy+=h*p}function d(){if(r){var a,s,f=r.length,c=e.length,l=new Map(r.map(((e,n)=>[u(e,n,r),e])));for(a=0,i=new Array(f);a<c;++a)(s=e[a]).index=a,"object"!=typeof s.source&&(s.source=B(l,s.source)),"object"!=typeof s.target&&(s.target=B(l,s.target)),i[s.source.index]=(i[s.source.index]||0)+1,i[s.target.index]=(i[s.target.index]||0)+1;for(a=0,o=new Array(c);a<c;++a)s=e[a],o[a]=i[s.source.index]/(i[s.source.index]+i[s.target.index]);n=new Array(c),h(),t=new Array(c),g()}}function h(){if(r)for(var t=0,i=e.length;t<i;++t)n[t]=+s(e[t],t,e)}function g(){if(r)for(var n=0,i=e.length;n<i;++n)t[n]=+f(e[n],n,e)}return null==e&&(e=[]),l.initialize=function(e,n){r=e,a=n,d()},l.links=function(n){return arguments.length?(e=n,d(),l):e},l.id=function(e){return arguments.length?(u=e,l):u},l.iterations=function(e){return arguments.length?(c=+e,l):c},l.strength=function(e){return arguments.length?(s="function"==typeof e?e:S(+e),h(),l):s},l.distance=function(e){return arguments.length?(f="function"==typeof e?e:S(+e),g(),l):f},l},e.forceManyBody=function(){var e,n,t,i,o,s=S(-30),f=1,c=1/0,l=.81;function d(t){var o,s=e.length,f=r.quadtree(e,a,u).visitAfter(g);for(i=t,o=0;o<s;++o)n=e[o],f.visit(p)}function h(){if(e){var n,t,r=e.length;for(o=new Array(r),n=0;n<r;++n)t=e[n],o[t.index]=+s(t,n,e)}}function g(e){var n,t,r,i,a,u=0,s=0;if(e.length){for(r=i=a=0;a<4;++a)(n=e[a])&&(t=Math.abs(n.value))&&(u+=n.value,s+=t,r+=t*n.x,i+=t*n.y);e.x=r/s,e.y=i/s}else{(n=e).x=n.data.x,n.y=n.data.y;do{u+=o[n.data.index]}while(n=n.next)}e.value=u}function p(e,r,a,u){if(!e.value)return!0;var s=e.x-n.x,d=e.y-n.y,h=u-r,g=s*s+d*d;if(h*h/l<g)return g<c&&(0===s&&(g+=(s=C(t))*s),0===d&&(g+=(d=C(t))*d),g<f&&(g=Math.sqrt(f*g)),n.vx+=s*e.value*i/g,n.vy+=d*e.value*i/g),!0;if(!(e.length||g>=c)){(e.data!==n||e.next)&&(0===s&&(g+=(s=C(t))*s),0===d&&(g+=(d=C(t))*d),g<f&&(g=Math.sqrt(f*g)));do{e.data!==n&&(h=o[e.data.index]*i/g,n.vx+=s*h,n.vy+=d*h)}while(e=e.next)}}return d.initialize=function(n,r){e=n,t=r,h()},d.strength=function(e){return arguments.length?(s="function"==typeof e?e:S(+e),h(),d):s},d.distanceMin=function(e){return arguments.length?(f=e*e,d):Math.sqrt(f)},d.distanceMax=function(e){return arguments.length?(c=e*e,d):Math.sqrt(c)},d.theta=function(e){return arguments.length?(l=e*e,d):Math.sqrt(l)},d},e.forceManyBodyGPU=function(){var e,n,t,r,i,o,a,u=S(-30),s=1,f=1/0,c=.81;async function l(i){if(!n||!t||!r)return;const a="function"==typeof u?u():u,l=new Float32Array([a,s,f,c,i,e.length,0,0]);n.queue.writeBuffer(o,0,l);const d=n.createCommandEncoder(),h=d.beginComputePass();h.setPipeline(t),h.setBindGroup(0,r),h.dispatchWorkgroups(Math.ceil(e.length/64)),h.end(),n.queue.submit([d.finish()]),await n.queue.onSubmittedWorkDone()}function d(){if(e){var t,r,o=e.length;for(a=new Float32Array(o),t=0;t<o;++t)r=e[t],a[r.index]=+u(r,t,e);n&&i&&n.queue.writeBuffer(i,0,a)}}return l.initialize=function(u,s,f,c){e=u,n=f,d();const l=n.createShaderModule({label:"Many Body Force Shader",code:"\nstruct Node {\n position: vec2<f32>,\n velocity: vec2<f32>,\n fixedPosition: vec2<f32>,\n index: f32,\n _padding: f32,\n}\n\nstruct ForceParams {\n strength: f32,\n distanceMin2: f32,\n distanceMax2: f32,\n theta2: f32,\n alpha: f32,\n nodeCount: f32,\n _padding: vec2<f32>,\n}\n\n@group(0) @binding(0) var<storage, read_write> nodes: array<Node>;\n@group(0) @binding(1) var<storage, read> strengths: array<f32>;\n@group(0) @binding(2) var<uniform> params: ForceParams;\n\nconst EPSILON: f32 = 1e-6;\n\n@compute @workgroup_size(64)\nfn main(@builtin(global_invocation_id) global_id: vec3<u32>) {\n let idx = global_id.x;\n let nodeCount = u32(params.nodeCount);\n \n if (idx >= nodeCount) {\n return;\n }\n \n var node = nodes[idx];\n var force = vec2<f32>(0.0, 0.0);\n \n for (var j = 0u; j < nodeCount; j++) {\n if (j == idx) {\n continue;\n }\n \n let other = nodes[j];\n var delta = node.position - other.position;\n var l = dot(delta, delta);\n \n if (l < EPSILON) {\n delta = vec2<f32>(\n (f32(idx) * 0.618033988749895 - floor(f32(idx) * 0.618033988749895)) * 2.0 - 1.0,\n (f32(j) * 0.618033988749895 - floor(f32(j) * 0.618033988749895)) * 2.0 - 1.0\n ) * EPSILON;\n l = dot(delta, delta);\n }\n \n if (l >= params.distanceMax2) {\n continue;\n }\n \n if (l < params.distanceMin2) {\n l = sqrt(params.distanceMin2 * l);\n } else {\n l = sqrt(l);\n }\n \n let strength = strengths[j] * params.alpha / l;\n force += delta / l * strength;\n }\n \n node.velocity += force;\n nodes[idx] = node;\n}"});i=n.createBuffer({label:"Strength Buffer",size:a.byteLength,usage:GPUBufferUsage.STORAGE|GPUBufferUsage.COPY_DST,mappedAtCreation:!0}),new Float32Array(i.getMappedRange()).set(a),i.unmap(),o=n.createBuffer({label:"Force Parameters",size:32,usage:GPUBufferUsage.UNIFORM|GPUBufferUsage.COPY_DST});const h=n.createBindGroupLayout({entries:[{binding:0,visibility:GPUShaderStage.COMPUTE,buffer:{type:"storage"}},{binding:1,visibility:GPUShaderStage.COMPUTE,buffer:{type:"read-only-storage"}},{binding:2,visibility:GPUShaderStage.COMPUTE,buffer:{type:"uniform"}}]});t=n.createComputePipeline({layout:n.createPipelineLayout({bindGroupLayouts:[h]}),compute:{module:l,entryPoint:"main"}}),r=n.createBindGroup({layout:h,entries:[{binding:0,resource:{buffer:c}},{binding:1,resource:{buffer:i}},{binding:2,resource:{buffer:o}}]})},l.strength=function(e){return arguments.length?(u="function"==typeof e?e:S(+e),d(),l):u},l.distanceMin=function(e){return arguments.length?(s=e*e,l):Math.sqrt(s)},l.distanceMax=function(e){return arguments.length?(f=e*e,l):Math.sqrt(f)},l.theta=function(e){return arguments.length?(c=e*e,l):Math.sqrt(c)},l},e.forceRadial=function(e,n,t){var r,i,o,a=S(.1);function u(e){for(var a=0,u=r.length;a<u;++a){var s=r[a],f=s.x-n||1e-6,c=s.y-t||1e-6,l=Math.sqrt(f*f+c*c),d=(o[a]-l)*i[a]*e/l;s.vx+=f*d,s.vy+=c*d}}function s(){if(r){var n,t=r.length;for(i=new Array(t),o=new Array(t),n=0;n<t;++n)o[n]=+e(r[n],n,r),i[n]=isNaN(o[n])?0:+a(r[n],n,r)}}return"function"!=typeof e&&(e=S(+e)),null==n&&(n=0),null==t&&(t=0),u.initialize=function(e){r=e,s()},u.strength=function(e){return arguments.length?(a="function"==typeof e?e:S(+e),s(),u):a},u.radius=function(n){return arguments.length?(e="function"==typeof n?n:S(+n),s(),u):e},u.x=function(e){return arguments.length?(n=+e,u):n},u.y=function(e){return arguments.length?(t=+e,u):t},u},e.forceSimulation=b,e.forceSimulationCPU=f,e.forceSimulationGPU=p,e.forceX=function(e){var n,t,r,i=S(.1);function o(e){for(var i,o=0,a=n.length;o<a;++o)(i=n[o]).vx+=(r[o]-i.x)*t[o]*e}function a(){if(n){var o,a=n.length;for(t=new Array(a),r=new Array(a),o=0;o<a;++o)t[o]=isNaN(r[o]=+e(n[o],o,n))?0:+i(n[o],o,n)}}return"function"!=typeof e&&(e=S(null==e?0:+e)),o.initialize=function(e){n=e,a()},o.strength=function(e){return arguments.length?(i="function"==typeof e?e:S(+e),a(),o):i},o.x=function(n){return arguments.length?(e="function"==typeof n?n:S(+n),a(),o):e},o},e.forceY=function(e){var n,t,r,i=S(.1);function o(e){for(var i,o=0,a=n.length;o<a;++o)(i=n[o]).vy+=(r[o]-i.y)*t[o]*e}function a(){if(n){var o,a=n.length;for(t=new Array(a),r=new Array(a),o=0;o<a;++o)t[o]=isNaN(r[o]=+e(n[o],o,n))?0:+i(n[o],o,n)}}return"function"!=typeof e&&(e=S(null==e?0:+e)),o.initialize=function(e){n=e,a()},o.strength=function(e){return arguments.length?(i="function"==typeof e?e:S(+e),a(),o):i},o.y=function(n){return arguments.length?(e="function"==typeof n?n:S(+n),a(),o):e},o},e.getRecommendedMode=N,Object.defineProperty(e,"__esModule",{value:!0})}));