@peter.prib/renderengine 0.15.2 → 0.16.0

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 (42) hide show
  1. package/dist/renderengine-workspace.umd.js +6030 -3766
  2. package/dist/renderengine.umd.js +5930 -3772
  3. package/documentation/REFERENCE.md +37 -3
  4. package/package.json +1 -1
  5. package/src/archimateArchiFormat.js +114 -10
  6. package/src/archimateArchive.js +27 -4
  7. package/src/archimateExchangeFormat.js +12 -4
  8. package/src/archimateSpecializations.js +11 -1
  9. package/src/chartCatalog.js +6 -4
  10. package/src/charting/ArchimateDiagramEditor.js +5 -108
  11. package/src/charting/DiagramTypeRegistry.js +20 -1
  12. package/src/charting/DiagramViewPropertiesDialog.js +54 -10
  13. package/src/charting/Legend.js +1 -2
  14. package/src/charting/Statistics.js +14 -0
  15. package/src/charting/charts/BarChart.js +59 -8
  16. package/src/charting/charts/HexbinChart.js +15 -0
  17. package/src/charting/charts/LollipopChart.js +42 -7
  18. package/src/charting/decision/DecisionWorkbench.js +137 -4
  19. package/src/charting/diagramModel/DiagramView.js +7 -2
  20. package/src/charting/diagrams/GraphDiagram3D.js +15 -0
  21. package/src/charting/graph/GraphSource.js +72 -0
  22. package/src/charting/modelTree/NavigatorModel.js +22 -8
  23. package/src/charting/report/TemplatedReport.js +27 -377
  24. package/src/charting/report/ViewsReport.js +118 -0
  25. package/src/charting/report/reportWriters.js +496 -0
  26. package/src/charting/templates/C4BoundaryTemplate.js +34 -0
  27. package/src/charting/templates/C4ComponentTemplate.js +30 -0
  28. package/src/charting/templates/C4ContainerTemplate.js +49 -0
  29. package/src/charting/templates/C4PersonTemplate.js +35 -0
  30. package/src/charting/templates/C4SystemTemplate.js +32 -0
  31. package/src/charting/templates/C4Template.js +95 -0
  32. package/src/charting/templates/index.js +6 -0
  33. package/src/charting/templates/templateRegistry.js +7 -0
  34. package/src/compute/ComputeBackend.js +132 -0
  35. package/src/compute/CpuBackend.js +209 -0
  36. package/src/compute/WebGpuBackend.js +630 -0
  37. package/src/compute/index.js +5 -0
  38. package/src/compute/selectBackend.js +85 -0
  39. package/src/index.js +3403 -3371
  40. package/src/kernelExpr.js +80 -5
  41. package/src/workspace/parts/DiagramEditorPart.js +3 -1
  42. package/src/workspace/parts/ViewsPart.js +106 -0
@@ -0,0 +1,630 @@
1
+ // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
2
+ import { ComputeBackend } from './ComputeBackend.js';
3
+ import { parseKernelExpr, wgslKernelModule } from '../kernelExpr.js';
4
+
5
+ const GPU_STORAGE = 0x0080;
6
+ const GPU_UNIFORM = 0x0040;
7
+ const GPU_COPY_SRC = 0x0004;
8
+ const GPU_COPY_DST = 0x0008;
9
+ const GPU_MAP_READ = 0x0001;
10
+
11
+ const _align4 = (n) => (n + 3) & ~3;
12
+
13
+ export const KDE_WGSL = /* wgsl */`
14
+ struct KdeParams {
15
+ minX: f32,
16
+ stepX: f32,
17
+ bandwidth: f32,
18
+ invTwoBwSq: f32,
19
+ invBwSqrt2Pi: f32,
20
+ sampleCount: u32,
21
+ n: u32,
22
+ _pad: u32,
23
+ };
24
+
25
+ @group(0) @binding(0) var<storage, read> values : array<f32>;
26
+ @group(0) @binding(1) var<storage, read_write> densities : array<f32>;
27
+ @group(0) @binding(2) var<uniform> params : KdeParams;
28
+
29
+ @compute @workgroup_size(64)
30
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
31
+ let i = gid.x;
32
+ if (i >= params.sampleCount) { return; }
33
+ if (params.n == 0u) {
34
+ densities[i] = 0.0;
35
+ return;
36
+ }
37
+ let x = params.minX + f32(i) * params.stepX;
38
+ var acc: f32 = 0.0;
39
+ for (var j: u32 = 0u; j < params.n; j = j + 1u) {
40
+ let diff = x - values[j];
41
+ acc = acc + exp(-diff * diff * params.invTwoBwSq);
42
+ }
43
+ densities[i] = (acc * params.invBwSqrt2Pi) / f32(params.n);
44
+ }
45
+ `;
46
+
47
+ export const FORCE_3D_WGSL = /* wgsl */`
48
+ struct SimParams {
49
+ nodeCount: u32,
50
+ edgeCount: u32,
51
+ K: f32,
52
+ temp: f32,
53
+ };
54
+
55
+ @group(0) @binding(0) var<storage, read_write> nodes : array<vec4<f32>>; // pos: xyz, w=fixed (1.0 or 0.0)
56
+ @group(0) @binding(1) var<storage, read> edges : array<vec2<u32>>; // sourceIdx, targetIdx
57
+ @group(0) @binding(2) var<uniform> params : SimParams;
58
+
59
+ @compute @workgroup_size(64)
60
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
61
+ let i = gid.x;
62
+ if (i >= params.nodeCount) { return; }
63
+ let self = nodes[i];
64
+ if (self.w > 0.5) { return; }
65
+
66
+ var vx: f32 = 0.0;
67
+ var vy: f32 = 0.0;
68
+ var vz: f32 = 0.0;
69
+ let K = params.K;
70
+ let K2 = K * K;
71
+
72
+ for (var j: u32 = 0u; j < params.nodeCount; j = j + 1u) {
73
+ if (i == j) { continue; }
74
+ let other = nodes[j];
75
+ let dx = other.x - self.x;
76
+ let dy = other.y - self.y;
77
+ let dz = other.z - self.z;
78
+ var dist = sqrt(dx * dx + dy * dy + dz * dz);
79
+ if (dist < 0.01) { dist = 0.01; }
80
+ let f = K2 / dist;
81
+ let ux = dx / dist;
82
+ let uy = dy / dist;
83
+ let uz = dz / dist;
84
+ vx = vx - ux * f;
85
+ vy = vy - uy * f;
86
+ vz = vz - uz * f;
87
+ }
88
+
89
+ for (var e: u32 = 0u; e < params.edgeCount; e = e + 1u) {
90
+ let edge = edges[e];
91
+ var otherIdx: u32 = 0u;
92
+ var signVal: f32 = 0.0;
93
+ if (edge.x == i) {
94
+ otherIdx = edge.y;
95
+ signVal = 1.0;
96
+ } else if (edge.y == i) {
97
+ otherIdx = edge.x;
98
+ signVal = 1.0;
99
+ }
100
+ if (signVal > 0.0) {
101
+ let other = nodes[otherIdx];
102
+ let dx = other.x - self.x;
103
+ let dy = other.y - self.y;
104
+ let dz = other.z - self.z;
105
+ var dist = sqrt(dx * dx + dy * dy + dz * dz);
106
+ if (dist < 0.01) { dist = 0.01; }
107
+ let f = (dist * dist) / (K * 1.5);
108
+ let ux = dx / dist;
109
+ let uy = dy / dist;
110
+ let uz = dz / dist;
111
+ vx = vx + ux * f;
112
+ vy = vy + uy * f;
113
+ vz = vz + uz * f;
114
+ }
115
+ }
116
+
117
+ vx = vx - self.x * 0.02;
118
+ vy = vy - self.y * 0.02;
119
+ vz = vz - self.z * 0.02;
120
+
121
+ let mag = sqrt(vx * vx + vy * vy + vz * vz);
122
+ let safeMag = select(mag, 1.0, mag == 0.0);
123
+ let dt = min(mag, params.temp) / safeMag;
124
+
125
+ nodes[i] = vec4<f32>(self.x + vx * dt, self.y + vy * dt, self.z + vz * dt, self.w);
126
+ }
127
+ `;
128
+
129
+ export const HEXBIN_WGSL = /* wgsl */`
130
+ struct HexParams {
131
+ r: f32,
132
+ count: u32,
133
+ _p1: u32,
134
+ _p2: u32,
135
+ };
136
+
137
+ @group(0) @binding(0) var<storage, read> points : array<vec2<f32>>;
138
+ @group(0) @binding(1) var<uniform> params : HexParams;
139
+ @group(0) @binding(2) var<storage, read_write> hexCoords : array<vec4<f32>>;
140
+
141
+ @compute @workgroup_size(64)
142
+ fn main(@builtin(global_invocation_id) gid : vec3<u32>) {
143
+ let i = gid.x;
144
+ if (i >= params.count) { return; }
145
+ let pt = points[i];
146
+ let r = params.r;
147
+ let sqrt3 = 1.7320508075688772;
148
+
149
+ let q = (sqrt3 / 3.0 * pt.x - 1.0 / 3.0 * pt.y) / r;
150
+ let s = (2.0 / 3.0 * pt.y) / r;
151
+
152
+ var rx = round(q);
153
+ var rz = round(s);
154
+ var ry = round(-q - s);
155
+
156
+ let xDiff = abs(rx - q);
157
+ let yDiff = abs(ry - (-q - s));
158
+ let zDiff = abs(rz - s);
159
+
160
+ if (xDiff > yDiff && xDiff > zDiff) {
161
+ rx = -ry - rz;
162
+ } else if (yDiff > zDiff) {
163
+ ry = -rx - rz;
164
+ } else {
165
+ rz = -rx - ry;
166
+ }
167
+
168
+ let cx = r * (sqrt3 * rx + sqrt3 / 2.0 * rz);
169
+ let cy = r * (1.5 * rz);
170
+
171
+ hexCoords[i] = vec4<f32>(rx, rz, cx, cy);
172
+ }
173
+ `;
174
+
175
+ /**
176
+ * WebGpuBackend — hardware-accelerated compute offload via WebGPU.
177
+ */
178
+ export class WebGpuBackend extends ComputeBackend {
179
+ constructor({ gpu = null, powerPreference = 'high-performance' } = {}) {
180
+ super();
181
+ this._gpu = gpu ?? (typeof navigator !== 'undefined' ? navigator.gpu : null);
182
+ this._power = powerPreference;
183
+ this.device = null;
184
+ this.adapter = null;
185
+ this._pipelines = new Map();
186
+ }
187
+
188
+ get name() {
189
+ return 'webgpu';
190
+ }
191
+
192
+ get isSync() {
193
+ return false;
194
+ }
195
+
196
+ static async isAvailable(gpu = (typeof navigator !== 'undefined' ? navigator.gpu : null)) {
197
+ if (!gpu?.requestAdapter) return false;
198
+ try {
199
+ return Boolean(await gpu.requestAdapter());
200
+ } catch {
201
+ return false;
202
+ }
203
+ }
204
+
205
+ async init() {
206
+ if (this.device) return this;
207
+ if (!this._gpu?.requestAdapter) {
208
+ throw new Error(
209
+ 'WebGpuBackend: navigator.gpu is not available here. Use CpuBackend or selectBackend().'
210
+ );
211
+ }
212
+ this.adapter = await this._gpu.requestAdapter({ powerPreference: this._power });
213
+ if (!this.adapter) {
214
+ throw new Error('WebGpuBackend: no GPU adapter was available');
215
+ }
216
+ this.device = await this.adapter.requestDevice();
217
+ return this;
218
+ }
219
+
220
+ _assertReady() {
221
+ if (!this.device) {
222
+ throw new Error('WebGpuBackend: device not initialized. Call await backend.init() first.');
223
+ }
224
+ }
225
+
226
+ _getPipeline(name, wgslCode) {
227
+ let p = this._pipelines.get(name);
228
+ if (!p) {
229
+ const module = this.device.createShaderModule({ code: wgslCode });
230
+ p = this.device.createComputePipeline({
231
+ layout: 'auto',
232
+ compute: { module, entryPoint: 'main' },
233
+ });
234
+ this._pipelines.set(name, p);
235
+ }
236
+ return p;
237
+ }
238
+
239
+ /**
240
+ * Compute Gaussian KDE asynchronously using GPU.
241
+ */
242
+ async kernelDensityEstimate(values, options = {}) {
243
+ return this.kernelDensityEstimateAsync(values, options);
244
+ }
245
+
246
+ async kernelDensityEstimateAsync(values, options = {}) {
247
+ this._assertReady();
248
+ const n = values.length;
249
+ const sampleCount = options.sampleCount ?? 60;
250
+ if (n === 0) {
251
+ const min = options.min ?? 0, max = options.max ?? 1;
252
+ const step = (max - min) / Math.max(1, sampleCount - 1);
253
+ return Array.from({ length: sampleCount }, (_, i) => ({ x: min + i * step, density: 0 }));
254
+ }
255
+
256
+ let sum = 0, sumSq = 0, dataMin = Infinity, dataMax = -Infinity;
257
+ for (let i = 0; i < n; i++) {
258
+ const v = values[i];
259
+ sum += v; sumSq += v * v;
260
+ if (v < dataMin) dataMin = v;
261
+ if (v > dataMax) dataMax = v;
262
+ }
263
+ const mean = sum / n;
264
+ const variance = Math.max(0, (sumSq / n) - (mean * mean));
265
+ const stdev = Math.sqrt(variance);
266
+ const fallbackBandwidth = (dataMax - dataMin) * 0.1 || 1;
267
+ const silverman = n >= 2 ? 1.06 * stdev * Math.pow(n, -0.2) : 0;
268
+ const bandwidth = options.bandwidth ?? (silverman || fallbackBandwidth);
269
+ const min = options.min ?? (dataMin - 3 * bandwidth);
270
+ const max = options.max ?? (dataMax + 3 * bandwidth);
271
+ const step = (max - min) / Math.max(1, sampleCount - 1);
272
+ const invBwSqrt2Pi = 1 / (bandwidth * Math.sqrt(2 * Math.PI));
273
+ const invTwoBwSq = 1 / (2 * bandwidth * bandwidth);
274
+
275
+ const device = this.device;
276
+ const pipeline = this._getPipeline('kde', KDE_WGSL);
277
+
278
+ const valFloats = new Float32Array(values);
279
+ const valBuf = device.createBuffer({
280
+ size: _align4(valFloats.byteLength),
281
+ usage: GPU_STORAGE | GPU_COPY_DST,
282
+ });
283
+ device.queue.writeBuffer(valBuf, 0, valFloats);
284
+
285
+ const outBytes = _align4(sampleCount * 4);
286
+ const outBuf = device.createBuffer({
287
+ size: outBytes,
288
+ usage: GPU_STORAGE | GPU_COPY_SRC,
289
+ });
290
+ const readBuf = device.createBuffer({
291
+ size: outBytes,
292
+ usage: GPU_COPY_DST | GPU_MAP_READ,
293
+ });
294
+
295
+ // struct KdeParams { minX, stepX, bandwidth, invTwoBwSq, invBwSqrt2Pi, sampleCount, n, _pad }
296
+ const uniformData = new ArrayBuffer(32);
297
+ const uF32 = new Float32Array(uniformData);
298
+ const uU32 = new Uint32Array(uniformData);
299
+ uF32[0] = min;
300
+ uF32[1] = step;
301
+ uF32[2] = bandwidth;
302
+ uF32[3] = invTwoBwSq;
303
+ uF32[4] = invBwSqrt2Pi;
304
+ uU32[5] = sampleCount;
305
+ uU32[6] = n;
306
+ uU32[7] = 0;
307
+
308
+ const uniformBuf = device.createBuffer({
309
+ size: 32,
310
+ usage: GPU_UNIFORM | GPU_COPY_DST,
311
+ });
312
+ device.queue.writeBuffer(uniformBuf, 0, uniformData);
313
+
314
+ const bindGroup = device.createBindGroup({
315
+ layout: pipeline.getBindGroupLayout(0),
316
+ entries: [
317
+ { binding: 0, resource: { buffer: valBuf } },
318
+ { binding: 1, resource: { buffer: outBuf } },
319
+ { binding: 2, resource: { buffer: uniformBuf } },
320
+ ],
321
+ });
322
+
323
+ const encoder = device.createCommandEncoder();
324
+ const pass = encoder.beginComputePass();
325
+ pass.setPipeline(pipeline);
326
+ pass.setBindGroup(0, bindGroup);
327
+ pass.dispatchWorkgroups(Math.ceil(sampleCount / 64));
328
+ pass.end();
329
+
330
+ encoder.copyBufferToBuffer(outBuf, 0, readBuf, 0, outBytes);
331
+ device.queue.submit([encoder.finish()]);
332
+
333
+ await readBuf.mapAsync(GPU_MAP_READ);
334
+ const mapped = new Float32Array(readBuf.getMappedRange().slice(0, sampleCount * 4));
335
+ readBuf.unmap();
336
+
337
+ valBuf.destroy?.();
338
+ outBuf.destroy?.();
339
+ readBuf.destroy?.();
340
+ uniformBuf.destroy?.();
341
+
342
+ const result = [];
343
+ for (let i = 0; i < sampleCount; i++) {
344
+ result.push({ x: min + i * step, density: mapped[i] });
345
+ }
346
+ return result;
347
+ }
348
+
349
+ /**
350
+ * Compute 3D force-directed layout simulation asynchronously on GPU.
351
+ */
352
+ async layoutForce3D(nodes, edges, options = {}) {
353
+ return this.layoutForce3DAsync(nodes, edges, options);
354
+ }
355
+
356
+ async layoutForce3DAsync(nodes, edges, { iterations = 150 } = {}) {
357
+ this._assertReady();
358
+ const nodeCount = nodes.length;
359
+ const edgeCount = edges.length;
360
+ if (nodeCount === 0) return new Map();
361
+
362
+ const idMap = new Map();
363
+ const fixedMap = new Set(nodes.filter(n => n.x != null || n.y != null || n.z != null).map(n => n.id));
364
+
365
+ const nodeData = new Float32Array(nodeCount * 4);
366
+ for (let i = 0; i < nodeCount; i++) {
367
+ const n = nodes[i];
368
+ idMap.set(n.id, i);
369
+ nodeData[i * 4 + 0] = n.x ?? (Math.random() * 2 - 1);
370
+ nodeData[i * 4 + 1] = n.y ?? (Math.random() * 2 - 1);
371
+ nodeData[i * 4 + 2] = n.z ?? (Math.random() * 2 - 1);
372
+ nodeData[i * 4 + 3] = fixedMap.has(n.id) ? 1.0 : 0.0;
373
+ }
374
+
375
+ const validEdges = edges.filter(e => idMap.has(e.source) && idMap.has(e.target));
376
+ const edgeData = new Uint32Array(Math.max(1, validEdges.length) * 2);
377
+ for (let i = 0; i < validEdges.length; i++) {
378
+ edgeData[i * 2 + 0] = idMap.get(validEdges[i].source);
379
+ edgeData[i * 2 + 1] = idMap.get(validEdges[i].target);
380
+ }
381
+
382
+ const device = this.device;
383
+ const pipeline = this._getPipeline('force3d', FORCE_3D_WGSL);
384
+
385
+ const nodeBytes = _align4(nodeData.byteLength);
386
+ const nodeBuf = device.createBuffer({
387
+ size: nodeBytes,
388
+ usage: GPU_STORAGE | GPU_COPY_DST | GPU_COPY_SRC,
389
+ });
390
+ device.queue.writeBuffer(nodeBuf, 0, nodeData);
391
+
392
+ const edgeBytes = _align4(edgeData.byteLength);
393
+ const edgeBuf = device.createBuffer({
394
+ size: edgeBytes,
395
+ usage: GPU_STORAGE | GPU_COPY_DST,
396
+ });
397
+ device.queue.writeBuffer(edgeBuf, 0, edgeData);
398
+
399
+ const readBuf = device.createBuffer({
400
+ size: nodeBytes,
401
+ usage: GPU_COPY_DST | GPU_MAP_READ,
402
+ });
403
+
404
+ const uniformBuf = device.createBuffer({
405
+ size: 16,
406
+ usage: GPU_UNIFORM | GPU_COPY_DST,
407
+ });
408
+
409
+ const bindGroup = device.createBindGroup({
410
+ layout: pipeline.getBindGroupLayout(0),
411
+ entries: [
412
+ { binding: 0, resource: { buffer: nodeBuf } },
413
+ { binding: 1, resource: { buffer: edgeBuf } },
414
+ { binding: 2, resource: { buffer: uniformBuf } },
415
+ ],
416
+ });
417
+
418
+ const K = 1 / Math.cbrt(Math.max(nodeCount, 1));
419
+ let temp = 0.2;
420
+ const workgroups = Math.ceil(nodeCount / 64);
421
+
422
+ // Run simulation iterations on device
423
+ for (let iter = 0; iter < iterations; iter++) {
424
+ const uData = new ArrayBuffer(16);
425
+ new Uint32Array(uData)[0] = nodeCount;
426
+ new Uint32Array(uData)[1] = validEdges.length;
427
+ new Float32Array(uData)[2] = K;
428
+ new Float32Array(uData)[3] = temp;
429
+ device.queue.writeBuffer(uniformBuf, 0, uData);
430
+
431
+ const enc = device.createCommandEncoder();
432
+ const pass = enc.beginComputePass();
433
+ pass.setPipeline(pipeline);
434
+ pass.setBindGroup(0, bindGroup);
435
+ pass.dispatchWorkgroups(workgroups);
436
+ pass.end();
437
+ device.queue.submit([enc.finish()]);
438
+
439
+ temp *= 0.97;
440
+ }
441
+
442
+ // Map positions back
443
+ const copyEnc = device.createCommandEncoder();
444
+ copyEnc.copyBufferToBuffer(nodeBuf, 0, readBuf, 0, nodeBytes);
445
+ device.queue.submit([copyEnc.finish()]);
446
+
447
+ await readBuf.mapAsync(GPU_MAP_READ);
448
+ const mapped = new Float32Array(readBuf.getMappedRange().slice(0, nodeCount * 16));
449
+ readBuf.unmap();
450
+
451
+ nodeBuf.destroy?.();
452
+ edgeBuf.destroy?.();
453
+ readBuf.destroy?.();
454
+ uniformBuf.destroy?.();
455
+
456
+ const pos = new Map();
457
+ for (let i = 0; i < nodeCount; i++) {
458
+ const id = nodes[i].id;
459
+ pos.set(id, {
460
+ x: mapped[i * 4 + 0],
461
+ y: mapped[i * 4 + 1],
462
+ z: mapped[i * 4 + 2],
463
+ vx: 0, vy: 0, vz: 0,
464
+ });
465
+ }
466
+ return pos;
467
+ }
468
+
469
+ /**
470
+ * Compute 2D hexagonal binning coordinates on GPU.
471
+ */
472
+ async binHexbins(points, r, options = {}) {
473
+ return this.binHexbinsAsync(points, r, options);
474
+ }
475
+
476
+ async binHexbinsAsync(points, r, options = {}) {
477
+ this._assertReady();
478
+ const count = points.length;
479
+ if (count === 0) return new Map();
480
+
481
+ const ptData = new Float32Array(count * 2);
482
+ for (let i = 0; i < count; i++) {
483
+ ptData[i * 2 + 0] = points[i].px;
484
+ ptData[i * 2 + 1] = points[i].py;
485
+ }
486
+
487
+ const device = this.device;
488
+ const pipeline = this._getPipeline('hexbin', HEXBIN_WGSL);
489
+
490
+ const ptBytes = _align4(ptData.byteLength);
491
+ const ptBuf = device.createBuffer({
492
+ size: ptBytes,
493
+ usage: GPU_STORAGE | GPU_COPY_DST,
494
+ });
495
+ device.queue.writeBuffer(ptBuf, 0, ptData);
496
+
497
+ const outBytes = _align4(count * 16);
498
+ const outBuf = device.createBuffer({
499
+ size: outBytes,
500
+ usage: GPU_STORAGE | GPU_COPY_SRC,
501
+ });
502
+ const readBuf = device.createBuffer({
503
+ size: outBytes,
504
+ usage: GPU_COPY_DST | GPU_MAP_READ,
505
+ });
506
+
507
+ const uBuf = device.createBuffer({
508
+ size: 16,
509
+ usage: GPU_UNIFORM | GPU_COPY_DST,
510
+ });
511
+ const uData = new ArrayBuffer(16);
512
+ new Float32Array(uData)[0] = r;
513
+ new Uint32Array(uData)[1] = count;
514
+ device.queue.writeBuffer(uBuf, 0, uData);
515
+
516
+ const bindGroup = device.createBindGroup({
517
+ layout: pipeline.getBindGroupLayout(0),
518
+ entries: [
519
+ { binding: 0, resource: { buffer: ptBuf } },
520
+ { binding: 1, resource: { buffer: uBuf } },
521
+ { binding: 2, resource: { buffer: outBuf } },
522
+ ],
523
+ });
524
+
525
+ const enc = device.createCommandEncoder();
526
+ const pass = enc.beginComputePass();
527
+ pass.setPipeline(pipeline);
528
+ pass.setBindGroup(0, bindGroup);
529
+ pass.dispatchWorkgroups(Math.ceil(count / 64));
530
+ pass.end();
531
+ enc.copyBufferToBuffer(outBuf, 0, readBuf, 0, outBytes);
532
+ device.queue.submit([enc.finish()]);
533
+
534
+ await readBuf.mapAsync(GPU_MAP_READ);
535
+ const mapped = new Float32Array(readBuf.getMappedRange().slice(0, count * 16));
536
+ readBuf.unmap();
537
+
538
+ ptBuf.destroy?.();
539
+ outBuf.destroy?.();
540
+ readBuf.destroy?.();
541
+ uBuf.destroy?.();
542
+
543
+ const bins = new Map();
544
+ for (let i = 0; i < count; i++) {
545
+ const rx = Math.round(mapped[i * 4 + 0]);
546
+ const rz = Math.round(mapped[i * 4 + 1]);
547
+ const cx = mapped[i * 4 + 2];
548
+ const cy = mapped[i * 4 + 3];
549
+ const key = `${rx},${rz}`;
550
+
551
+ let bin = bins.get(key);
552
+ if (!bin) {
553
+ bin = { key, q: rx, r: rz, cx, cy, count: 0, points: [], value: 0 };
554
+ bins.set(key, bin);
555
+ }
556
+ bin.count++;
557
+ bin.points.push(points[i]);
558
+ if (points[i].val != null) bin.value += Number(points[i].val) || 0;
559
+ }
560
+ return bins;
561
+ }
562
+
563
+ /**
564
+ * Evaluate a formula AST or source across input column arrays via WGSL compute shader.
565
+ */
566
+ async evaluateFormula(sourceOrAst, params, columns) {
567
+ return this.evaluateFormulaAsync(sourceOrAst, params, columns);
568
+ }
569
+
570
+ async evaluateFormulaAsync(sourceOrAst, params, columns) {
571
+ this._assertReady();
572
+ const { ast } = typeof sourceOrAst === 'string'
573
+ ? parseKernelExpr(sourceOrAst, params)
574
+ : { ast: sourceOrAst };
575
+
576
+ const count = columns[params[0]]?.length ?? 0;
577
+ if (count === 0) return new Float32Array(0);
578
+
579
+ const wgslCode = wgslKernelModule(ast, params);
580
+ const pipeline = this._getPipeline(`formula_${params.join('_')}`, wgslCode);
581
+ const device = this.device;
582
+
583
+ const inBuffers = [];
584
+ const entries = [];
585
+ for (let i = 0; i < params.length; i++) {
586
+ const colData = columns[params[i]];
587
+ const floats = colData instanceof Float32Array ? colData : new Float32Array(colData);
588
+ const size = _align4(floats.byteLength);
589
+ const buf = device.createBuffer({ size, usage: GPU_STORAGE | GPU_COPY_DST });
590
+ device.queue.writeBuffer(buf, 0, floats);
591
+ inBuffers.push(buf);
592
+ entries.push({ binding: i, resource: { buffer: buf } });
593
+ }
594
+
595
+ const outBytes = _align4(count * 4);
596
+ const outBuf = device.createBuffer({ size: outBytes, usage: GPU_STORAGE | GPU_COPY_SRC });
597
+ const readBuf = device.createBuffer({ size: outBytes, usage: GPU_COPY_DST | GPU_MAP_READ });
598
+
599
+ const countBuf = device.createBuffer({ size: 16, usage: GPU_UNIFORM | GPU_COPY_DST });
600
+ device.queue.writeBuffer(countBuf, 0, new Uint32Array([count, 0, 0, 0]));
601
+
602
+ entries.push({ binding: params.length, resource: { buffer: outBuf } });
603
+ entries.push({ binding: params.length + 1, resource: { buffer: countBuf } });
604
+
605
+ const bindGroup = device.createBindGroup({
606
+ layout: pipeline.getBindGroupLayout(0),
607
+ entries,
608
+ });
609
+
610
+ const enc = device.createCommandEncoder();
611
+ const pass = enc.beginComputePass();
612
+ pass.setPipeline(pipeline);
613
+ pass.setBindGroup(0, bindGroup);
614
+ pass.dispatchWorkgroups(Math.ceil(count / 64));
615
+ pass.end();
616
+ enc.copyBufferToBuffer(outBuf, 0, readBuf, 0, outBytes);
617
+ device.queue.submit([enc.finish()]);
618
+
619
+ await readBuf.mapAsync(GPU_MAP_READ);
620
+ const result = new Float32Array(readBuf.getMappedRange().slice(0, count * 4));
621
+ readBuf.unmap();
622
+
623
+ for (const b of inBuffers) b.destroy?.();
624
+ outBuf.destroy?.();
625
+ readBuf.destroy?.();
626
+ countBuf.destroy?.();
627
+
628
+ return result;
629
+ }
630
+ }
@@ -0,0 +1,5 @@
1
+ // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
2
+ export { ComputeBackend } from './ComputeBackend.js';
3
+ export { CpuBackend } from './CpuBackend.js';
4
+ export { WebGpuBackend, KDE_WGSL, FORCE_3D_WGSL, HEXBIN_WGSL } from './WebGpuBackend.js';
5
+ export { selectBackend, availableBackends, BACKENDS } from './selectBackend.js';