@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,49 @@
1
+ import { C4Template } from './C4Template.js';
2
+ import { svgEl } from './svg.js';
3
+
4
+ /** C4ContainerTemplate — An application, service, web app, or data store in C4 model. */
5
+ export class C4ContainerTemplate extends C4Template {
6
+ static defaults = {
7
+ width: 180,
8
+ height: 120,
9
+ fillColor: '#438dd5',
10
+ strokeColor: '#2e6295',
11
+ strokeWidth: 2,
12
+ textColor: '#ffffff',
13
+ };
14
+
15
+ _initState(name, options = {}) {
16
+ super._initState(name, options);
17
+ this.containerType = options.containerType ?? 'application';
18
+ }
19
+
20
+ get cornerRadius() { return 8; }
21
+
22
+ get c4Tag() {
23
+ if (this.technology) return `[Container: ${this.technology}]`;
24
+ return this.containerType === 'database' ? '[Container: Database]' : '[Container]';
25
+ }
26
+
27
+ shapeElements(doc) {
28
+ const elements = super.shapeElements(doc);
29
+ if (this.containerType === 'database') {
30
+ const ix = this.right - 24, iy = this.top + 10;
31
+ const w = 14, h = 16;
32
+ const stroke = { stroke: '#ffffff', 'stroke-width': 1.5, fill: 'none', 'stroke-opacity': 0.9 };
33
+ // Database cylinder icon (top ellipse, middle body, bottom curve)
34
+ elements.push(
35
+ svgEl('ellipse', { cx: ix + w / 2, cy: iy + 3, rx: w / 2, ry: 2.5, ...stroke }, doc),
36
+ svgEl('path', { d: `M ${ix} ${iy + 3} V ${iy + h} C ${ix} ${iy + h + 2.5}, ${ix + w} ${iy + h + 2.5}, ${ix + w} ${iy + h} V ${iy + 3}`, ...stroke }, doc),
37
+ svgEl('path', { d: `M ${ix} ${iy + 8} C ${ix} ${iy + 10.5}, ${ix + w} ${iy + 10.5}, ${ix + w} ${iy + 8}`, ...stroke }, doc),
38
+ );
39
+ }
40
+ return elements;
41
+ }
42
+
43
+ serialize() {
44
+ return {
45
+ ...super.serialize(),
46
+ ...(this.containerType && this.containerType !== 'application' ? { containerType: this.containerType } : {}),
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,35 @@
1
+ import { C4Template } from './C4Template.js';
2
+ import { svgEl } from './svg.js';
3
+
4
+ /** C4PersonTemplate — A human user, actor, persona, or stakeholder in C4 model. */
5
+ export class C4PersonTemplate extends C4Template {
6
+ static defaults = {
7
+ width: 160,
8
+ height: 120,
9
+ fillColor: '#08427b',
10
+ strokeColor: '#052e56',
11
+ strokeWidth: 2,
12
+ textColor: '#ffffff',
13
+ };
14
+
15
+ get cornerRadius() { return 10; }
16
+ get c4Tag() { return '[Person]'; }
17
+ get hasTopIcon() { return true; }
18
+
19
+ shapeElements(doc) {
20
+ const elements = super.shapeElements(doc);
21
+ const cx = this.x;
22
+ const cy = this.top + 18;
23
+ const headR = 8;
24
+ const head = svgEl('circle', {
25
+ cx, cy: cy - 2, r: headR, fill: '#ffffff', 'fill-opacity': '0.9', stroke: 'none',
26
+ }, doc);
27
+ const shoulderY = cy + headR + 1;
28
+ const shoulders = svgEl('path', {
29
+ d: `M ${cx - 12} ${shoulderY + 10} C ${cx - 12} ${shoulderY + 2}, ${cx - 6} ${shoulderY}, ${cx} ${shoulderY} C ${cx + 6} ${shoulderY}, ${cx + 12} ${shoulderY + 2}, ${cx + 12} ${shoulderY + 10} Z`,
30
+ fill: '#ffffff', 'fill-opacity': '0.9', stroke: 'none',
31
+ }, doc);
32
+ elements.push(head, shoulders);
33
+ return elements;
34
+ }
35
+ }
@@ -0,0 +1,32 @@
1
+ import { C4Template } from './C4Template.js';
2
+
3
+ /** C4SystemTemplate — Software System in focus or external software system. */
4
+ export class C4SystemTemplate extends C4Template {
5
+ static defaults = {
6
+ width: 180,
7
+ height: 120,
8
+ fillColor: '#1168bd',
9
+ strokeColor: '#0b4884',
10
+ strokeWidth: 2,
11
+ textColor: '#ffffff',
12
+ };
13
+
14
+ _initState(name, options = {}) {
15
+ super._initState(name, options);
16
+ this.external = Boolean(options.external);
17
+ if (this.external && !options.fillColor) {
18
+ this.fillColor = '#999999';
19
+ this.strokeColor = '#666666';
20
+ }
21
+ }
22
+
23
+ get cornerRadius() { return 8; }
24
+ get c4Tag() { return this.external ? '[Software System: External]' : '[Software System]'; }
25
+
26
+ serialize() {
27
+ return {
28
+ ...super.serialize(),
29
+ ...(this.external ? { external: true } : {}),
30
+ };
31
+ }
32
+ }
@@ -0,0 +1,95 @@
1
+ import { RectangleTemplate } from './RectangleTemplate.js';
2
+ import { svgEl } from './svg.js';
3
+ import { TextStyle } from '../TextStyle.js';
4
+
5
+ /**
6
+ * C4Template — Base class for Simon Brown's C4 model architectural elements.
7
+ * Provides hierarchical multi-line label rendering (Name, Tag, Technology, Description).
8
+ */
9
+ export class C4Template extends RectangleTemplate {
10
+ _initState(name, options = {}) {
11
+ super._initState(name, options);
12
+ this.description = options.description ?? null;
13
+ this.technology = options.technology ?? null;
14
+ }
15
+
16
+ /** The tagline in brackets, e.g. [Person] or [Container: Java] */
17
+ get c4Tag() {
18
+ return null;
19
+ }
20
+
21
+ get isBoundary() {
22
+ return false;
23
+ }
24
+
25
+ get hasTopIcon() {
26
+ return false;
27
+ }
28
+
29
+ _renderLabel(g, doc) {
30
+ const text = (attrs, content) => {
31
+ const t = svgEl('text', {
32
+ 'pointer-events': 'none', ...attrs,
33
+ }, doc);
34
+ t.textContent = content;
35
+ g.appendChild(t);
36
+ return t;
37
+ };
38
+
39
+ const tag = this.c4Tag;
40
+ const desc = this.description;
41
+ const font = TextStyle.svgAttributes(this.textStyle, { defaultPixels: 13 });
42
+ const textColor = this.textColor;
43
+
44
+ // Boundary: top-left corner title
45
+ if (this.isBoundary) {
46
+ const leftPad = this.left + 14;
47
+ const topPad = this.top + 18;
48
+ text({
49
+ x: leftPad, y: topPad, 'text-anchor': 'start', 'dominant-baseline': 'middle',
50
+ ...font, 'font-weight': 'bold', fill: textColor,
51
+ }, this.name);
52
+ if (tag) {
53
+ text({
54
+ x: leftPad, y: topPad + 16, 'text-anchor': 'start', 'dominant-baseline': 'middle',
55
+ ...font, 'font-size': '10px', 'font-weight': 'normal', fill: textColor, opacity: '0.8',
56
+ }, tag);
57
+ }
58
+ return;
59
+ }
60
+
61
+ // Standard C4 box: centered stack
62
+ const yOffset = this.hasTopIcon ? 14 : 0;
63
+ const cy = this.y + yOffset;
64
+
65
+ // 1. Primary Name (bold)
66
+ text({
67
+ x: this.x, y: tag ? cy - 10 : cy, 'text-anchor': 'middle', 'dominant-baseline': 'middle',
68
+ ...font, 'font-weight': 'bold', fill: textColor,
69
+ }, this.name);
70
+
71
+ // 2. C4 Tag (e.g. [Software System], [Container: Java])
72
+ if (tag) {
73
+ text({
74
+ x: this.x, y: cy + 8, 'text-anchor': 'middle', 'dominant-baseline': 'middle',
75
+ ...font, 'font-size': '10px', 'font-weight': 'normal', fill: textColor, opacity: '0.85',
76
+ }, tag);
77
+ }
78
+
79
+ // 3. Description (italic / smaller)
80
+ if (desc) {
81
+ text({
82
+ x: this.x, y: cy + 24, 'text-anchor': 'middle', 'dominant-baseline': 'middle',
83
+ ...font, 'font-size': '9px', 'font-style': 'italic', fill: textColor, opacity: '0.75',
84
+ }, desc);
85
+ }
86
+ }
87
+
88
+ serialize() {
89
+ return {
90
+ ...super.serialize(),
91
+ ...(this.description ? { description: this.description } : {}),
92
+ ...(this.technology ? { technology: this.technology } : {}),
93
+ };
94
+ }
95
+ }
@@ -29,3 +29,9 @@ export { PackageTemplate } from './PackageTemplate.js';
29
29
  export { BoxTemplate } from './BoxTemplate.js';
30
30
  export { DataStoreTemplate } from './DataStoreTemplate.js';
31
31
  export { LifelineTemplate } from './LifelineTemplate.js';
32
+ export { C4Template } from './C4Template.js';
33
+ export { C4PersonTemplate } from './C4PersonTemplate.js';
34
+ export { C4SystemTemplate } from './C4SystemTemplate.js';
35
+ export { C4ContainerTemplate } from './C4ContainerTemplate.js';
36
+ export { C4ComponentTemplate } from './C4ComponentTemplate.js';
37
+ export { C4BoundaryTemplate } from './C4BoundaryTemplate.js';
@@ -5,6 +5,7 @@ import {
5
5
  OctagonTemplate, StarTemplate, ParallelogramTemplate, TrapezoidTemplate, ManualInputTemplate,
6
6
  CylinderTemplate, DocumentTemplate, NoteTemplate, CloudTemplate, ActorTemplate,
7
7
  PackageTemplate, BoxTemplate, DataStoreTemplate, LifelineTemplate,
8
+ C4Template, C4PersonTemplate, C4SystemTemplate, C4ContainerTemplate, C4ComponentTemplate, C4BoundaryTemplate,
8
9
  } from './index.js';
9
10
 
10
11
  export const TEMPLATE_CLASSES = Object.freeze({
@@ -36,6 +37,12 @@ export const TEMPLATE_CLASSES = Object.freeze({
36
37
  'box': BoxTemplate,
37
38
  'data-store': DataStoreTemplate,
38
39
  'lifeline': LifelineTemplate,
40
+ 'c4-generic': C4Template,
41
+ 'c4-person': C4PersonTemplate,
42
+ 'c4-system': C4SystemTemplate,
43
+ 'c4-container': C4ContainerTemplate,
44
+ 'c4-component': C4ComponentTemplate,
45
+ 'c4-boundary': C4BoundaryTemplate,
39
46
  });
40
47
 
41
48
  const CLASS_TO_KEY = new Map(Object.entries(TEMPLATE_CLASSES).map(([k, c]) => [c, k]));
@@ -0,0 +1,132 @@
1
+ // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
2
+ /**
3
+ * ComputeBackend — base class for pure-math compute offload in RenderEngine.
4
+ *
5
+ * Modeled after sqlengine's ComputeBackend:
6
+ * - A synchronous or asynchronous engine for heavy mathematical workloads:
7
+ * Gaussian kernel density estimation (KDE), 3D force-directed graph physics,
8
+ * hexagonal binning, and compiled formula expressions.
9
+ * - CpuBackend is the synchronous pure-JavaScript bedrock: always available,
10
+ * Node.js and browser compatible, zero dependencies, and the numerical reference.
11
+ * - WebGpuBackend is the asynchronous GPU accelerator: compiles WGSL compute
12
+ * shaders, uploads device buffers, dispatches workgroups, and maps results back.
13
+ * - selectBackend({ prefer: ['webgpu', 'cpu'] }) detects device availability at
14
+ * runtime and falls back gracefully.
15
+ */
16
+ export class ComputeBackend {
17
+ constructor() {
18
+ if (new.target === ComputeBackend) {
19
+ throw new Error('ComputeBackend is abstract: use CpuBackend or WebGpuBackend');
20
+ }
21
+ }
22
+
23
+ /** A short name for reporting which backend was selected. */
24
+ get name() {
25
+ throw new Error(`${this.constructor.name} must define a name`);
26
+ }
27
+
28
+ /** Whether operations return immediately (true) or return a Promise (false). */
29
+ get isSync() {
30
+ return false;
31
+ }
32
+
33
+ /**
34
+ * Whether this backend is supported in the current environment.
35
+ * Static probe safe to call without instantiating or acquiring devices.
36
+ * @returns {Promise<boolean>}
37
+ */
38
+ static async isAvailable() {
39
+ return false;
40
+ }
41
+
42
+ /** Acquire devices or pipelines. Safe to call multiple times. */
43
+ async init() {
44
+ return this;
45
+ }
46
+
47
+ /**
48
+ * Gaussian kernel density estimation over raw numeric values.
49
+ * @param {number[]} values
50
+ * @param {object} [options]
51
+ * @returns {Array<{x: number, density: number}> | Promise<Array<{x: number, density: number}>>}
52
+ */
53
+ kernelDensityEstimate(values, options = {}) {
54
+ throw new Error(`${this.constructor.name} must implement kernelDensityEstimate`);
55
+ }
56
+
57
+ /**
58
+ * Asynchronous Gaussian kernel density estimation.
59
+ * @param {number[]} values
60
+ * @param {object} [options]
61
+ * @returns {Promise<Array<{x: number, density: number}>>}
62
+ */
63
+ async kernelDensityEstimateAsync(values, options = {}) {
64
+ return this.kernelDensityEstimate(values, options);
65
+ }
66
+
67
+ /**
68
+ * 3D force-directed graph simulation.
69
+ * @param {Array<{id: string, x?: number, y?: number, z?: number}>} nodes
70
+ * @param {Array<{source: string, target: string}>} edges
71
+ * @param {object} [options]
72
+ * @returns {Map<string, {x: number, y: number, z: number, vx: number, vy: number, vz: number}> | Promise<Map<string, {x: number, y: number, z: number}>>}
73
+ */
74
+ layoutForce3D(nodes, edges, options = {}) {
75
+ throw new Error(`${this.constructor.name} must implement layoutForce3D`);
76
+ }
77
+
78
+ /**
79
+ * Asynchronous 3D force-directed graph simulation.
80
+ * @param {Array<{id: string, x?: number, y?: number, z?: number}>} nodes
81
+ * @param {Array<{source: string, target: string}>} edges
82
+ * @param {object} [options]
83
+ * @returns {Promise<Map<string, {x: number, y: number, z: number}>>}
84
+ */
85
+ async layoutForce3DAsync(nodes, edges, options = {}) {
86
+ return this.layoutForce3D(nodes, edges, options);
87
+ }
88
+
89
+ /**
90
+ * 2D hexagonal binning.
91
+ * @param {Array<{px: number, py: number, [key: string]: any}>} points
92
+ * @param {number} r
93
+ * @param {object} [options]
94
+ * @returns {Map<string, {cx: number, cy: number, count: number, points: Array}> | Promise<Map<string, {cx: number, cy: number, count: number, points: Array}>>}
95
+ */
96
+ binHexbins(points, r, options = {}) {
97
+ throw new Error(`${this.constructor.name} must implement binHexbins`);
98
+ }
99
+
100
+ /**
101
+ * Asynchronous 2D hexagonal binning.
102
+ * @param {Array<{px: number, py: number, [key: string]: any}>} points
103
+ * @param {number} r
104
+ * @param {object} [options]
105
+ * @returns {Promise<Map<string, {cx: number, cy: number, count: number, points: Array}>>}
106
+ */
107
+ async binHexbinsAsync(points, r, options = {}) {
108
+ return this.binHexbins(points, r, options);
109
+ }
110
+
111
+ /**
112
+ * Evaluate a formula AST or source across input column arrays.
113
+ * @param {string|object} sourceOrAst
114
+ * @param {string[]} params
115
+ * @param {Record<string, number[]|Float32Array>} columns
116
+ * @returns {Float32Array | Promise<Float32Array>}
117
+ */
118
+ evaluateFormula(sourceOrAst, params, columns) {
119
+ throw new Error(`${this.constructor.name} must implement evaluateFormula`);
120
+ }
121
+
122
+ /**
123
+ * Asynchronous formula evaluation.
124
+ * @param {string|object} sourceOrAst
125
+ * @param {string[]} params
126
+ * @param {Record<string, number[]|Float32Array>} columns
127
+ * @returns {Promise<Float32Array>}
128
+ */
129
+ async evaluateFormulaAsync(sourceOrAst, params, columns) {
130
+ return this.evaluateFormula(sourceOrAst, params, columns);
131
+ }
132
+ }
@@ -0,0 +1,209 @@
1
+ // Copyright 2026 Jaroslav Peter Prib, Purveyor of Excellence
2
+ import { ComputeBackend } from './ComputeBackend.js';
3
+ import { parseKernelExpr, compileFormula, emitJs } from '../kernelExpr.js';
4
+
5
+ /**
6
+ * CpuBackend — synchronous, pure-JavaScript compute backend.
7
+ *
8
+ * Always available in Node.js and modern browsers, needs no runtime dependencies
9
+ * or WebGPU device, and acts as the golden reference for output equivalence.
10
+ */
11
+ export class CpuBackend extends ComputeBackend {
12
+ get name() {
13
+ return 'cpu';
14
+ }
15
+
16
+ get isSync() {
17
+ return true;
18
+ }
19
+
20
+ static async isAvailable() {
21
+ return true;
22
+ }
23
+
24
+ /**
25
+ * Standard Gaussian kernel density estimate.
26
+ */
27
+ kernelDensityEstimate(values, options = {}) {
28
+ const n = values.length;
29
+ if (n === 0) {
30
+ const sampleCount = options.sampleCount ?? 60;
31
+ const min = options.min ?? 0;
32
+ const max = options.max ?? 1;
33
+ const step = (max - min) / Math.max(1, sampleCount - 1);
34
+ return Array.from({ length: sampleCount }, (_, i) => ({ x: min + i * step, density: 0 }));
35
+ }
36
+
37
+ let sum = 0, sumSq = 0;
38
+ let dataMin = Infinity, dataMax = -Infinity;
39
+ for (let i = 0; i < n; i++) {
40
+ const v = values[i];
41
+ sum += v;
42
+ sumSq += v * v;
43
+ if (v < dataMin) dataMin = v;
44
+ if (v > dataMax) dataMax = v;
45
+ }
46
+
47
+ const mean = sum / n;
48
+ const variance = Math.max(0, (sumSq / n) - (mean * mean));
49
+ const stdev = Math.sqrt(variance);
50
+
51
+ const fallbackBandwidth = (dataMax - dataMin) * 0.1 || 1;
52
+ const silverman = n >= 2 ? 1.06 * stdev * Math.pow(n, -0.2) : 0;
53
+ const bandwidth = options.bandwidth ?? (silverman || fallbackBandwidth);
54
+ const sampleCount = options.sampleCount ?? 60;
55
+ const min = options.min ?? (dataMin - 3 * bandwidth);
56
+ const max = options.max ?? (dataMax + 3 * bandwidth);
57
+ const step = (max - min) / Math.max(1, sampleCount - 1);
58
+ const invBwSqrt2Pi = 1 / (bandwidth * Math.sqrt(2 * Math.PI));
59
+ const invTwoBwSq = 1 / (2 * bandwidth * bandwidth);
60
+
61
+ const points = [];
62
+ for (let i = 0; i < sampleCount; i++) {
63
+ const x = min + i * step;
64
+ let acc = 0;
65
+ for (let j = 0; j < n; j++) {
66
+ const diff = x - values[j];
67
+ acc += Math.exp(-diff * diff * invTwoBwSq);
68
+ }
69
+ const density = (acc * invBwSqrt2Pi) / n;
70
+ points.push({ x, density });
71
+ }
72
+ return points;
73
+ }
74
+
75
+ /**
76
+ * 3D force-directed layout simulation.
77
+ */
78
+ layoutForce3D(nodes, edges, { iterations = 150 } = {}) {
79
+ const fixed = new Set(nodes.filter(n => n.x != null || n.y != null || n.z != null).map(n => n.id));
80
+ const pos = new Map(nodes.map(n => [n.id, {
81
+ x: n.x ?? (Math.random() * 2 - 1),
82
+ y: n.y ?? (Math.random() * 2 - 1),
83
+ z: n.z ?? (Math.random() * 2 - 1),
84
+ vx: 0, vy: 0, vz: 0,
85
+ }]));
86
+ const K = 1 / Math.cbrt(Math.max(nodes.length, 1));
87
+ let temp = 0.2;
88
+
89
+ for (let iter = 0; iter < iterations; iter++) {
90
+ nodes.forEach(n => {
91
+ if (!fixed.has(n.id)) {
92
+ const p = pos.get(n.id);
93
+ p.vx = 0; p.vy = 0; p.vz = 0;
94
+ }
95
+ });
96
+
97
+ for (let i = 0; i < nodes.length; i++) {
98
+ for (let j = i + 1; j < nodes.length; j++) {
99
+ const a = pos.get(nodes[i].id), b = pos.get(nodes[j].id);
100
+ const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
101
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.01;
102
+ const f = (K * K) / dist;
103
+ const ux = dx / dist, uy = dy / dist, uz = dz / dist;
104
+ a.vx -= ux * f; a.vy -= uy * f; a.vz -= uz * f;
105
+ b.vx += ux * f; b.vy += uy * f; b.vz += uz * f;
106
+ }
107
+ }
108
+
109
+ edges.forEach(e => {
110
+ const a = pos.get(e.source), b = pos.get(e.target);
111
+ if (!a || !b) return;
112
+ const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
113
+ const dist = Math.sqrt(dx * dx + dy * dy + dz * dz) || 0.01;
114
+ const f = (dist * dist) / (K * 1.5);
115
+ const ux = dx / dist, uy = dy / dist, uz = dz / dist;
116
+ a.vx += ux * f; a.vy += uy * f; a.vz += uz * f;
117
+ b.vx += ux * f; b.vy += uy * f; b.vz += uz * f;
118
+ });
119
+
120
+ nodes.forEach(n => {
121
+ if (fixed.has(n.id)) return;
122
+ const p = pos.get(n.id);
123
+ p.vx += -p.x * 0.02; p.vy += -p.y * 0.02; p.vz += -p.z * 0.02;
124
+ const mag = Math.sqrt(p.vx * p.vx + p.vy * p.vy + p.vz * p.vz) || 1;
125
+ const dt = Math.min(mag, temp) / mag;
126
+ p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;
127
+ });
128
+ temp *= 0.97;
129
+ }
130
+ return pos;
131
+ }
132
+
133
+ /**
134
+ * 2D hexagonal binning into regular hexagons of radius r in screen coords.
135
+ */
136
+ binHexbins(points, r, options = {}) {
137
+ const dx = r * Math.sqrt(3);
138
+ const dy = r * 1.5;
139
+ const bins = new Map();
140
+
141
+ for (const pt of points) {
142
+ const px = pt.px;
143
+ const py = pt.py;
144
+ // Pointy-top hex grid coordinate estimation
145
+ const q = (Math.sqrt(3) / 3 * px - 1 / 3 * py) / r;
146
+ const s = (2 / 3 * py) / r;
147
+ // Cube round
148
+ let rx = Math.round(q);
149
+ let rz = Math.round(s);
150
+ let ry = Math.round(-q - s);
151
+ const xDiff = Math.abs(rx - q);
152
+ const yDiff = Math.abs(ry - (-q - s));
153
+ const zDiff = Math.abs(rz - s);
154
+ if (xDiff > yDiff && xDiff > zDiff) {
155
+ rx = -ry - rz;
156
+ } else if (yDiff > zDiff) {
157
+ ry = -rx - rz;
158
+ } else {
159
+ rz = -rx - ry;
160
+ }
161
+
162
+ // Hex center from cube coordinates
163
+ const cx = r * (Math.sqrt(3) * rx + Math.sqrt(3) / 2 * rz);
164
+ const cy = r * (3 / 2 * rz);
165
+ const key = `${rx},${rz}`;
166
+
167
+ let bin = bins.get(key);
168
+ if (!bin) {
169
+ bin = { key, q: rx, r: rz, cx, cy, count: 0, points: [], value: 0 };
170
+ bins.set(key, bin);
171
+ }
172
+ bin.count++;
173
+ bin.points.push(pt);
174
+ if (pt.val != null) bin.value += Number(pt.val) || 0;
175
+ }
176
+
177
+ return bins;
178
+ }
179
+
180
+ /**
181
+ * Evaluate a formula AST or source across input column arrays into a Float32Array.
182
+ */
183
+ evaluateFormula(sourceOrAst, params, columns) {
184
+ const fn = typeof sourceOrAst === 'string'
185
+ ? compileFormula(sourceOrAst, params)
186
+ : compileFormulaFromAst(sourceOrAst, params);
187
+
188
+ const count = columns[params[0]]?.length ?? 0;
189
+ const result = new Float32Array(count);
190
+ const numParams = params.length;
191
+ const argBuffer = new Array(numParams);
192
+
193
+ for (let i = 0; i < count; i++) {
194
+ for (let p = 0; p < numParams; p++) {
195
+ argBuffer[p] = columns[params[p]][i];
196
+ }
197
+ result[i] = fn(...argBuffer);
198
+ }
199
+ return result;
200
+ }
201
+ }
202
+
203
+ function compileFormulaFromAst(ast, params) {
204
+ const __clamp = (x, lo, hi) => Math.min(Math.max(x, lo), hi);
205
+ const body = `return ${emitJs(ast)};`;
206
+ // eslint-disable-next-line no-new-func
207
+ const fn = new Function('__clamp', ...params, body);
208
+ return (...values) => fn(__clamp, ...values);
209
+ }