diff --git a/src/app/client/webgpu/core.cljs b/src/app/client/webgpu/core.cljs index 675c789..49031f3 100644 --- a/src/app/client/webgpu/core.cljs +++ b/src/app/client/webgpu/core.cljs @@ -194,10 +194,10 @@ :label "render parss"}))] ;_ (println "num indexes" num-indices)] ;(println "COMPILING") - (-> (.getCompilationInfo shader-module-vertex) - (.then (fn [info] (js/console.log "compute shader info:" info)))) - (-> (.getCompilationInfo shader-module-fragment) - (.then (fn [info] (js/console.log "compute shader info:" info)))) + #_(-> (.getCompilationInfo shader-module-vertex) + (.then (fn [info] (js/console.log "compute shader info:" info)))) + #_(-> (.getCompilationInfo shader-module-fragment) + (.then (fn [info] (js/console.log "compute shader info:" info)))) (.copyExternalImageToTexture (.-queue device) (clj->js {:source font-bitmap}) @@ -224,7 +224,7 @@ (defn render-rect [from data device fformat context config ids] ;(println 'uplaod-vertices data ":::::::" ids) - (println 'config config) + ;(println 'config config) (let [varray (js/Float32Array. (clj->js data)) ids-array (js/Uint32Array. (clj->js ids)) @@ -408,3 +408,397 @@ (.end render-pass) (.submit (.-queue device) [(.finish encoder)])))) + + +;; --- Data Structures --- + +(defn create-graph [nodes edges] + (let [num-nodes (count nodes) + num-edges (count edges) + ;; Each node needs 6 floats: position(2) + velocity(2) + force(2) + nodes-typed-array (js/Float32Array. + (flatten + (map (fn [n] + [(:x n) (:y n) ; position (vec2) + 0.0 0.0 ; velocity (vec2) + 0.0 0.0]) ; force (vec2) + nodes))) + ;; Each edge needs 2 uint32s: node1_index and node2_index + edges-typed-array (js/Uint32Array. + (flatten + (map (fn [e] + [(:node1-index e) + (:node2-index e)]) + edges)))] + (println "Node struct size:" (* 6 4) "bytes") ; 6 floats * 4 bytes + (println "Total nodes buffer size:" (* num-nodes 6 4) "bytes") + (println "Total edges buffer size:" (* num-edges 2 4) "bytes") + {:num-nodes num-nodes + :num-edges num-edges + :nodes nodes-typed-array + :edges edges-typed-array})) +;; --- Shader Modules --- +;; +;; + + + +(def repulsion-shader-code + "struct Node { + position: vec2, + velocity: vec2, + force: vec2 + }; + + @group(0) @binding(0) var nodes: array; + @group(0) @binding(1) var num_nodes: u32; + + const repulsion_strength: f32 = 100.0; + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id: vec3) { + let node_index = global_id.x; + if (node_index >= num_nodes) { + return; + } + + var force = vec2(0.0, 0.0); + for (var i: u32 = 0; i < num_nodes; i++) { + if (i == node_index) { + continue; + } + + let other_node_pos = nodes[i].position; + let delta = nodes[node_index].position - other_node_pos; + let distance = length(delta); + + if (distance > 0.0) { + let repulsion = repulsion_strength / (distance * distance); + force += repulsion * delta / distance; + } + } + + nodes[node_index].force += force; + } +") + + + +(def attraction-shader-code + " + struct Node { + position: vec2, + velocity: vec2, + force: vec2 + }; + + struct Edge { + node1_index: u32, + node2_index: u32 + }; + + @group(0) @binding(0) var nodes: array; + @group(0) @binding(1) var edges: array; + @group(0) @binding(2) var num_edges: u32; + + const ideal_edge_length: f32 = 50.0; + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id: vec3) { + let edge_index = global_id.x; + if (edge_index >= num_edges) { + return;} + + + let edge = edges[edge_index]; + let node1_pos = nodes[edge.node1_index].position; + let node2_pos = nodes[edge.node2_index].position; + + let delta = node2_pos - node1_pos; + let distance = length(delta); + + if (distance > 0.0) { + let attraction = (distance - ideal_edge_length) * delta / distance; + nodes[edge.node1_index].force += attraction; + nodes[edge.node2_index].force -= attraction;}}") + + +(def integration-shader-code + "struct Node { + position: vec2, + velocity: vec2, + force: vec2 + }; + + @group(0) @binding(0) var nodes: array; + @group(0) @binding(1) var num_nodes: u32; + + const cooling_factor: f32 = 0.99; + const dt: f32 = 0.1; + + @compute @workgroup_size(64) + fn main(@builtin(global_invocation_id) global_id: vec3) { + let node_index = global_id.x; + if (node_index >= num_nodes) { + return;} + + + nodes[node_index].velocity = (nodes[node_index].velocity + nodes[node_index].force * dt) * cooling_factor; + nodes[node_index].position += nodes[node_index].velocity * dt; + nodes[node_index].force = vec2(0.0, 0.0);}") + + +(defn create-graph-buffers [device graph] + (let [ + ;; Each node has position (2 floats), velocity (2 floats), and force (2 floats) + node-struct-size (* 6 4) ; 6 floats * 4 bytes per float + node-buffer-size (* (:num-nodes graph) node-struct-size) + + ;; Each edge has two u32 indices + edge-struct-size (* 2 4) ; 2 u32s * 4 bytes per u32 + edge-buffer-size (* (:num-edges graph) edge-struct-size) + + node-count-buffer-size 4 ; Size of one u32 value + edge-count-buffer-size 4] ; Size of one u32 value + (println "bytes per element " (.-BYTES_PER_ELEMENT (:nodes graph))) + (println "bytes per element " (.-BYTES_PER_ELEMENT (:edges graph))) + (println "num nodes " (:num-nodes graph)) + (println "num edges " (:num-edges graph)) + + {:node-buffer (-> device (.createBuffer (clj->js {:label "node buffer" + :size node-buffer-size + :usage (bit-or js/GPUBufferUsage.STORAGE + js/GPUBufferUsage.COPY_DST + js/GPUBufferUsage.COPY_SRC)}))) + :edge-buffer (-> device (.createBuffer (clj->js {:label "edge buffer" + :size edge-buffer-size + :usage (bit-or js/GPUBufferUsage.STORAGE + js/GPUBufferUsage.COPY_DST)}))) + :node-count-buffer (-> device (.createBuffer (clj->js {:label "node count buffer" + :size node-count-buffer-size + :usage (bit-or js/GPUBufferUsage.UNIFORM + js/GPUBufferUsage.COPY_DST)}))) + :edge-count-buffer (-> device (.createBuffer (clj->js {:label "edge count buffer" + :size edge-count-buffer-size + :usage (bit-or js/GPUBufferUsage.UNIFORM + js/GPUBufferUsage.COPY_DST)})))})) + +(defn create-compute-pipeline [device graph-buffers] + (println "create compute pipeline") + (let [repulsion-bind-group-layout (-> device + (.createBindGroupLayout + (clj->js {:label "repulsion bind group layout" + :entries [{:binding 0 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "storage"}} + {:binding 1 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "uniform"}}]}))) + + + attraction-bind-group-layout (-> device + (.createBindGroupLayout + (clj->js {:label "attraction bind group layout" + :entries [{:binding 0 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "storage"}} + {:binding 1 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "read-only-storage"}} + {:binding 2 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "uniform"}}]}))) + + integration-bind-group-layout (-> device + (.createBindGroupLayout + (clj->js {:label "integration bind group layout" + :entries [{:binding 0 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "storage"}} + {:binding 1 + :visibility js/GPUShaderStage.COMPUTE + :buffer {:type "uniform"}}]}))) + + repulsion-pipeline-layout (-> device + (.createPipelineLayout + (clj->js {:label "repulsion pipeline layout" + :bindGroupLayouts [repulsion-bind-group-layout]}))) + + attraction-pipeline-layout (-> device + (.createPipelineLayout + (clj->js {:label "attraction pipeline layout" + :bindGroupLayouts [attraction-bind-group-layout]}))) + + integration-pipeline-layout (-> device + (.createPipelineLayout + (clj->js {:label "integration pipeline layout" + :bindGroupLayouts [integration-bind-group-layout]}))) + + repulsion-shader-module (-> device + (.createShaderModule + (clj->js {:code repulsion-shader-code}))) + + attraction-shader-module (-> device + (.createShaderModule + (clj->js {:code attraction-shader-code}))) + + integration-shader-module (-> device + (.createShaderModule + (clj->js {:code integration-shader-code}))) + + repulsion-pipeline (-> device + (.createComputePipeline + (clj->js {:layout repulsion-pipeline-layout + :compute {:module repulsion-shader-module + :entryPoint "main"}}))) + + attraction-pipeline (-> device + (.createComputePipeline + (clj->js {:layout attraction-pipeline-layout + :compute {:module attraction-shader-module + :entryPoint "main"}}))) + + integration-pipeline (-> device + (.createComputePipeline + (clj->js {:layout integration-pipeline-layout + :compute {:module integration-shader-module + :entryPoint "main"}}))) + + repulsion-bind-group (-> device + (.createBindGroup + (clj->js {:layout repulsion-bind-group-layout + :entries [{:binding 0 + :resource {:buffer (:node-buffer graph-buffers)}} + {:binding 1 + :resource {:buffer (:node-count-buffer graph-buffers)}}]}))) + + attraction-bind-group (-> device + (.createBindGroup + (clj->js {:layout attraction-bind-group-layout + :entries [{:binding 0 + :resource {:buffer (:node-buffer graph-buffers)}} + {:binding 1 + :resource {:buffer (:edge-buffer graph-buffers)}} + {:binding 2 + :resource {:buffer (:edge-count-buffer graph-buffers)}}]}))) + + integration-bind-group (-> device + (.createBindGroup + (clj->js {:layout integration-bind-group-layout + :entries [{:binding 0 + :resource {:buffer (:node-buffer graph-buffers)}} + {:binding 1 + :resource {:buffer (:node-count-buffer graph-buffers)}}]})))] + + {:repulsion-pipeline repulsion-pipeline + :attraction-pipeline attraction-pipeline + :integration-pipeline integration-pipeline + :repulsion-bind-group repulsion-bind-group + :attraction-bind-group attraction-bind-group + :integration-bind-group integration-bind-group})) + +(defn run-simulation [device graph pipelines buffers] + (let [num-iterations 5] + (println "run simulation") + + ;; Initial buffer writes (outside the simulation loop) + (.writeBuffer + (.-queue device) + (:node-count-buffer buffers) + 0 + (js/Uint32Array. (clj->js [(:num-nodes graph)]))) + + (.writeBuffer + (.-queue device) + (:edge-count-buffer buffers) + 0 + (js/Uint32Array. (clj->js [(:num-edges graph)]))) + + (.writeBuffer + (.-queue device) + (:node-buffer buffers) + 0 + (:nodes graph)) + + (.writeBuffer + (.-queue device) + (:edge-buffer buffers) + 0 + (:edges graph)) + + ;; Run iterations + (dotimes [i num-iterations] + (let [command-encoder (.createCommandEncoder device) + compute-pass (.beginComputePass command-encoder)] + + (println "iteration" i) + + ;; Repulsion pass + (.setPipeline compute-pass (:repulsion-pipeline pipelines)) + (.setBindGroup compute-pass 0 (:repulsion-bind-group pipelines)) + (.dispatchWorkgroups compute-pass (js/Math.ceil (/ (:num-nodes graph) 64))) + + ;; Attraction pass + (.setPipeline compute-pass (:attraction-pipeline pipelines)) + (.setBindGroup compute-pass 0 (:attraction-bind-group pipelines)) + (.dispatchWorkgroups compute-pass (js/Math.ceil (/ (:num-edges graph) 64))) + + ;; Integration pass + (.setPipeline compute-pass (:integration-pipeline pipelines)) + (.setBindGroup compute-pass 0 (:integration-bind-group pipelines)) + (.dispatchWorkgroups compute-pass (js/Math.ceil (/ (:num-nodes graph) 64))) + + ;; End compute pass and submit commands + (.end compute-pass) + (.submit (.-queue device) [(.finish command-encoder)])) + (let [staging-buffer (.createBuffer + device + (clj->js {:label "staging buffer" + :size (* (:num-nodes graph) + (.-BYTES_PER_ELEMENT (:nodes graph))) + :usage (bit-or js/GPUBufferUsage.MAP_READ + js/GPUBufferUsage.COPY_DST)})) + command-encoder (.createCommandEncoder device)] + (.copyBufferToBuffer command-encoder + (:node-buffer buffers) + 0 + staging-buffer + 0 + (* (:num-nodes graph) + (.-BYTES_PER_ELEMENT (:nodes graph)))) + (.submit (.-queue device) [(.finish command-encoder)]) + (-> (.mapAsync staging-buffer js/GPUMapMode.READ) + (.then (fn [] + (let [result-buffer (.getMappedRange staging-buffer) + result-array (js/Float32Array. result-buffer)] + (println "Final node positions:" (vec result-array)) + (.unmap staging-buffer))))))))) + + +;; Example usage (you'll need to adapt this to your specific application context) +(defn main-simulation [device] + (println "main simulation") + (let [nodes [{:x 10 :y 20} + {:x 100 :y 100} + {:x 200 :y 50} + {:x 300 :y 300} + {:x 350 :y 350}] ; Example node positions + edges [{:node1-index 0 :node2-index 1} + {:node1-index 1 :node2-index 2} + {:node1-index 2 :node2-index 3} + {:node1-index 3 :node2-index 4} + {:node1-index 4 :node2-index 0} + {:node1-index 0 :node2-index 2} + {:node1-index 1 :node2-index 3} + {:node1-index 2 :node2-index 4}] ; Example edge connections + graph (create-graph nodes edges) + _ (println "graph" graph) + buffers (create-graph-buffers device graph) + _ (println "buffers" buffers) + pipelines (create-compute-pipeline device buffers) + _ (println "pipelines" pipelines)] + + (run-simulation device graph pipelines buffers))) + + + diff --git a/src/app/client/webgpu/shader.cljs b/src/app/client/webgpu/shader.cljs index 72fee9a..a51164e 100644 --- a/src/app/client/webgpu/shader.cljs +++ b/src/app/client/webgpu/shader.cljs @@ -120,9 +120,9 @@ let panX = canvas_settings.panX; let panY = canvas_settings.panY; - let l = (x * z) + panX; + let l = (x * z) + panX; let r = ((x + width) * z) + panX; - let t = (y * z) + panY; + let t = (y * z) + panY; let b = ((y + height) * z) + panY; let left = clipX(l, cwidth); @@ -200,3 +200,338 @@ return color; } "})) + + + +(def simulation-shader-code + " +////////////////////////////////////////////////////////////////////// +// EXAMPLE WGSL FOR A MULTI-PASS FORCE-DIRECTED LAYOUT (COSMOS-STYLE) +////////////////////////////////////////////////////////////////////// + +///////////////////////////// +// Common Data Structures +///////////////////////////// + +// Each node has (x, y). We store them in a flat array, length = 2 * numNodes. +// Bind group #0, binding #0 +@group(0) @binding(0) +var nodes : array; + +// We'll store final forces in a float buffer, length = 2 * numNodes. +// You can store them directly or use atomic if you prefer atomic accumulation. +// For simplicity, assume 32-bit float array for direct writing. +@group(0) @binding(1) +var outForces : array; + +// We also assume a uniform struct with simulation params. +struct SimParams { + numNodes : u32, + spaceSize : f32, + alpha : f32, + repulsion : f32, + theta : f32, + linkSpring : f32, + linkDistance : f32, + gravity : f32, + // etc., if needed +}; +@group(0) @binding(2) +var sim : SimParams; + +// If we want link-based force, we can store adjacency in a buffer. +// For each node i, we have adjacencyCount plus adjacencyIndices... +// This is just one possible layout, similar to the `linkInfoTexture` logic in Cosmos. +// We'll define a struct for adjacency info: + +struct LinkInfo { + // how many neighbors + count : u32, + // index into the adjacency array + offset : u32, +}; +@group(0) @binding(3) +var linkInfos : array; + +// Then we have a big array of neighbor indices. +@group(0) @binding(4) +var linkIndices : array; + +// For a multi-level quadtree, we might store multiple “levels” of center-of-mass data. +// We'll illustrate a single level first. Each cell has sumX, sumY, and count for that cell. +struct CoMCell { + sumX : atomic, // we’ll store float bits via floatBitsToUint + sumY : atomic, + count : atomic, // integer count +}; + +// Suppose we have one such level with dimension levelSize × levelSize cells. +// In a real system, you might have an array of these for multiple levels. +@group(1) @binding(0) +var levelCoM : array; + +// We also pass in the dimension of this level (for indexing): +@group(1) @binding(1) +var levelParams : vec2; +// x = levelSize (number of cells in one dimension), y = not used or might store cellSize, etc. + +// Or you might store each level’s dimension in an array if you have multiple levels. + +///////////////////////////// +// Pass 1: Clear the CoM buffer +///////////////////////////// +// Typically we just set sumX,sumY,count = 0. +// We can do that in a small compute pass that runs over [levelSize * levelSize] threads. + +@compute @workgroup_size(64) +fn clearCoM(@builtin(global_invocation_id) global_id : vec3) { + let idx = global_id.x; + let levelSize = u32(levelParams.x); + + if (idx >= levelSize * levelSize) { + return; + } + + atomicStore(&levelCoM[idx].sumX, 0u); + atomicStore(&levelCoM[idx].sumY, 0u); + atomicStore(&levelCoM[idx].count, 0u); +} + +///////////////////////////// +// Pass 2: Accumulate CoM for this level +///////////////////////////// +// Each thread processes one node, finds which cell it belongs to, does atomicAdd of sumX, sumY, and count. + +@compute @workgroup_size(64) +fn calcCenterOfMassLevel(@builtin(global_invocation_id) global_id : vec3) { + let i = global_id.x; // node index + if (i >= sim.numNodes) { + return; + } + + // read node position + let base = i * 2u; + let x = nodes[base]; + let y = nodes[base + 1u]; + + // figure out which cell of the level we fall into + let levelSizeF = levelParams.x; + let spaceSize = sim.spaceSize; + + // clamp or assume x,y in [0, spaceSize], for example + // you might do something else if the layout can go negative + let cx = clamp(x, 0.0, spaceSize); + let cy = clamp(y, 0.0, spaceSize); + + // map into [0, levelSize) + let cellXf = floor(cx / spaceSize * levelSizeF); + let cellYf = floor(cy / spaceSize * levelSizeF); + + let cellX = u32(cellXf); + let cellY = u32(cellYf); + + let levelSz = u32(levelSizeF); + if (cellX >= levelSz || cellY >= levelSz) { + // out of range, skip + return; + } + + let cellIndex = cellY * levelSz + cellX; + + // atomicAdd to sumX,sumY, count + // we store floats as bits + let xbits = floatBitsToUint(x); + let ybits = floatBitsToUint(y); + + atomicAdd(&levelCoM[cellIndex].sumX, xbits); + atomicAdd(&levelCoM[cellIndex].sumY, ybits); + atomicAdd(&levelCoM[cellIndex].count, 1u); +} + +///////////////////////////// +// Pass 3: Repulsion from CoM +///////////////////////////// +// Each thread processes one node, scanning some or all cells in the level. +// Or do a Barnes–Hut style approach: check if cell is far enough (using sim.theta). +// For simplicity, let’s do a naive approach: sum force from every cell that has count>0 +// ignoring the “far enough” test. Real code might subdivide or skip recursion, etc. + +@compute @workgroup_size(64) +fn calcRepulsionLevel(@builtin(global_invocation_id) global_id : vec3) { + let i = global_id.x; + if (i >= sim.numNodes) { + return; + } + + let base = i * 2u; + let x = nodes[base]; + let y = nodes[base + 1u]; + + var fx = 0.0; + var fy = 0.0; + + let levelSizeF = levelParams.x; + let levelSz = u32(levelSizeF); + + // We do a naive pass over all cells in [0, levelSz*levelSz). + // For a large grid, this can be expensive. But it shows the idea. + // A real version might do a more advanced Barnes-Hut skip. + + for (var cellIndex = 0u; cellIndex < levelSz*levelSz; cellIndex = cellIndex + 1u) { + let cCount = atomicLoad(&levelCoM[cellIndex].count); + if (cCount == 0u) { + continue; // empty cell + } + + let sumX = atomicLoad(&levelCoM[cellIndex].sumX); + let sumY = atomicLoad(&levelCoM[cellIndex].sumY); + + let massF = f32(cCount); + + let centerX = bitcast(sumX / cCount); + let centerY = bitcast(sumY / cCount); + + // skip if it’s the same node or extremely close + // (some tolerance so we don’t get inf) + let dx = centerX - x; + let dy = centerY - y; + let distSqr = dx*dx + dy*dy + 0.0001; // avoid div zero + let dist = sqrt(distSqr); + + // If you wanted a barnes-hut style check, you'd do: + // if cellSize / dist < sim.theta { + // // use approximation + // } else { + // // subdivide or skip + // } + + // Basic repulsion formula (like ForceAtlas2 / Fruchterman–Reingold style): + // F ~ (repulsion * massOfCell) / dist^2 + let mag = sim.repulsion * massF / distSqr; + // multiply by alpha + mag = mag * sim.alpha; + + // accumulate + fx += mag * dx / dist; + fy += mag * dy / dist; + } + + // Write repulsion force to outForces. + // If you have multiple passes (repulsion, link, gravity), + // you might want to accumulate in outForces, so do e.g. outForces[base] += fx + // but WGSL has no built-in atomicAdd for floats. One workaround is: + // - store partial forces in a separate float buffer, then sum later on CPU, + // - or do 32-bit atomics using bit patterns, etc. + // For simplicity, let's directly write (overwriting). + // In practice, you'd want either an atomic approach or multi-buffer approach. + + outForces[base] = fx; + outForces[base + 1u] = fy; +} + +///////////////////////////// +// Pass 4: Link (Spring) Forces +///////////////////////////// +// Similar to your snippet from Cosmos: For each node, loop over that node’s neighbors +// in linkIndices to accumulate spring force. + +@compute @workgroup_size(64) +fn calcLinkForces(@builtin(global_invocation_id) global_id : vec3) { + let i = global_id.x; + if (i >= sim.numNodes) { + return; + } + + let base = i * 2u; + let x = nodes[base]; + let y = nodes[base + 1u]; + + var fx = 0.0; + var fy = 0.0; + + // how many neighbors does this node have? + let info = linkInfos[i]; + let count = info.count; + let offset = info.offset; + + for (var e = 0u; e < count; e = e + 1u) { + let neighborIndex = linkIndices[offset + e]; + if (neighborIndex == i || neighborIndex >= sim.numNodes) { + continue; + } + let nbBase = neighborIndex * 2u; + let nx = nodes[nbBase]; + let ny = nodes[nbBase + 1u]; + + // standard spring formula: + // F = k * (dist - linkDistance) + let dx = nx - x; + let dy = ny - y; + let dist = sqrt(dx*dx + dy*dy + 0.00001); + let desired = sim.linkDistance; // or with random variation if you like + + // “Spring force” magnitude: + let stretch = dist - desired; + let k = sim.linkSpring; + // multiply by alpha if you want damping + let mag = k * stretch * sim.alpha; + + // direction + fx += (mag * dx / dist); + fy += (mag * dy / dist); + } + + // Add to outForces or combine with existing. + // For simplicity, let's do a direct add to the outForces from the repulsion pass: + outForces[base] = outForces[base] + fx; + outForces[base + 1u] = outForces[base + 1u] + fy; +} + +///////////////////////////// +// Pass 5 (Optional): Gravity / Center Pull +///////////////////////////// +// Just like in Cosmos, we might have a pass that pulls each node toward center. + +@compute @workgroup_size(64) +fn calcGravity(@builtin(global_invocation_id) global_id : vec3) { + let i = global_id.x; + if (i >= sim.numNodes) { + return; + } + + let base = i * 2u; + let x = nodes[base]; + let y = nodes[base + 1u]; + + // Let center be (spaceSize/2, spaceSize/2) + let cx = sim.spaceSize * 0.5; + let cy = sim.spaceSize * 0.5; + + let dx = cx - x; + let dy = cy - y; + let dist = sqrt(dx*dx + dy*dy + 0.00001); + + // Some gravity constant: + let g = sim.gravity; // or config.simulationGravity + // multiply by alpha for damping + let mag = g * dist * sim.alpha; + + // direction + let fx = mag * (dx / dist); + let fy = mag * (dy / dist); + + // accumulate + outForces[base] = outForces[base] + fx; + outForces[base + 1u] = outForces[base + 1u] + fy; +} + +////////////////////////////////////////////////////////////////////// +// Usage Summary (Pseudo-code) +// 1) Clear CoM: dispatch( (levelSize*levelSize + 63)/64, 1, 1 ) -> clearCoM +// 2) Calc CoM: dispatch( (numNodes+63)/64, 1, 1 ) -> calcCenterOfMassLevel +// 3) Calc Repulsion: dispatch( (numNodes+63)/64, 1, 1 ) -> calcRepulsionLevel +// 4) Calc Link Forces: dispatch( (numNodes+63)/64, 1, 1 ) -> calcLinkForces +// 5) Calc Gravity: dispatch( (numNodes+63)/64, 1, 1 ) -> calcGravity +// 6) Then integrate positions in another pass, using outForces as velocity or acceleration. +////////////////////////////////////////////////////////////////////// +") diff --git a/src/app/electric_flow.cljc b/src/app/electric_flow.cljc index 1c5d9ab..622b871 100644 --- a/src/app/electric_flow.cljc +++ b/src/app/electric_flow.cljc @@ -7,7 +7,7 @@ [hyperfiddle.kvs :as kvs] [hyperfiddle.domlike :as dl] [hyperfiddle.incseq :as i] - #?@(:cljs [[app.client.webgpu.core :as wcore :refer [render-rect render-text]] + #?@(:cljs [[app.client.webgpu.core :as wcore :refer [render-rect render-text main-simulation]] [global-flow :refer [await-promise mouse-down?> debounce @@ -21,6 +21,7 @@ !atlas-data !command-encoder !format + !selected !all-rects !width !height @@ -59,6 +60,7 @@ (declare font-bitmap) (declare atlas-data) (declare dpr) +(declare selected) (defn create-random-rects [rects ch cw] @@ -151,6 +153,7 @@ (some? context) (some? format)) (when (some? spend) + (println "spend token" spend (some? spend) nu all-rects) (let [rects-data (flatten (into [] (vals all-rects))) rects-ids (into [] (keys all-rects)) [cx cy] nu @@ -199,31 +202,37 @@ ([x] (Tap-diffs prn x))) +#?(:cljs + (defn wx [cx] + (+ (first @!offset) + (* + cx + @!zoom-factor)))) +#?(:cljs + (defn wy [cy] + (+ (second @!offset) + (* + cy + @!zoom-factor)))) + + (e/defn On-node-add [id] (when-some [[x y h w] (id all-rects)] ((fn [] - (let [gx (-> global-atom :cords first) - gy (-> global-atom :cords second) - [ox oy zf] offset - cgx (- (clip-x gx width) ox) - cgy (- (clip-y gy height) oy) - cl (clip-x x width) - cr (clip-x (+ x w) width) - ct (clip-y y height) - cb (clip-y (+ y h) height) - zff (or zf zoom-factor) - clicked? (and (<= cgx cr) (>= cgx cl) - (<= cgy ct) (>= cgy cb))] - (println - id - gx gy - zoom-factor - offset - ":R:" - [x y h w] - (format-float (+ ox (* zff cl))) - (format-float (+ oy (* zff ct))))))))) + (let [gx (* @!dpr (-> global-atom :cords first)) + gy (* @!dpr (-> global-atom :cords second)) + cl (wx x) + cr (wx (+ x w)) + ct (wy y) + cb (wy (+ y h)) + clicked? (and (>= gx cl) (<= gx cr) + (>= gy ct) (<= gy cb))] + (when clicked? + (reset! !selected id) + + (println id "::" gx gy "::" cl ct cr cb))))))) + (e/defn Canvas-view [] (e/client @@ -237,17 +246,15 @@ (Render-with-webgpu) (when-some [down (Mouse-down-cords dom/node)] - (println "DOWN") (reset! !global-atom {:cords down})) - #_(e/for-by identity [node (e/as-vec (e/input (e/join (i/items data-spine))))] - - (println node global-atom) - #_(On-node-add node)) + (e/for-by identity [node (e/as-vec (e/input (e/join (i/items data-spine))))] + #_(println node global-atom) + (On-node-add node)) #_(println "NEW SPINE" - (count visible-rects) - (e/input (i/count data-spine)) - visible-rects - (e/as-vec (e/input (e/join (i/items data-spine))))) + (count visible-rects) + (e/input (i/count data-spine)) + visible-rects + (e/as-vec (e/input (e/join (i/items data-spine))))) (let [mount-items (mount (fn [element child] (do (data-spine @@ -322,10 +329,11 @@ visible-rects (e/watch !visible-rects) old-visible-rects (e/watch !old-visible-rects) data-spine (i/spine) - rect-ids (vec (range 1 30)) + rect-ids (vec (range 1 5)) global-atom (e/watch !global-atom) font-bitmap (e/watch !font-bitmap) atlas-data (e/watch !atlas-data) + selected (e/watch !selected) dpr (e/watch !dpr)] (reset! !dpr (.-devicePixelRatio js/window)) @@ -349,4 +357,7 @@ (println "success canvas" canvas all-rects) (Setup-webgpu) (Add-panning) - (Add-wheel)))))))) + (Add-wheel) + (when (some? device) + (main-simulation device))))))))) + diff --git a/src/global_flow.cljs b/src/global_flow.cljs index 795fe34..3757b01 100644 --- a/src/global_flow.cljs +++ b/src/global_flow.cljs @@ -62,13 +62,14 @@ (defonce !font-bitmap (atom nil)) (defonce !atlas-data (atom nil)) (defonce !dpr (atom nil)) +(defonce !selected (atom nil)) (defn mouse-down?> [node] (->> (mx/mix (m/observe (fn [!] (dom/with-listener node "mousedown" (fn [e] (.preventDefault e) (do - (js/console.log e) - (println "md" (.-clientX e)(.-clientY e)) + ;(js/console.log e) + ;(println "md" (.-clientX e)(.-clientY e)) (! [(.-clientX e) (.-clientY e)])))))) (m/observe (fn [!] (dom/with-listener node "mouseup" (fn [_] (! nil)))))) (m/reductions {} nil)