@yurialvesbordin/graphium 0.0.0-stage → 2.0.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -2
  3. package/dist/Graphium.d.ts +156 -0
  4. package/dist/Graphium.d.ts.map +1 -0
  5. package/dist/Graphium.js +413 -0
  6. package/dist/Graphium.js.map +1 -0
  7. package/dist/core/FocusController.d.ts +16 -0
  8. package/dist/core/FocusController.d.ts.map +1 -0
  9. package/dist/core/FocusController.js +53 -0
  10. package/dist/core/FocusController.js.map +1 -0
  11. package/dist/core/GraphEditor.d.ts +61 -0
  12. package/dist/core/GraphEditor.d.ts.map +1 -0
  13. package/dist/core/GraphEditor.js +165 -0
  14. package/dist/core/GraphEditor.js.map +1 -0
  15. package/dist/core/GraphModel.d.ts +166 -0
  16. package/dist/core/GraphModel.d.ts.map +1 -0
  17. package/dist/core/GraphModel.js +730 -0
  18. package/dist/core/GraphModel.js.map +1 -0
  19. package/dist/core/SelectionController.d.ts +26 -0
  20. package/dist/core/SelectionController.d.ts.map +1 -0
  21. package/dist/core/SelectionController.js +78 -0
  22. package/dist/core/SelectionController.js.map +1 -0
  23. package/dist/core/colors.d.ts +27 -0
  24. package/dist/core/colors.d.ts.map +1 -0
  25. package/dist/core/colors.js +141 -0
  26. package/dist/core/colors.js.map +1 -0
  27. package/dist/core/events.d.ts +21 -0
  28. package/dist/core/events.d.ts.map +1 -0
  29. package/dist/core/events.js +64 -0
  30. package/dist/core/events.js.map +1 -0
  31. package/dist/core/handles.d.ts +60 -0
  32. package/dist/core/handles.d.ts.map +1 -0
  33. package/dist/core/handles.js +116 -0
  34. package/dist/core/handles.js.map +1 -0
  35. package/dist/core/theme.d.ts +9 -0
  36. package/dist/core/theme.d.ts.map +1 -0
  37. package/dist/core/theme.js +167 -0
  38. package/dist/core/theme.js.map +1 -0
  39. package/dist/core/themes.d.ts +9 -0
  40. package/dist/core/themes.d.ts.map +1 -0
  41. package/dist/core/themes.js +103 -0
  42. package/dist/core/themes.js.map +1 -0
  43. package/dist/core/types.d.ts +287 -0
  44. package/dist/core/types.d.ts.map +1 -0
  45. package/dist/core/types.js +7 -0
  46. package/dist/core/types.js.map +1 -0
  47. package/dist/index.d.ts +21 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +20 -0
  50. package/dist/index.js.map +1 -0
  51. package/dist/interaction/InteractionBinder.d.ts +43 -0
  52. package/dist/interaction/InteractionBinder.d.ts.map +1 -0
  53. package/dist/interaction/InteractionBinder.js +121 -0
  54. package/dist/interaction/InteractionBinder.js.map +1 -0
  55. package/dist/interaction/PointerController.d.ts +83 -0
  56. package/dist/interaction/PointerController.d.ts.map +1 -0
  57. package/dist/interaction/PointerController.js +294 -0
  58. package/dist/interaction/PointerController.js.map +1 -0
  59. package/dist/physics/PhysicsEngine.d.ts +82 -0
  60. package/dist/physics/PhysicsEngine.d.ts.map +1 -0
  61. package/dist/physics/PhysicsEngine.js +469 -0
  62. package/dist/physics/PhysicsEngine.js.map +1 -0
  63. package/dist/physics/SpatialHash.d.ts +36 -0
  64. package/dist/physics/SpatialHash.d.ts.map +1 -0
  65. package/dist/physics/SpatialHash.js +126 -0
  66. package/dist/physics/SpatialHash.js.map +1 -0
  67. package/dist/physics/presets.d.ts +14 -0
  68. package/dist/physics/presets.d.ts.map +1 -0
  69. package/dist/physics/presets.js +108 -0
  70. package/dist/physics/presets.js.map +1 -0
  71. package/dist/render/Camera.d.ts +55 -0
  72. package/dist/render/Camera.d.ts.map +1 -0
  73. package/dist/render/Camera.js +94 -0
  74. package/dist/render/Camera.js.map +1 -0
  75. package/dist/render/LabelLayer.d.ts +32 -0
  76. package/dist/render/LabelLayer.d.ts.map +1 -0
  77. package/dist/render/LabelLayer.js +142 -0
  78. package/dist/render/LabelLayer.js.map +1 -0
  79. package/dist/render/RenderLoop.d.ts +67 -0
  80. package/dist/render/RenderLoop.d.ts.map +1 -0
  81. package/dist/render/RenderLoop.js +126 -0
  82. package/dist/render/RenderLoop.js.map +1 -0
  83. package/dist/render/Renderer.d.ts +73 -0
  84. package/dist/render/Renderer.d.ts.map +1 -0
  85. package/dist/render/Renderer.js +512 -0
  86. package/dist/render/Renderer.js.map +1 -0
  87. package/dist/render/TextureAtlas.d.ts +51 -0
  88. package/dist/render/TextureAtlas.d.ts.map +1 -0
  89. package/dist/render/TextureAtlas.js +160 -0
  90. package/dist/render/TextureAtlas.js.map +1 -0
  91. package/dist/render/ViewportController.d.ts +34 -0
  92. package/dist/render/ViewportController.d.ts.map +1 -0
  93. package/dist/render/ViewportController.js +57 -0
  94. package/dist/render/ViewportController.js.map +1 -0
  95. package/dist/render/geometry.d.ts +19 -0
  96. package/dist/render/geometry.d.ts.map +1 -0
  97. package/dist/render/geometry.js +71 -0
  98. package/dist/render/geometry.js.map +1 -0
  99. package/dist/render/shaders.d.ts +13 -0
  100. package/dist/render/shaders.d.ts.map +1 -0
  101. package/dist/render/shaders.js +183 -0
  102. package/dist/render/shaders.js.map +1 -0
  103. package/package.json +73 -4
@@ -0,0 +1,730 @@
1
+ /**
2
+ * Stores the whole graph in contiguous TypedArrays so physics, picking and
3
+ * rendering all run with zero per-frame allocations. External string ids map
4
+ * to dense integer indices through hash maps, and swap-remove keeps every
5
+ * mutation O(links) in the worst case while preserving cache locality.
6
+ */
7
+ import { packRGBA, deterministicColor, DEFAULT_COLOR } from './colors.js';
8
+ export const NODE_FIXED = 1;
9
+ export const NODE_SELECTED = 2;
10
+ export const NODE_DRAGGED = 4;
11
+ export const NODE_HIDDEN = 8;
12
+ export const NODE_DISABLED = 16;
13
+ export const NODE_DIM = 32;
14
+ export const LINK_SELECTED = 1;
15
+ export const LINK_HIDDEN = 2;
16
+ export const LINK_DIM = 4;
17
+ export const STATE_NORMAL = 0;
18
+ export const STATE_HOVER = 1;
19
+ export const STATE_SELECTED = 2;
20
+ export const STATE_DRAGGING = 3;
21
+ export const STATE_FIXED = 4;
22
+ export const STATE_DISABLED = 5;
23
+ export const STYLE_SOLID = 0;
24
+ export const STYLE_DASHED = 1;
25
+ const DEFAULT_NODE_CAPACITY = 512;
26
+ const DEFAULT_LINK_CAPACITY = 1024;
27
+ const stateFromName = (value) => {
28
+ if (typeof value === 'number') {
29
+ return Math.max(STATE_NORMAL, Math.min(STATE_DISABLED, value | 0));
30
+ }
31
+ switch (value) {
32
+ case 'hover':
33
+ return STATE_HOVER;
34
+ case 'selected':
35
+ return STATE_SELECTED;
36
+ case 'dragging':
37
+ return STATE_DRAGGING;
38
+ case 'fixed':
39
+ return STATE_FIXED;
40
+ case 'disabled':
41
+ return STATE_DISABLED;
42
+ default:
43
+ return STATE_NORMAL;
44
+ }
45
+ };
46
+ const nameFromState = (code) => {
47
+ switch (code) {
48
+ case STATE_HOVER:
49
+ return 'hover';
50
+ case STATE_SELECTED:
51
+ return 'selected';
52
+ case STATE_DRAGGING:
53
+ return 'dragging';
54
+ case STATE_FIXED:
55
+ return 'fixed';
56
+ case STATE_DISABLED:
57
+ return 'disabled';
58
+ default:
59
+ return 'normal';
60
+ }
61
+ };
62
+ const styleCode = (value) => (value === 'dashed' ? STYLE_DASHED : STYLE_SOLID);
63
+ const styleName = (code) => (code === STYLE_DASHED ? 'dashed' : 'solid');
64
+ const finiteOr = (value, fallback) => (typeof value === 'number' && Number.isFinite(value) ? value : fallback);
65
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
66
+ export class GraphModel {
67
+ constructor(options) {
68
+ this.nodeCount = 0;
69
+ this.linkCount = 0;
70
+ this.topologyRevision = 0;
71
+ this.positionRevision = 0;
72
+ this.appearanceRevision = 0;
73
+ this.nodeIds = [];
74
+ this.linkIds = [];
75
+ this.nodeIndexById = new Map();
76
+ this.linkIndexById = new Map();
77
+ this.nodeData = [];
78
+ this.linkData = [];
79
+ const theme = options.theme;
80
+ this.nodeDefaults = {
81
+ radius: theme.nodeRadius,
82
+ color: theme.nodeColor,
83
+ borderColor: theme.nodeBorderColor,
84
+ borderWidth: theme.nodeBorderWidth,
85
+ borderOpacity: theme.nodeBorderOpacity,
86
+ opacity: theme.nodeOpacity,
87
+ };
88
+ this.linkDefaults = {
89
+ color: theme.edgeColor,
90
+ width: theme.edgeWidth,
91
+ opacity: theme.edgeOpacity,
92
+ curvature: theme.edgeCurvature,
93
+ style: styleCode(theme.edgeStyle),
94
+ arrow: theme.edgeArrowEnabled ? 1 : -1,
95
+ arrowSize: theme.edgeArrowSize,
96
+ arrowWidth: theme.edgeArrowWidth,
97
+ };
98
+ this.palette = theme.nodeColorPalette;
99
+ this.nodeCapacity = Math.max(16, options.nodeCapacity ?? DEFAULT_NODE_CAPACITY);
100
+ this.linkCapacity = Math.max(16, options.linkCapacity ?? DEFAULT_LINK_CAPACITY);
101
+ this.x = new Float32Array(this.nodeCapacity);
102
+ this.y = new Float32Array(this.nodeCapacity);
103
+ this.px = new Float32Array(this.nodeCapacity);
104
+ this.py = new Float32Array(this.nodeCapacity);
105
+ this.vx = new Float32Array(this.nodeCapacity);
106
+ this.vy = new Float32Array(this.nodeCapacity);
107
+ this.fx = new Float32Array(this.nodeCapacity);
108
+ this.fy = new Float32Array(this.nodeCapacity);
109
+ this.radius = new Float32Array(this.nodeCapacity);
110
+ this.visualRadius = new Float32Array(this.nodeCapacity);
111
+ this.mass = new Float32Array(this.nodeCapacity);
112
+ this.degree = new Int32Array(this.nodeCapacity);
113
+ this.color = new Uint32Array(this.nodeCapacity);
114
+ this.borderColor = new Uint32Array(this.nodeCapacity);
115
+ this.borderWidth = new Float32Array(this.nodeCapacity);
116
+ this.nodeOpacity = new Float32Array(this.nodeCapacity);
117
+ this.nodeImage = new Array(this.nodeCapacity).fill(null);
118
+ this.nodeImageMode = new Uint8Array(this.nodeCapacity);
119
+ this.nodeLabel = new Array(this.nodeCapacity).fill(null);
120
+ this.flags = new Uint8Array(this.nodeCapacity);
121
+ this.state = new Uint8Array(this.nodeCapacity);
122
+ this.source = new Int32Array(this.linkCapacity);
123
+ this.target = new Int32Array(this.linkCapacity);
124
+ this.restLength = new Float32Array(this.linkCapacity);
125
+ this.linkStrength = new Float32Array(this.linkCapacity);
126
+ this.linkWidth = new Float32Array(this.linkCapacity);
127
+ this.linkColor = new Uint32Array(this.linkCapacity);
128
+ this.linkFlags = new Uint8Array(this.linkCapacity);
129
+ this.linkOpacity = new Float32Array(this.linkCapacity);
130
+ this.linkCurvature = new Float32Array(this.linkCapacity);
131
+ this.linkStyle = new Uint8Array(this.linkCapacity);
132
+ this.dashLength = new Float32Array(this.linkCapacity);
133
+ this.dashGap = new Float32Array(this.linkCapacity);
134
+ this.dashOffset = new Float32Array(this.linkCapacity);
135
+ this.dashWorldScale = new Uint8Array(this.linkCapacity);
136
+ this.arrowMode = new Int8Array(this.linkCapacity);
137
+ this.arrowSize = new Float32Array(this.linkCapacity);
138
+ this.arrowWidth = new Float32Array(this.linkCapacity);
139
+ }
140
+ /** Resolves a node id or raw index into a dense index, or returns -1. */
141
+ nodeIndexOf(idOrIndex) {
142
+ if (typeof idOrIndex === 'number' && Number.isInteger(idOrIndex) && idOrIndex >= 0 && idOrIndex < this.nodeCount && !this.nodeIndexById.has(idOrIndex)) {
143
+ return idOrIndex;
144
+ }
145
+ return this.nodeIndexById.get(idOrIndex) ?? -1;
146
+ }
147
+ /** Resolves a link id or raw index into a dense index, or returns -1. */
148
+ linkIndexOf(idOrIndex) {
149
+ if (typeof idOrIndex === 'number' && Number.isInteger(idOrIndex) && idOrIndex >= 0 && idOrIndex < this.linkCount && !this.linkIndexById.has(idOrIndex)) {
150
+ return idOrIndex;
151
+ }
152
+ return this.linkIndexById.get(idOrIndex) ?? -1;
153
+ }
154
+ /** Returns the external id attached to a dense node index. */
155
+ nodeIdAt(index) {
156
+ return this.nodeIds[index];
157
+ }
158
+ /** Returns the external id attached to a dense link index. */
159
+ linkIdAt(index) {
160
+ return this.linkIds[index];
161
+ }
162
+ /** Adds a node or updates an existing one and returns its dense index. */
163
+ addNode(id, options = {}) {
164
+ const existing = this.nodeIndexById.get(id);
165
+ if (existing !== undefined) {
166
+ this.applyNodeOptions(existing, options, false);
167
+ return existing;
168
+ }
169
+ const index = this.nodeCount++;
170
+ this.ensureNodeCapacity(this.nodeCount);
171
+ this.nodeIds[index] = id;
172
+ this.nodeIndexById.set(id, index);
173
+ this.nodeData[index] = options.data ?? {};
174
+ const hasPosition = typeof options.x === 'number' && Number.isFinite(options.x) && typeof options.y === 'number' && Number.isFinite(options.y);
175
+ if (hasPosition) {
176
+ this.x[index] = options.x;
177
+ this.y[index] = options.y;
178
+ }
179
+ else {
180
+ const angle = Math.random() * Math.PI * 2;
181
+ const distance = 60 + Math.sqrt(this.nodeCount) * 24;
182
+ this.x[index] = Math.cos(angle) * distance;
183
+ this.y[index] = Math.sin(angle) * distance;
184
+ }
185
+ this.px[index] = this.x[index];
186
+ this.py[index] = this.y[index];
187
+ this.vx[index] = 0;
188
+ this.vy[index] = 0;
189
+ this.fx[index] = 0;
190
+ this.fy[index] = 0;
191
+ this.degree[index] = 0;
192
+ this.state[index] = STATE_NORMAL;
193
+ this.flags[index] = options.hidden ? NODE_HIDDEN : 0;
194
+ this.applyNodeOptions(index, options, true);
195
+ this.topologyRevision++;
196
+ return index;
197
+ }
198
+ /** Adds a link or updates an existing one and returns its dense index. */
199
+ addLink(id, sourceId, targetId, options = {}) {
200
+ const existing = this.linkIndexById.get(id);
201
+ if (existing !== undefined) {
202
+ this.applyLinkOptions(existing, options, false);
203
+ return existing;
204
+ }
205
+ const source = this.nodeIndexOf(sourceId);
206
+ const target = this.nodeIndexOf(targetId);
207
+ if (source < 0 || target < 0) {
208
+ throw new Error(`Graphium cannot link "${String(sourceId)}" to "${String(targetId)}" because a node is missing.`);
209
+ }
210
+ const index = this.linkCount++;
211
+ this.ensureLinkCapacity(this.linkCount);
212
+ this.linkIds[index] = id;
213
+ this.linkIndexById.set(id, index);
214
+ this.linkData[index] = options.data ?? {};
215
+ this.source[index] = source;
216
+ this.target[index] = target;
217
+ const dx = this.x[target] - this.x[source];
218
+ const dy = this.y[target] - this.y[source];
219
+ this.restLength[index] = Math.max(1, Math.hypot(dx, dy) || 90);
220
+ this.linkStrength[index] = 1;
221
+ this.linkFlags[index] = options.hidden ? LINK_HIDDEN : 0;
222
+ this.arrowMode[index] = 0;
223
+ this.degree[source]++;
224
+ this.degree[target]++;
225
+ this.applyLinkOptions(index, options, true);
226
+ this.topologyRevision++;
227
+ return index;
228
+ }
229
+ /** Applies node level appearance and physics properties to an index. */
230
+ applyNodeOptions(index, options, isNew) {
231
+ const defaults = this.nodeDefaults;
232
+ if (isNew || options.radius !== undefined) {
233
+ this.radius[index] = Math.max(0.5, finiteOr(options.radius, defaults.radius));
234
+ this.visualRadius[index] = this.radius[index];
235
+ }
236
+ if (isNew || options.visualRadius !== undefined) {
237
+ this.visualRadius[index] = Math.max(0.5, finiteOr(options.visualRadius, this.radius[index]));
238
+ }
239
+ if (isNew || options.mass !== undefined) {
240
+ this.mass[index] = Math.max(0.0001, finiteOr(options.mass, 1));
241
+ }
242
+ const fill = options.color;
243
+ if (isNew || fill !== undefined) {
244
+ this.color[index] = fill === undefined ? (this.palette.length ? deterministicColor(this.nodeIds[index] ?? index, this.palette) : DEFAULT_COLOR) : packRGBA(fill, DEFAULT_COLOR);
245
+ }
246
+ if (isNew || options.borderColor !== undefined) {
247
+ this.borderColor[index] = packRGBA(options.borderColor, defaults.borderColor);
248
+ }
249
+ if (isNew || options.borderWidth !== undefined) {
250
+ this.borderWidth[index] = Math.max(0, finiteOr(options.borderWidth, defaults.borderWidth));
251
+ }
252
+ if (isNew || options.opacity !== undefined) {
253
+ this.nodeOpacity[index] = clamp01(finiteOr(options.opacity, defaults.opacity));
254
+ }
255
+ if (isNew || options.image !== undefined) {
256
+ this.nodeImage[index] = options.image ? String(options.image) : null;
257
+ }
258
+ if (isNew || options.imageMode !== undefined) {
259
+ this.nodeImageMode[index] = options.imageMode === 'contain' ? 1 : 0;
260
+ }
261
+ if (isNew || options.label !== undefined) {
262
+ this.nodeLabel[index] = options.label === undefined || options.label === null ? null : String(options.label);
263
+ }
264
+ if (options.fixed !== undefined) {
265
+ if (options.fixed) {
266
+ this.flags[index] |= NODE_FIXED;
267
+ this.state[index] = STATE_FIXED;
268
+ }
269
+ else {
270
+ this.flags[index] &= ~NODE_FIXED;
271
+ if (this.state[index] === STATE_FIXED) {
272
+ this.state[index] = STATE_NORMAL;
273
+ }
274
+ }
275
+ }
276
+ if (options.disabled !== undefined) {
277
+ if (options.disabled) {
278
+ this.flags[index] |= NODE_DISABLED;
279
+ this.state[index] = STATE_DISABLED;
280
+ }
281
+ else {
282
+ this.flags[index] &= ~NODE_DISABLED;
283
+ if (this.state[index] === STATE_DISABLED) {
284
+ this.state[index] = STATE_NORMAL;
285
+ }
286
+ }
287
+ }
288
+ if (options.hidden !== undefined) {
289
+ if (options.hidden) {
290
+ this.flags[index] |= NODE_HIDDEN;
291
+ }
292
+ else {
293
+ this.flags[index] &= ~NODE_HIDDEN;
294
+ }
295
+ }
296
+ if (options.state !== undefined) {
297
+ this.setNodeState(index, options.state);
298
+ }
299
+ this.appearanceRevision++;
300
+ }
301
+ /** Applies link level appearance and physics properties to an index. */
302
+ applyLinkOptions(index, options, isNew) {
303
+ const defaults = this.linkDefaults;
304
+ if (isNew || options.restLength !== undefined) {
305
+ this.restLength[index] = Math.max(1, finiteOr(options.restLength, this.restLength[index]));
306
+ }
307
+ if (isNew || options.strength !== undefined) {
308
+ this.linkStrength[index] = Math.max(0.0001, finiteOr(options.strength, 1));
309
+ }
310
+ if (isNew || options.color !== undefined) {
311
+ this.linkColor[index] = packRGBA(options.color, defaults.color);
312
+ }
313
+ if (isNew || options.width !== undefined) {
314
+ this.linkWidth[index] = Math.max(0.05, finiteOr(options.width, defaults.width));
315
+ }
316
+ if (isNew || options.opacity !== undefined) {
317
+ this.linkOpacity[index] = clamp01(finiteOr(options.opacity, defaults.opacity));
318
+ }
319
+ if (isNew || options.curvature !== undefined) {
320
+ this.linkCurvature[index] = Math.max(-1.5, Math.min(1.5, finiteOr(options.curvature, defaults.curvature)));
321
+ }
322
+ if (isNew || options.style !== undefined) {
323
+ this.linkStyle[index] = styleCode(options.style ?? 'solid');
324
+ }
325
+ if (isNew || options.dashPattern !== undefined) {
326
+ const pattern = options.dashPattern ?? [6, 8];
327
+ this.dashLength[index] = Math.max(0.25, finiteOr(pattern[0], 6));
328
+ this.dashGap[index] = Math.max(0.25, finiteOr(pattern[1], 8));
329
+ }
330
+ if (isNew || options.dashOffset !== undefined) {
331
+ this.dashOffset[index] = finiteOr(options.dashOffset, 0);
332
+ }
333
+ if (isNew || options.dashWorldScale !== undefined) {
334
+ this.dashWorldScale[index] = options.dashWorldScale ? 1 : 0;
335
+ }
336
+ if (options.arrow !== undefined) {
337
+ this.arrowMode[index] = options.arrow ? 1 : -1;
338
+ }
339
+ if (isNew || options.arrowSize !== undefined) {
340
+ this.arrowSize[index] = Math.max(0, finiteOr(options.arrowSize, defaults.arrowSize));
341
+ }
342
+ if (isNew || options.arrowWidth !== undefined) {
343
+ this.arrowWidth[index] = Math.max(0, finiteOr(options.arrowWidth, defaults.arrowWidth));
344
+ }
345
+ if (options.hidden !== undefined) {
346
+ if (options.hidden) {
347
+ this.linkFlags[index] |= LINK_HIDDEN;
348
+ }
349
+ else {
350
+ this.linkFlags[index] &= ~LINK_HIDDEN;
351
+ }
352
+ }
353
+ this.appearanceRevision++;
354
+ }
355
+ /** Writes a visual state code and keeps the flag mask synchronized. */
356
+ setNodeState(index, value) {
357
+ if (index < 0 || index >= this.nodeCount) {
358
+ return;
359
+ }
360
+ const code = stateFromName(value);
361
+ this.state[index] = code;
362
+ this.flags[index] &= ~(NODE_SELECTED | NODE_DRAGGED | NODE_DISABLED);
363
+ if (code === STATE_SELECTED) {
364
+ this.flags[index] |= NODE_SELECTED;
365
+ }
366
+ else if (code === STATE_DRAGGING) {
367
+ this.flags[index] |= NODE_DRAGGED;
368
+ }
369
+ else if (code === STATE_DISABLED) {
370
+ this.flags[index] |= NODE_DISABLED;
371
+ }
372
+ this.appearanceRevision++;
373
+ }
374
+ /** Updates node appearance properties without touching topology. */
375
+ updateNode(index, options) {
376
+ if (index >= 0 && index < this.nodeCount) {
377
+ this.applyNodeOptions(index, options, false);
378
+ }
379
+ }
380
+ /** Updates link appearance properties without touching topology. */
381
+ updateLink(index, options) {
382
+ if (index >= 0 && index < this.linkCount) {
383
+ this.applyLinkOptions(index, options, false);
384
+ }
385
+ }
386
+ /** Sets node world position and optionally resets Verlet history. */
387
+ setNodePosition(index, x, y, resetPrevious = false) {
388
+ if (index < 0 || index >= this.nodeCount) {
389
+ return;
390
+ }
391
+ this.x[index] = x;
392
+ this.y[index] = y;
393
+ if (resetPrevious) {
394
+ this.px[index] = x;
395
+ this.py[index] = y;
396
+ this.vx[index] = 0;
397
+ this.vy[index] = 0;
398
+ }
399
+ this.positionRevision++;
400
+ }
401
+ /** Locks or unlocks a node inside the physics solver. */
402
+ setNodeFixed(index, fixed) {
403
+ if (index < 0 || index >= this.nodeCount) {
404
+ return;
405
+ }
406
+ if (fixed) {
407
+ this.flags[index] |= NODE_FIXED;
408
+ if (this.state[index] === STATE_NORMAL) {
409
+ this.state[index] = STATE_FIXED;
410
+ }
411
+ }
412
+ else {
413
+ this.flags[index] &= ~NODE_FIXED;
414
+ if (this.state[index] === STATE_FIXED) {
415
+ this.state[index] = STATE_NORMAL;
416
+ }
417
+ }
418
+ this.appearanceRevision++;
419
+ }
420
+ /** Toggles the selected flag of a node. */
421
+ setNodeSelected(index, selected) {
422
+ if (index < 0 || index >= this.nodeCount) {
423
+ return;
424
+ }
425
+ if (selected) {
426
+ this.flags[index] |= NODE_SELECTED;
427
+ this.state[index] = STATE_SELECTED;
428
+ }
429
+ else {
430
+ this.flags[index] &= ~NODE_SELECTED;
431
+ if (this.state[index] === STATE_SELECTED) {
432
+ this.state[index] = STATE_NORMAL;
433
+ }
434
+ }
435
+ this.appearanceRevision++;
436
+ }
437
+ /** Toggles the selected flag of a link. */
438
+ setLinkSelected(index, selected) {
439
+ if (index < 0 || index >= this.linkCount) {
440
+ return;
441
+ }
442
+ if (selected) {
443
+ this.linkFlags[index] |= LINK_SELECTED;
444
+ }
445
+ else {
446
+ this.linkFlags[index] &= ~LINK_SELECTED;
447
+ }
448
+ this.appearanceRevision++;
449
+ }
450
+ /** Removes a link using swap-remove and returns whether it existed. */
451
+ removeLink(id) {
452
+ const index = this.linkIndexById.get(id);
453
+ if (index === undefined) {
454
+ return false;
455
+ }
456
+ const last = this.linkCount - 1;
457
+ this.degree[this.source[index]]--;
458
+ this.degree[this.target[index]]--;
459
+ this.linkIndexById.delete(id);
460
+ if (index !== last) {
461
+ const movedId = this.linkIds[last];
462
+ this.linkIds[index] = movedId;
463
+ this.linkIndexById.set(movedId, index);
464
+ this.linkData[index] = this.linkData[last];
465
+ this.source[index] = this.source[last];
466
+ this.target[index] = this.target[last];
467
+ this.restLength[index] = this.restLength[last];
468
+ this.linkStrength[index] = this.linkStrength[last];
469
+ this.linkWidth[index] = this.linkWidth[last];
470
+ this.linkColor[index] = this.linkColor[last];
471
+ this.linkFlags[index] = this.linkFlags[last];
472
+ this.linkOpacity[index] = this.linkOpacity[last];
473
+ this.linkCurvature[index] = this.linkCurvature[last];
474
+ this.linkStyle[index] = this.linkStyle[last];
475
+ this.dashLength[index] = this.dashLength[last];
476
+ this.dashGap[index] = this.dashGap[last];
477
+ this.dashOffset[index] = this.dashOffset[last];
478
+ this.dashWorldScale[index] = this.dashWorldScale[last];
479
+ this.arrowMode[index] = this.arrowMode[last];
480
+ this.arrowSize[index] = this.arrowSize[last];
481
+ this.arrowWidth[index] = this.arrowWidth[last];
482
+ }
483
+ this.linkCount--;
484
+ this.topologyRevision++;
485
+ this.appearanceRevision++;
486
+ return true;
487
+ }
488
+ /** Removes every link touching a node before the node itself is removed. */
489
+ removeLinksOf(index) {
490
+ for (let link = this.linkCount - 1; link >= 0; link--) {
491
+ if (this.source[link] === index || this.target[link] === index) {
492
+ this.removeLink(this.linkIds[link]);
493
+ }
494
+ }
495
+ }
496
+ /** Removes a node using swap-remove and reindexes surviving links. */
497
+ removeNode(id) {
498
+ const index = this.nodeIndexById.get(id);
499
+ if (index === undefined) {
500
+ return false;
501
+ }
502
+ this.removeLinksOf(index);
503
+ const last = this.nodeCount - 1;
504
+ this.nodeIndexById.delete(id);
505
+ if (index !== last) {
506
+ const movedId = this.nodeIds[last];
507
+ this.nodeIds[index] = movedId;
508
+ this.nodeIndexById.set(movedId, index);
509
+ this.nodeData[index] = this.nodeData[last];
510
+ this.x[index] = this.x[last];
511
+ this.y[index] = this.y[last];
512
+ this.px[index] = this.px[last];
513
+ this.py[index] = this.py[last];
514
+ this.vx[index] = this.vx[last];
515
+ this.vy[index] = this.vy[last];
516
+ this.fx[index] = this.fx[last];
517
+ this.fy[index] = this.fy[last];
518
+ this.radius[index] = this.radius[last];
519
+ this.visualRadius[index] = this.visualRadius[last];
520
+ this.mass[index] = this.mass[last];
521
+ this.degree[index] = this.degree[last];
522
+ this.color[index] = this.color[last];
523
+ this.borderColor[index] = this.borderColor[last];
524
+ this.borderWidth[index] = this.borderWidth[last];
525
+ this.nodeOpacity[index] = this.nodeOpacity[last];
526
+ this.nodeImage[index] = this.nodeImage[last];
527
+ this.nodeImageMode[index] = this.nodeImageMode[last];
528
+ this.nodeLabel[index] = this.nodeLabel[last];
529
+ this.flags[index] = this.flags[last];
530
+ this.state[index] = this.state[last];
531
+ for (let link = 0; link < this.linkCount; link++) {
532
+ if (this.source[link] === last) {
533
+ this.source[link] = index;
534
+ }
535
+ if (this.target[link] === last) {
536
+ this.target[link] = index;
537
+ }
538
+ }
539
+ }
540
+ this.nodeIds.length = this.nodeCount - 1;
541
+ this.nodeData.length = this.nodeCount - 1;
542
+ this.nodeImage[this.nodeCount - 1] = null;
543
+ this.nodeLabel[this.nodeCount - 1] = null;
544
+ this.nodeCount--;
545
+ this.topologyRevision++;
546
+ this.appearanceRevision++;
547
+ return true;
548
+ }
549
+ /** Replaces the whole graph with the supplied data payload. */
550
+ setData(data) {
551
+ this.clear();
552
+ const nodes = data.nodes ?? [];
553
+ for (let index = 0; index < nodes.length; index++) {
554
+ const node = nodes[index];
555
+ this.addNode(node.id ?? index, node);
556
+ }
557
+ const links = data.links ?? [];
558
+ for (let index = 0; index < links.length; index++) {
559
+ const link = links[index];
560
+ const linkId = link.id ?? `${String(link.source)}->${String(link.target)}:${index}`;
561
+ this.addLink(linkId, link.source, link.target, link);
562
+ }
563
+ }
564
+ /** Clears topology while keeping allocated storage and current theme. */
565
+ clear() {
566
+ this.nodeCount = 0;
567
+ this.linkCount = 0;
568
+ this.nodeIds.length = 0;
569
+ this.linkIds.length = 0;
570
+ this.nodeData.length = 0;
571
+ this.linkData.length = 0;
572
+ this.nodeIndexById.clear();
573
+ this.linkIndexById.clear();
574
+ this.topologyRevision++;
575
+ this.appearanceRevision++;
576
+ this.positionRevision++;
577
+ }
578
+ /** Builds a readable snapshot object for one node. */
579
+ nodeSnapshot(index) {
580
+ return {
581
+ id: this.nodeIds[index],
582
+ index,
583
+ x: this.x[index],
584
+ y: this.y[index],
585
+ vx: this.vx[index],
586
+ vy: this.vy[index],
587
+ radius: this.radius[index],
588
+ visualRadius: this.visualRadius[index],
589
+ mass: this.mass[index],
590
+ degree: this.degree[index],
591
+ color: this.color[index],
592
+ borderColor: this.borderColor[index],
593
+ borderWidth: this.borderWidth[index],
594
+ opacity: this.nodeOpacity[index],
595
+ image: this.nodeImage[index],
596
+ label: this.nodeLabel[index],
597
+ state: nameFromState(this.state[index]),
598
+ fixed: (this.flags[index] & NODE_FIXED) !== 0,
599
+ hidden: (this.flags[index] & NODE_HIDDEN) !== 0,
600
+ disabled: (this.flags[index] & NODE_DISABLED) !== 0,
601
+ data: this.nodeData[index] ?? {},
602
+ };
603
+ }
604
+ /** Builds a readable snapshot object for one link. */
605
+ linkSnapshot(index) {
606
+ return {
607
+ id: this.linkIds[index],
608
+ index,
609
+ source: this.nodeIds[this.source[index]],
610
+ target: this.nodeIds[this.target[index]],
611
+ sourceIndex: this.source[index],
612
+ targetIndex: this.target[index],
613
+ color: this.linkColor[index],
614
+ width: this.linkWidth[index],
615
+ opacity: this.linkOpacity[index],
616
+ curvature: this.linkCurvature[index],
617
+ style: styleName(this.linkStyle[index]),
618
+ dashPattern: [this.dashLength[index], this.dashGap[index]],
619
+ dashOffset: this.dashOffset[index],
620
+ arrow: this.arrowMode[index] === 1 ? true : this.arrowMode[index] === -1 ? false : undefined,
621
+ arrowSize: this.arrowSize[index],
622
+ arrowWidth: this.arrowWidth[index],
623
+ strength: this.linkStrength[index],
624
+ restLength: this.restLength[index],
625
+ hidden: (this.linkFlags[index] & LINK_HIDDEN) !== 0,
626
+ data: this.linkData[index] ?? {},
627
+ };
628
+ }
629
+ /** Returns the custom data object attached to a node. */
630
+ nodeUserData(index) {
631
+ return this.nodeData[index] ?? (this.nodeData[index] = {});
632
+ }
633
+ /** Returns the custom data object attached to a link. */
634
+ linkUserData(index) {
635
+ return this.linkData[index] ?? (this.linkData[index] = {});
636
+ }
637
+ /** Doubles node storage when the dense count would overflow. */
638
+ ensureNodeCapacity(required) {
639
+ if (required <= this.nodeCapacity) {
640
+ return;
641
+ }
642
+ let capacity = this.nodeCapacity;
643
+ while (capacity < required) {
644
+ capacity *= 2;
645
+ }
646
+ this.x = grow(this.x, capacity);
647
+ this.y = grow(this.y, capacity);
648
+ this.px = grow(this.px, capacity);
649
+ this.py = grow(this.py, capacity);
650
+ this.vx = grow(this.vx, capacity);
651
+ this.vy = grow(this.vy, capacity);
652
+ this.fx = grow(this.fx, capacity);
653
+ this.fy = grow(this.fy, capacity);
654
+ this.radius = grow(this.radius, capacity);
655
+ this.visualRadius = grow(this.visualRadius, capacity);
656
+ this.mass = grow(this.mass, capacity);
657
+ this.degree = growInt(this.degree, capacity);
658
+ this.color = growUint32(this.color, capacity);
659
+ this.borderColor = growUint32(this.borderColor, capacity);
660
+ this.borderWidth = grow(this.borderWidth, capacity);
661
+ this.nodeOpacity = grow(this.nodeOpacity, capacity);
662
+ this.nodeImage = growObjects(this.nodeImage, capacity);
663
+ this.nodeImageMode = growUint8(this.nodeImageMode, capacity);
664
+ this.nodeLabel = growObjects(this.nodeLabel, capacity);
665
+ this.flags = growUint8(this.flags, capacity);
666
+ this.state = growUint8(this.state, capacity);
667
+ this.nodeCapacity = capacity;
668
+ }
669
+ /** Doubles link storage when the dense count would overflow. */
670
+ ensureLinkCapacity(required) {
671
+ if (required <= this.linkCapacity) {
672
+ return;
673
+ }
674
+ let capacity = this.linkCapacity;
675
+ while (capacity < required) {
676
+ capacity *= 2;
677
+ }
678
+ this.source = growInt(this.source, capacity);
679
+ this.target = growInt(this.target, capacity);
680
+ this.restLength = grow(this.restLength, capacity);
681
+ this.linkStrength = grow(this.linkStrength, capacity);
682
+ this.linkWidth = grow(this.linkWidth, capacity);
683
+ this.linkColor = growUint32(this.linkColor, capacity);
684
+ this.linkFlags = growUint8(this.linkFlags, capacity);
685
+ this.linkOpacity = grow(this.linkOpacity, capacity);
686
+ this.linkCurvature = grow(this.linkCurvature, capacity);
687
+ this.linkStyle = growUint8(this.linkStyle, capacity);
688
+ this.dashLength = grow(this.dashLength, capacity);
689
+ this.dashGap = grow(this.dashGap, capacity);
690
+ this.dashOffset = grow(this.dashOffset, capacity);
691
+ this.dashWorldScale = growUint8(this.dashWorldScale, capacity);
692
+ this.arrowMode = growInt8(this.arrowMode, capacity);
693
+ this.arrowSize = grow(this.arrowSize, capacity);
694
+ this.arrowWidth = grow(this.arrowWidth, capacity);
695
+ this.linkCapacity = capacity;
696
+ }
697
+ }
698
+ function grow(array, capacity) {
699
+ const next = new Float32Array(capacity);
700
+ next.set(array);
701
+ return next;
702
+ }
703
+ function growInt(array, capacity) {
704
+ const next = new Int32Array(capacity);
705
+ next.set(array);
706
+ return next;
707
+ }
708
+ function growInt8(array, capacity) {
709
+ const next = new Int8Array(capacity);
710
+ next.set(array);
711
+ return next;
712
+ }
713
+ function growUint8(array, capacity) {
714
+ const next = new Uint8Array(capacity);
715
+ next.set(array);
716
+ return next;
717
+ }
718
+ function growUint32(array, capacity) {
719
+ const next = new Uint32Array(capacity);
720
+ next.set(array);
721
+ return next;
722
+ }
723
+ function growObjects(array, capacity) {
724
+ const next = new Array(capacity).fill(null);
725
+ for (let index = 0; index < array.length; index++) {
726
+ next[index] = array[index];
727
+ }
728
+ return next;
729
+ }
730
+ //# sourceMappingURL=GraphModel.js.map