aifastdb-devplan 1.3.9 → 1.6.1

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 (78) hide show
  1. package/dist/autopilot.d.ts +58 -0
  2. package/dist/autopilot.d.ts.map +1 -0
  3. package/dist/autopilot.js +250 -0
  4. package/dist/autopilot.js.map +1 -0
  5. package/dist/dev-plan-document-store.d.ts +2 -0
  6. package/dist/dev-plan-document-store.d.ts.map +1 -1
  7. package/dist/dev-plan-document-store.js +3 -0
  8. package/dist/dev-plan-document-store.js.map +1 -1
  9. package/dist/dev-plan-factory.d.ts +69 -3
  10. package/dist/dev-plan-factory.d.ts.map +1 -1
  11. package/dist/dev-plan-factory.js +111 -18
  12. package/dist/dev-plan-factory.js.map +1 -1
  13. package/dist/dev-plan-graph-store.d.ts +15 -0
  14. package/dist/dev-plan-graph-store.d.ts.map +1 -1
  15. package/dist/dev-plan-graph-store.js +57 -2
  16. package/dist/dev-plan-graph-store.js.map +1 -1
  17. package/dist/index.d.ts +3 -2
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +14 -1
  20. package/dist/index.js.map +1 -1
  21. package/dist/mcp-server/index.d.ts +3 -0
  22. package/dist/mcp-server/index.d.ts.map +1 -1
  23. package/dist/mcp-server/index.js +278 -4
  24. package/dist/mcp-server/index.js.map +1 -1
  25. package/dist/types.d.ts +72 -0
  26. package/dist/types.d.ts.map +1 -1
  27. package/dist/types.js +9 -1
  28. package/dist/types.js.map +1 -1
  29. package/dist/visualize/graph-canvas/api-compat.d.ts +20 -0
  30. package/dist/visualize/graph-canvas/api-compat.d.ts.map +1 -0
  31. package/dist/visualize/graph-canvas/api-compat.js +334 -0
  32. package/dist/visualize/graph-canvas/api-compat.js.map +1 -0
  33. package/dist/visualize/graph-canvas/clusterer.d.ts +16 -0
  34. package/dist/visualize/graph-canvas/clusterer.d.ts.map +1 -0
  35. package/dist/visualize/graph-canvas/clusterer.js +460 -0
  36. package/dist/visualize/graph-canvas/clusterer.js.map +1 -0
  37. package/dist/visualize/graph-canvas/core.d.ts +11 -0
  38. package/dist/visualize/graph-canvas/core.d.ts.map +1 -0
  39. package/dist/visualize/graph-canvas/core.js +844 -0
  40. package/dist/visualize/graph-canvas/core.js.map +1 -0
  41. package/dist/visualize/graph-canvas/index.d.ts +22 -0
  42. package/dist/visualize/graph-canvas/index.d.ts.map +1 -0
  43. package/dist/visualize/graph-canvas/index.js +69 -0
  44. package/dist/visualize/graph-canvas/index.js.map +1 -0
  45. package/dist/visualize/graph-canvas/interaction.d.ts +13 -0
  46. package/dist/visualize/graph-canvas/interaction.d.ts.map +1 -0
  47. package/dist/visualize/graph-canvas/interaction.js +446 -0
  48. package/dist/visualize/graph-canvas/interaction.js.map +1 -0
  49. package/dist/visualize/graph-canvas/layout-worker.d.ts +17 -0
  50. package/dist/visualize/graph-canvas/layout-worker.d.ts.map +1 -0
  51. package/dist/visualize/graph-canvas/layout-worker.js +541 -0
  52. package/dist/visualize/graph-canvas/layout-worker.js.map +1 -0
  53. package/dist/visualize/graph-canvas/lod.d.ts +10 -0
  54. package/dist/visualize/graph-canvas/lod.d.ts.map +1 -0
  55. package/dist/visualize/graph-canvas/lod.js +111 -0
  56. package/dist/visualize/graph-canvas/lod.js.map +1 -0
  57. package/dist/visualize/graph-canvas/renderer.d.ts +12 -0
  58. package/dist/visualize/graph-canvas/renderer.d.ts.map +1 -0
  59. package/dist/visualize/graph-canvas/renderer.js +682 -0
  60. package/dist/visualize/graph-canvas/renderer.js.map +1 -0
  61. package/dist/visualize/graph-canvas/spatial-index.d.ts +13 -0
  62. package/dist/visualize/graph-canvas/spatial-index.d.ts.map +1 -0
  63. package/dist/visualize/graph-canvas/spatial-index.js +482 -0
  64. package/dist/visualize/graph-canvas/spatial-index.js.map +1 -0
  65. package/dist/visualize/graph-canvas/styles.d.ts +11 -0
  66. package/dist/visualize/graph-canvas/styles.d.ts.map +1 -0
  67. package/dist/visualize/graph-canvas/styles.js +137 -0
  68. package/dist/visualize/graph-canvas/styles.js.map +1 -0
  69. package/dist/visualize/graph-canvas/viewport.d.ts +17 -0
  70. package/dist/visualize/graph-canvas/viewport.d.ts.map +1 -0
  71. package/dist/visualize/graph-canvas/viewport.js +375 -0
  72. package/dist/visualize/graph-canvas/viewport.js.map +1 -0
  73. package/dist/visualize/server.js +619 -6
  74. package/dist/visualize/server.js.map +1 -1
  75. package/dist/visualize/template.d.ts.map +1 -1
  76. package/dist/visualize/template.js +604 -18
  77. package/dist/visualize/template.js.map +1 -1
  78. package/package.json +1 -1
@@ -0,0 +1,844 @@
1
+ "use strict";
2
+ /**
3
+ * GraphCanvas Core — Canvas2D 渲染器骨架
4
+ *
5
+ * 职责:
6
+ * - 创建/管理 Canvas 元素
7
+ * - 维护节点/边数据
8
+ * - requestAnimationFrame 渲染循环
9
+ * - 协调各子模块 (Viewport, SpatialIndex, Renderer, etc.)
10
+ */
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.getCoreScript = getCoreScript;
13
+ function getCoreScript() {
14
+ return `
15
+ // ============================================================================
16
+ // GraphCanvas Core
17
+ // ============================================================================
18
+
19
+ /**
20
+ * GraphCanvas — 高性能 Canvas2D 图谱渲染引擎
21
+ *
22
+ * @param {HTMLElement} container - 容器 DOM 元素
23
+ * @param {Object} options - 配置选项
24
+ */
25
+ function GraphCanvas(container, options) {
26
+ if (!container) throw new Error('GraphCanvas: container is required');
27
+
28
+ this._container = container;
29
+ this._options = Object.assign({
30
+ pixelRatio: Math.min(window.devicePixelRatio || 1, 2),
31
+ backgroundColor: '#111827',
32
+ maxFPS: 60,
33
+ debugMode: false,
34
+ }, options || {});
35
+
36
+ // ── Canvas Setup ──────────────────────────────────────────────────────
37
+ this._canvas = document.createElement('canvas');
38
+ this._canvas.style.cssText = 'width:100%;height:100%;display:block;outline:none;';
39
+ this._canvas.tabIndex = 0; // focusable for keyboard events
40
+ container.appendChild(this._canvas);
41
+ this._ctx = this._canvas.getContext('2d');
42
+
43
+ // ── Data ──────────────────────────────────────────────────────────────
44
+ this._nodes = []; // GraphNode[] — all nodes
45
+ this._edges = []; // GraphEdge[] — all edges
46
+ this._nodeMap = {}; // id → node (O(1) lookup)
47
+ this._edgeMap = {}; // id → edge
48
+ this._nodeEdges = {}; // nodeId → [edge, ...] (adjacency list)
49
+ this._nodeCount = 0;
50
+ this._edgeCount = 0;
51
+
52
+ // ── Sub-modules (created lazily or during init) ───────────────────────
53
+ this._viewport = new ViewportManager(this);
54
+ this._spatialIndex = new SpatialIndex();
55
+ this._renderer = new RenderPipeline(this);
56
+ this._lod = new LODManager(this);
57
+ this._interaction = new InteractionManager(this);
58
+ this._layoutEngine = null; // Created on demand
59
+ this._clusterer = null; // Created on demand
60
+ this._styles = new StyleManager();
61
+
62
+ // ── Render Loop State ─────────────────────────────────────────────────
63
+ this._rafId = null;
64
+ this._dirty = true; // needs full redraw
65
+ this._dirtyRects = []; // partial redraw regions [{x,y,w,h}, ...]
66
+ this._lastFrameTime = 0;
67
+ this._frameInterval = 1000 / this._options.maxFPS;
68
+ this._running = false;
69
+ this._sleeping = false; // true = render loop paused (no rAF)
70
+ this._frameCount = 0;
71
+ this._fpsTimer = 0;
72
+ this._currentFPS = 0;
73
+ this._idleFrames = 0; // consecutive frames with nothing to draw
74
+ this._idleThreshold = 30; // sleep after N idle frames (~0.5s at 60fps)
75
+
76
+ // ── Event Callbacks ───────────────────────────────────────────────────
77
+ this._eventHandlers = {}; // eventName → [callback, ...]
78
+
79
+ // ── Performance Metrics ───────────────────────────────────────────────
80
+ this._metrics = {
81
+ lastRenderMs: 0,
82
+ visibleNodes: 0,
83
+ visibleEdges: 0,
84
+ fps: 0,
85
+ totalNodes: 0,
86
+ totalEdges: 0,
87
+ visibleClusters: 0,
88
+ layoutProgress: 0,
89
+ layoutAlgorithm: '',
90
+ layoutEta: 0,
91
+ };
92
+
93
+ // Listen for layout progress events to store in metrics
94
+ var self = this;
95
+ this.on('layoutProgress', function(data) {
96
+ self._metrics.layoutProgress = data.percent || 0;
97
+ self._metrics.layoutAlgorithm = data.algorithm || '';
98
+ self._metrics.layoutEta = data.eta || 0;
99
+ self.markDirty(); // Ensure progress bar redraws
100
+ });
101
+ this.on('stabilizationDone', function() {
102
+ self._metrics.layoutProgress = 0;
103
+ self._metrics.layoutAlgorithm = '';
104
+ self._metrics.layoutEta = 0;
105
+ self.markDirty();
106
+ });
107
+
108
+ // ── Initialize ────────────────────────────────────────────────────────
109
+ this._resizeObserver = null;
110
+ this._handleResize();
111
+ this._setupResizeObserver();
112
+ }
113
+
114
+ // ── Canvas Sizing ─────────────────────────────────────────────────────────
115
+ GraphCanvas.prototype._handleResize = function() {
116
+ var rect = this._container.getBoundingClientRect();
117
+ var pr = this._options.pixelRatio;
118
+ var w = Math.max(rect.width, 1);
119
+ var h = Math.max(rect.height, 1);
120
+
121
+ this._canvas.width = w * pr;
122
+ this._canvas.height = h * pr;
123
+ this._width = w;
124
+ this._height = h;
125
+
126
+ // Scale context for HiDPI
127
+ this._ctx.setTransform(pr, 0, 0, pr, 0, 0);
128
+
129
+ this.markDirty();
130
+ };
131
+
132
+ GraphCanvas.prototype._setupResizeObserver = function() {
133
+ var self = this;
134
+ if (typeof ResizeObserver !== 'undefined') {
135
+ this._resizeObserver = new ResizeObserver(function() {
136
+ self._handleResize();
137
+ });
138
+ this._resizeObserver.observe(this._container);
139
+ } else {
140
+ // Fallback for older browsers
141
+ window.addEventListener('resize', function() { self._handleResize(); });
142
+ }
143
+ };
144
+
145
+ // ── Data Management ───────────────────────────────────────────────────────
146
+
147
+ /**
148
+ * Set graph data (bulk load).
149
+ * @param {{ nodes: Array, edges: Array }} data
150
+ */
151
+ GraphCanvas.prototype.setData = function(data) {
152
+ var nodes = data.nodes || [];
153
+ var edges = data.edges || [];
154
+
155
+ // Clear previous data
156
+ this._nodes = [];
157
+ this._edges = [];
158
+ this._nodeMap = {};
159
+ this._edgeMap = {};
160
+ this._nodeEdges = {};
161
+ this._nodeIndexMap = {}; // id → array index (for TypedArray)
162
+
163
+ // ── TypedArray backing store (Phase-8C) ──
164
+ // Float32Array for positions: [x0, y0, x1, y1, ...] (stride=2)
165
+ // Float32Array for properties: [radius0, degree0, radius1, degree1, ...] (stride=2)
166
+ var nodeCount = nodes.length;
167
+ this._positionArray = new Float32Array(nodeCount * 2);
168
+ this._propertyArray = new Float32Array(nodeCount * 2);
169
+
170
+ // Process nodes
171
+ for (var i = 0; i < nodeCount; i++) {
172
+ var n = nodes[i];
173
+ var px = n.x != null ? n.x : 0;
174
+ var py = n.y != null ? n.y : 0;
175
+ this._positionArray[i * 2] = px;
176
+ this._positionArray[i * 2 + 1] = py;
177
+ this._propertyArray[i * 2] = 10; // default radius
178
+ this._propertyArray[i * 2 + 1] = n.degree || 0;
179
+
180
+ var node = {
181
+ id: n.id,
182
+ label: n.label || n.id,
183
+ type: n.type || 'default',
184
+ x: px,
185
+ y: py,
186
+ _idx: i, // TypedArray index
187
+ // Computed fields
188
+ _screenX: 0,
189
+ _screenY: 0,
190
+ _radius: 10,
191
+ _visible: true,
192
+ _lodLevel: 2, // 0=minimal, 1=standard, 2=detailed
193
+ _hovered: false,
194
+ _selected: false,
195
+ _dragging: false,
196
+ _clustered: false,
197
+ _style: null, // cached style
198
+ _aabb: null, // { minX, minY, maxX, maxY } for R-tree
199
+ // Original data
200
+ properties: n.properties || {},
201
+ degree: n.degree || 0,
202
+ _origData: n,
203
+ };
204
+ this._nodes.push(node);
205
+ this._nodeMap[node.id] = node;
206
+ this._nodeIndexMap[node.id] = i;
207
+ this._nodeEdges[node.id] = [];
208
+ }
209
+
210
+ // Process edges
211
+ for (var i = 0; i < edges.length; i++) {
212
+ var e = edges[i];
213
+ var eid = e.id || ('e_' + i);
214
+ var edge = {
215
+ id: eid,
216
+ from: e.from,
217
+ to: e.to,
218
+ label: e.label || '',
219
+ _visible: true,
220
+ _highlighted: false,
221
+ _style: null,
222
+ _origData: e,
223
+ };
224
+ this._edges.push(edge);
225
+ this._edgeMap[eid] = edge;
226
+ if (this._nodeEdges[e.from]) this._nodeEdges[e.from].push(edge);
227
+ if (this._nodeEdges[e.to]) this._nodeEdges[e.to].push(edge);
228
+ }
229
+
230
+ this._nodeCount = this._nodes.length;
231
+ this._edgeCount = this._edges.length;
232
+ this._metrics.totalNodes = this._nodeCount;
233
+ this._metrics.totalEdges = this._edgeCount;
234
+
235
+ // Apply styles to all nodes/edges
236
+ this._styles.applyAllStyles(this._nodes, this._edges);
237
+
238
+ // Build spatial index (nodes + edges)
239
+ this._spatialIndex.buildFromNodes(this._nodes);
240
+ this._spatialIndex.buildEdgeIndex(this._edges, this._nodeMap);
241
+
242
+ // Auto-enable clusterer for larger datasets (>500 nodes)
243
+ if (this._nodeCount > 500) {
244
+ if (!this._clusterer) {
245
+ this._clusterer = new Clusterer(this);
246
+ }
247
+ this._clusterer.setEnabled(true);
248
+ } else if (this._clusterer) {
249
+ this._clusterer.setEnabled(false);
250
+ }
251
+
252
+ // Assign initial positions if not set (random layout as starting point)
253
+ var hasPositions = false;
254
+ for (var i = 0; i < this._nodes.length; i++) {
255
+ if (this._nodes[i].x !== 0 || this._nodes[i].y !== 0) {
256
+ hasPositions = true;
257
+ break;
258
+ }
259
+ }
260
+ if (!hasPositions && this._nodes.length > 0) {
261
+ this._assignRandomPositions();
262
+ }
263
+
264
+ this.markDirty();
265
+ this._emit('dataLoaded', { nodeCount: this._nodeCount, edgeCount: this._edgeCount });
266
+ };
267
+
268
+ /**
269
+ * Assign random initial positions for layout bootstrapping.
270
+ */
271
+ GraphCanvas.prototype._assignRandomPositions = function() {
272
+ var spread = Math.sqrt(this._nodeCount) * 80;
273
+ for (var i = 0; i < this._nodes.length; i++) {
274
+ this._nodes[i].x = (Math.random() - 0.5) * spread;
275
+ this._nodes[i].y = (Math.random() - 0.5) * spread;
276
+ }
277
+ this._spatialIndex.buildFromNodes(this._nodes);
278
+ this._spatialIndex.buildEdgeIndex(this._edges, this._nodeMap);
279
+ };
280
+
281
+ // ── TypedArray Accessors (Phase-8C) ────────────────────────────────────────
282
+
283
+ /**
284
+ * Get node position from TypedArray (fast path for Worker sync).
285
+ * @param {number} index — node array index
286
+ * @returns {{ x: number, y: number }}
287
+ */
288
+ GraphCanvas.prototype.getNodePos = function(index) {
289
+ return {
290
+ x: this._positionArray[index * 2],
291
+ y: this._positionArray[index * 2 + 1],
292
+ };
293
+ };
294
+
295
+ /**
296
+ * Set node position in both TypedArray and node object.
297
+ * @param {number} index — node array index
298
+ * @param {number} x
299
+ * @param {number} y
300
+ */
301
+ GraphCanvas.prototype.setNodePos = function(index, x, y) {
302
+ this._positionArray[index * 2] = x;
303
+ this._positionArray[index * 2 + 1] = y;
304
+ if (this._nodes[index]) {
305
+ this._nodes[index].x = x;
306
+ this._nodes[index].y = y;
307
+ }
308
+ };
309
+
310
+ /**
311
+ * Bulk sync: copy positions from TypedArray → node objects.
312
+ * Called after Worker returns Transferable ArrayBuffer.
313
+ */
314
+ GraphCanvas.prototype._syncFromPositionArray = function() {
315
+ var arr = this._positionArray;
316
+ var nodes = this._nodes;
317
+ for (var i = 0; i < nodes.length; i++) {
318
+ nodes[i].x = arr[i * 2];
319
+ nodes[i].y = arr[i * 2 + 1];
320
+ }
321
+ };
322
+
323
+ /**
324
+ * Bulk sync: copy positions from node objects → TypedArray.
325
+ * Called before sending to Worker.
326
+ */
327
+ GraphCanvas.prototype._syncToPositionArray = function() {
328
+ var arr = this._positionArray;
329
+ var nodes = this._nodes;
330
+ for (var i = 0; i < nodes.length; i++) {
331
+ arr[i * 2] = nodes[i].x;
332
+ arr[i * 2 + 1] = nodes[i].y;
333
+ }
334
+ };
335
+
336
+ /**
337
+ * Get a transferable copy of the position array for Worker.
338
+ * @returns {Float32Array} — new copy (original stays usable)
339
+ */
340
+ GraphCanvas.prototype.getPositionBuffer = function() {
341
+ return new Float32Array(this._positionArray);
342
+ };
343
+
344
+ // ── Incremental Data API (Phase-8C T8C.5) ─────────────────────────────────
345
+
346
+ /**
347
+ * Incrementally add nodes (batch).
348
+ * Appends to existing data without full rebuild.
349
+ * @param {Array} newNodes — [{id, label, type, x, y, ...}, ...]
350
+ */
351
+ GraphCanvas.prototype.addNodes = function(newNodes) {
352
+ if (!newNodes || newNodes.length === 0) return;
353
+
354
+ var oldCount = this._nodeCount;
355
+ var totalCount = oldCount + newNodes.length;
356
+
357
+ // Grow TypedArrays
358
+ var newPosArray = new Float32Array(totalCount * 2);
359
+ var newPropArray = new Float32Array(totalCount * 2);
360
+ newPosArray.set(this._positionArray);
361
+ newPropArray.set(this._propertyArray);
362
+
363
+ for (var i = 0; i < newNodes.length; i++) {
364
+ var n = newNodes[i];
365
+ var idx = oldCount + i;
366
+ var px = n.x != null ? n.x : (Math.random() - 0.5) * Math.sqrt(totalCount) * 80;
367
+ var py = n.y != null ? n.y : (Math.random() - 0.5) * Math.sqrt(totalCount) * 80;
368
+ newPosArray[idx * 2] = px;
369
+ newPosArray[idx * 2 + 1] = py;
370
+ newPropArray[idx * 2] = 10;
371
+ newPropArray[idx * 2 + 1] = n.degree || 0;
372
+
373
+ var node = {
374
+ id: n.id,
375
+ label: n.label || n.id,
376
+ type: n.type || 'default',
377
+ x: px, y: py,
378
+ _idx: idx,
379
+ _screenX: 0, _screenY: 0,
380
+ _radius: 10, _visible: true, _lodLevel: 2,
381
+ _hovered: false, _selected: false, _dragging: false, _clustered: false,
382
+ _style: null, _aabb: null,
383
+ properties: n.properties || {},
384
+ degree: n.degree || 0,
385
+ _origData: n,
386
+ };
387
+ this._nodes.push(node);
388
+ this._nodeMap[node.id] = node;
389
+ this._nodeIndexMap[node.id] = idx;
390
+ this._nodeEdges[node.id] = [];
391
+
392
+ // Incremental R-tree insert
393
+ this._spatialIndex.insertNode(node);
394
+ }
395
+
396
+ this._positionArray = newPosArray;
397
+ this._propertyArray = newPropArray;
398
+ this._nodeCount = totalCount;
399
+ this._metrics.totalNodes = totalCount;
400
+
401
+ // Apply styles to new nodes
402
+ this._styles.applyAllStyles(this._nodes, this._edges);
403
+
404
+ this.markDirty();
405
+ this._emit('nodesAdded', { count: newNodes.length, total: totalCount });
406
+ };
407
+
408
+ /**
409
+ * Incrementally add edges (batch).
410
+ * @param {Array} newEdges — [{id, from, to, label, ...}, ...]
411
+ */
412
+ GraphCanvas.prototype.addEdges = function(newEdges) {
413
+ if (!newEdges || newEdges.length === 0) return;
414
+
415
+ for (var i = 0; i < newEdges.length; i++) {
416
+ var e = newEdges[i];
417
+ var eid = e.id || ('e_' + (this._edgeCount + i));
418
+ var edge = {
419
+ id: eid,
420
+ from: e.from,
421
+ to: e.to,
422
+ label: e.label || '',
423
+ _visible: true,
424
+ _highlighted: false,
425
+ _style: null,
426
+ _origData: e,
427
+ };
428
+ this._edges.push(edge);
429
+ this._edgeMap[eid] = edge;
430
+ if (this._nodeEdges[e.from]) this._nodeEdges[e.from].push(edge);
431
+ if (this._nodeEdges[e.to]) this._nodeEdges[e.to].push(edge);
432
+
433
+ // Incremental edge R-tree insert
434
+ var fromNode = this._nodeMap[e.from];
435
+ var toNode = this._nodeMap[e.to];
436
+ if (fromNode && toNode) {
437
+ this._spatialIndex.insertEdge(edge, fromNode, toNode);
438
+ }
439
+ }
440
+
441
+ this._edgeCount = this._edges.length;
442
+ this._metrics.totalEdges = this._edgeCount;
443
+ this._styles.applyAllStyles(this._nodes, this._edges);
444
+ this.markDirty();
445
+ this._emit('edgesAdded', { count: newEdges.length, total: this._edgeCount });
446
+ };
447
+
448
+ // ── Dirty Marking ─────────────────────────────────────────────────────────
449
+
450
+ /**
451
+ * Mark the entire canvas as needing redraw.
452
+ */
453
+ GraphCanvas.prototype.markDirty = function() {
454
+ this._dirty = true;
455
+ this._idleFrames = 0;
456
+ this._wakeRenderLoop();
457
+ };
458
+
459
+ /**
460
+ * Mark a specific rectangular region as dirty (world coordinates).
461
+ * @param {number} x - World X
462
+ * @param {number} y - World Y
463
+ * @param {number} w - Width
464
+ * @param {number} h - Height
465
+ */
466
+ GraphCanvas.prototype.markDirtyRect = function(x, y, w, h) {
467
+ this._dirtyRects.push({ x: x, y: y, w: w, h: h });
468
+ this._idleFrames = 0;
469
+ this._wakeRenderLoop();
470
+ };
471
+
472
+ /**
473
+ * Mark a node's region as dirty — automatically computes AABB with padding.
474
+ * Also marks connected edges' regions as dirty.
475
+ * @param {Object} node — graph node with .x, .y, ._radius
476
+ * @param {Object} [prevPos] — { x, y } previous position (for drag, to dirty old + new area)
477
+ */
478
+ GraphCanvas.prototype.markDirtyNode = function(node, prevPos) {
479
+ if (!node) return;
480
+ var r = (node._radius || 10);
481
+ // Padding accounts for glow/shadow, labels, and edge connection changes
482
+ var pad = r * 0.8 + 20 / Math.max(this._viewport.getScale(), 0.01);
483
+ var totalR = r + pad;
484
+
485
+ // Dirty the current node area
486
+ this._dirtyRects.push({
487
+ x: node.x - totalR,
488
+ y: node.y - totalR,
489
+ w: totalR * 2,
490
+ h: totalR * 2,
491
+ });
492
+
493
+ // If node moved, also dirty the old position
494
+ if (prevPos) {
495
+ this._dirtyRects.push({
496
+ x: prevPos.x - totalR,
497
+ y: prevPos.y - totalR,
498
+ w: totalR * 2,
499
+ h: totalR * 2,
500
+ });
501
+ }
502
+
503
+ // Dirty connected edges — mark each connected node's area too
504
+ var connEdges = this._nodeEdges[node.id] || [];
505
+ for (var i = 0; i < connEdges.length; i++) {
506
+ var e = connEdges[i];
507
+ var otherId = e.from === node.id ? e.to : e.from;
508
+ var otherNode = this._nodeMap[otherId];
509
+ if (otherNode) {
510
+ var oR = (otherNode._radius || 10) + pad;
511
+ this._dirtyRects.push({
512
+ x: otherNode.x - oR,
513
+ y: otherNode.y - oR,
514
+ w: oR * 2,
515
+ h: oR * 2,
516
+ });
517
+ }
518
+ }
519
+
520
+ this._idleFrames = 0;
521
+ this._wakeRenderLoop();
522
+ };
523
+
524
+ /**
525
+ * Merge overlapping/adjacent dirty rects to reduce clip operations.
526
+ * Uses a greedy merge: merge any two rects that overlap or are close together.
527
+ * @returns {Array} merged rects [{x,y,w,h}, ...]
528
+ */
529
+ GraphCanvas.prototype._mergeDirtyRects = function() {
530
+ var rects = this._dirtyRects;
531
+ if (rects.length <= 1) return rects;
532
+
533
+ // If too many dirty rects, just do a full redraw — cheaper than many clips
534
+ if (rects.length > 16) {
535
+ this._dirty = true;
536
+ return [];
537
+ }
538
+
539
+ // Convert to minX/minY/maxX/maxY for easier merging
540
+ var merged = [];
541
+ for (var i = 0; i < rects.length; i++) {
542
+ var r = rects[i];
543
+ merged.push({ minX: r.x, minY: r.y, maxX: r.x + r.w, maxY: r.y + r.h });
544
+ }
545
+
546
+ // Greedy merge pass
547
+ var changed = true;
548
+ while (changed) {
549
+ changed = false;
550
+ for (var i = 0; i < merged.length; i++) {
551
+ for (var j = i + 1; j < merged.length; j++) {
552
+ var a = merged[i], b = merged[j];
553
+ // Check if rects overlap or are very close (within merge gap)
554
+ var gap = 20 / Math.max(this._viewport.getScale(), 0.01);
555
+ if (a.minX - gap <= b.maxX && a.maxX + gap >= b.minX &&
556
+ a.minY - gap <= b.maxY && a.maxY + gap >= b.minY) {
557
+ // Merge b into a
558
+ a.minX = Math.min(a.minX, b.minX);
559
+ a.minY = Math.min(a.minY, b.minY);
560
+ a.maxX = Math.max(a.maxX, b.maxX);
561
+ a.maxY = Math.max(a.maxY, b.maxY);
562
+ merged.splice(j, 1);
563
+ changed = true;
564
+ break;
565
+ }
566
+ }
567
+ if (changed) break;
568
+ }
569
+ }
570
+
571
+ // Convert back to {x,y,w,h}
572
+ var result = [];
573
+ for (var i = 0; i < merged.length; i++) {
574
+ var m = merged[i];
575
+ result.push({ x: m.minX, y: m.minY, w: m.maxX - m.minX, h: m.maxY - m.minY });
576
+ }
577
+ return result;
578
+ };
579
+
580
+ // ── Render Loop ───────────────────────────────────────────────────────────
581
+
582
+ GraphCanvas.prototype._startRenderLoop = function() {
583
+ if (this._running) return;
584
+ this._running = true;
585
+ this._sleeping = false;
586
+ this._idleFrames = 0;
587
+ this._lastFrameTime = performance.now();
588
+ this._fpsTimer = this._lastFrameTime;
589
+ this._frameCount = 0;
590
+ var self = this;
591
+ function loop(now) {
592
+ self._rafId = requestAnimationFrame(loop);
593
+ // FPS throttle
594
+ var elapsed = now - self._lastFrameTime;
595
+ if (elapsed < self._frameInterval) return;
596
+ self._lastFrameTime = now - (elapsed % self._frameInterval);
597
+
598
+ // FPS counter
599
+ self._frameCount++;
600
+ if (now - self._fpsTimer >= 1000) {
601
+ self._currentFPS = self._frameCount;
602
+ self._metrics.fps = self._currentFPS;
603
+ self._frameCount = 0;
604
+ self._fpsTimer = now;
605
+ }
606
+
607
+ // Render frame if needed
608
+ if (self._dirty || self._dirtyRects.length > 0) {
609
+ var t0 = performance.now();
610
+ self._renderFrame();
611
+ self._metrics.lastRenderMs = Math.round((performance.now() - t0) * 100) / 100;
612
+ self._idleFrames = 0;
613
+ } else {
614
+ // No work this frame — increment idle counter
615
+ self._idleFrames++;
616
+ if (self._idleFrames >= self._idleThreshold) {
617
+ // Go to sleep: stop rAF loop to save CPU/battery
618
+ self._sleep();
619
+ }
620
+ }
621
+ }
622
+ this._rafId = requestAnimationFrame(loop);
623
+ };
624
+
625
+ /**
626
+ * Put the render loop to sleep (stop rAF). Wakes on markDirty/markDirtyNode.
627
+ */
628
+ GraphCanvas.prototype._sleep = function() {
629
+ if (this._rafId) {
630
+ cancelAnimationFrame(this._rafId);
631
+ this._rafId = null;
632
+ }
633
+ this._sleeping = true;
634
+ this._running = false;
635
+ this._metrics.fps = 0;
636
+ };
637
+
638
+ /**
639
+ * Wake the render loop from sleep (triggered by markDirty/markDirtyNode).
640
+ */
641
+ GraphCanvas.prototype._wakeRenderLoop = function() {
642
+ if (!this._running) {
643
+ this._startRenderLoop();
644
+ }
645
+ };
646
+
647
+ GraphCanvas.prototype._stopRenderLoop = function() {
648
+ if (this._rafId) {
649
+ cancelAnimationFrame(this._rafId);
650
+ this._rafId = null;
651
+ }
652
+ this._running = false;
653
+ this._sleeping = false;
654
+ };
655
+
656
+ /**
657
+ * Render one frame.
658
+ * Delegates to RenderPipeline for viewport culling + draw.
659
+ * Uses merged dirty rects for partial rendering when possible.
660
+ */
661
+ GraphCanvas.prototype._renderFrame = function() {
662
+ var mergedRects = [];
663
+ if (!this._dirty && this._dirtyRects.length > 0) {
664
+ mergedRects = this._mergeDirtyRects();
665
+ // If merge decided it's too many → _dirty was set to true
666
+ }
667
+ this._renderer.render(this._ctx, this._dirty, mergedRects);
668
+ this._dirty = false;
669
+ this._dirtyRects = [];
670
+ };
671
+
672
+ // ── Event System ──────────────────────────────────────────────────────────
673
+
674
+ /**
675
+ * Register event handler.
676
+ * Supported events: click, doubleClick, hover, dragStart, dragging, dragEnd,
677
+ * select, deselect, viewportChanged, stabilizationDone, dataLoaded
678
+ */
679
+ GraphCanvas.prototype.on = function(event, callback) {
680
+ if (!this._eventHandlers[event]) this._eventHandlers[event] = [];
681
+ this._eventHandlers[event].push(callback);
682
+ return this;
683
+ };
684
+
685
+ GraphCanvas.prototype.off = function(event, callback) {
686
+ var handlers = this._eventHandlers[event];
687
+ if (!handlers) return this;
688
+ if (callback) {
689
+ for (var i = handlers.length - 1; i >= 0; i--) {
690
+ if (handlers[i] === callback) handlers.splice(i, 1);
691
+ }
692
+ } else {
693
+ this._eventHandlers[event] = [];
694
+ }
695
+ return this;
696
+ };
697
+
698
+ GraphCanvas.prototype._emit = function(event, data) {
699
+ var handlers = this._eventHandlers[event];
700
+ if (!handlers) return;
701
+ for (var i = 0; i < handlers.length; i++) {
702
+ try {
703
+ handlers[i](data);
704
+ } catch (e) {
705
+ console.error('GraphCanvas event handler error (' + event + '):', e);
706
+ }
707
+ }
708
+ };
709
+
710
+ // ── Public API ────────────────────────────────────────────────────────────
711
+
712
+ /**
713
+ * Fit view to show all nodes with padding.
714
+ */
715
+ GraphCanvas.prototype.fit = function(options) {
716
+ this._viewport.fitToNodes(this._nodes, options);
717
+ this.markDirty();
718
+ };
719
+
720
+ /**
721
+ * Center view on a specific node.
722
+ */
723
+ GraphCanvas.prototype.focusNode = function(nodeId, options) {
724
+ var node = this._nodeMap[nodeId];
725
+ if (!node) return;
726
+ this._viewport.centerOn(node.x, node.y, options);
727
+ this.markDirty();
728
+ };
729
+
730
+ /**
731
+ * Get current viewport bounds in world coordinates.
732
+ */
733
+ GraphCanvas.prototype.getViewportBounds = function() {
734
+ return this._viewport.getWorldBounds();
735
+ };
736
+
737
+ /**
738
+ * Get node positions.
739
+ */
740
+ GraphCanvas.prototype.getPositions = function(nodeIds) {
741
+ var result = {};
742
+ if (nodeIds) {
743
+ for (var i = 0; i < nodeIds.length; i++) {
744
+ var n = this._nodeMap[nodeIds[i]];
745
+ if (n) result[nodeIds[i]] = { x: n.x, y: n.y };
746
+ }
747
+ } else {
748
+ for (var i = 0; i < this._nodes.length; i++) {
749
+ var n = this._nodes[i];
750
+ result[n.id] = { x: n.x, y: n.y };
751
+ }
752
+ }
753
+ return result;
754
+ };
755
+
756
+ /**
757
+ * Move a node to new coordinates.
758
+ */
759
+ GraphCanvas.prototype.moveNode = function(nodeId, x, y) {
760
+ var node = this._nodeMap[nodeId];
761
+ if (!node) return;
762
+ var prevPos = { x: node.x, y: node.y };
763
+ node.x = x;
764
+ node.y = y;
765
+ // Update spatial index (node + connected edges)
766
+ this._spatialIndex.updateNode(node);
767
+ this._spatialIndex.updateEdgesForNode(node.id, this._nodeEdges, this._nodeMap);
768
+ this.markDirtyNode(node, prevPos);
769
+ };
770
+
771
+ /**
772
+ * Get connected edges for a node.
773
+ */
774
+ GraphCanvas.prototype.getConnectedEdges = function(nodeId) {
775
+ var edges = this._nodeEdges[nodeId] || [];
776
+ var ids = [];
777
+ for (var i = 0; i < edges.length; i++) ids.push(edges[i].id);
778
+ return ids;
779
+ };
780
+
781
+ /**
782
+ * Get connected nodes for a node.
783
+ */
784
+ GraphCanvas.prototype.getConnectedNodes = function(nodeId) {
785
+ var edges = this._nodeEdges[nodeId] || [];
786
+ var result = [];
787
+ var seen = {};
788
+ for (var i = 0; i < edges.length; i++) {
789
+ var otherId = edges[i].from === nodeId ? edges[i].to : edges[i].from;
790
+ if (!seen[otherId]) {
791
+ result.push(otherId);
792
+ seen[otherId] = true;
793
+ }
794
+ }
795
+ return result;
796
+ };
797
+
798
+ /**
799
+ * Get performance metrics.
800
+ */
801
+ GraphCanvas.prototype.getMetrics = function() {
802
+ return Object.assign({}, this._metrics);
803
+ };
804
+
805
+ /**
806
+ * Force a full redraw.
807
+ */
808
+ GraphCanvas.prototype.redraw = function() {
809
+ this.markDirty();
810
+ };
811
+
812
+ /**
813
+ * Destroy the engine, clean up all resources.
814
+ */
815
+ GraphCanvas.prototype.destroy = function() {
816
+ this._stopRenderLoop();
817
+ this._interaction.destroy();
818
+ if (this._resizeObserver) this._resizeObserver.disconnect();
819
+ if (this._layoutEngine) this._layoutEngine.destroy();
820
+ if (this._canvas.parentNode) this._canvas.parentNode.removeChild(this._canvas);
821
+ this._nodes = [];
822
+ this._edges = [];
823
+ this._nodeMap = {};
824
+ this._edgeMap = {};
825
+ this._nodeEdges = {};
826
+ this._eventHandlers = {};
827
+ };
828
+
829
+ /**
830
+ * Convert screen coordinates to world coordinates.
831
+ */
832
+ GraphCanvas.prototype.screenToWorld = function(sx, sy) {
833
+ return this._viewport.screenToWorld(sx, sy);
834
+ };
835
+
836
+ /**
837
+ * Convert world coordinates to screen coordinates.
838
+ */
839
+ GraphCanvas.prototype.worldToScreen = function(wx, wy) {
840
+ return this._viewport.worldToScreen(wx, wy);
841
+ };
842
+ `;
843
+ }
844
+ //# sourceMappingURL=core.js.map