@riebeckite/graph 0.0.0-stage → 0.0.36

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.
package/dist/client.js ADDED
@@ -0,0 +1,1290 @@
1
+ // src/animation.ts
2
+ function buildRevealSchedule(nodes) {
3
+ const dated = nodes.filter((node) => node.date !== null).sort(
4
+ (left, right) => Date.parse(left.date) - Date.parse(right.date) || left.id.localeCompare(right.id)
5
+ );
6
+ const undated = nodes.filter((node) => node.date === null).sort((left, right) => left.id.localeCompare(right.id));
7
+ if (dated.length === 0) {
8
+ return [...undated].map((node, index) => ({
9
+ id: node.id,
10
+ at: (index + 1) / Math.max(undated.length, 1),
11
+ dated: false
12
+ }));
13
+ }
14
+ const datedShare = undated.length === 0 ? 1 : 0.85;
15
+ const entries = dated.map((node, index) => ({
16
+ id: node.id,
17
+ at: (index + 1) / dated.length * datedShare,
18
+ dated: true
19
+ }));
20
+ undated.forEach((node, index) => {
21
+ entries.push({
22
+ id: node.id,
23
+ at: datedShare + (index + 1) / undated.length * (1 - datedShare),
24
+ dated: false
25
+ });
26
+ });
27
+ return entries;
28
+ }
29
+
30
+ // src/query.ts
31
+ var SUPPORTED_OPERATORS = /* @__PURE__ */ new Set(["file", "path", "tag"]);
32
+ var TOKEN_PATTERN = /"[^"]*"|\S+/g;
33
+ function parseGraphQuery(input) {
34
+ const terms = [];
35
+ const unsupportedOperators = /* @__PURE__ */ new Set();
36
+ for (const rawToken of input.match(TOKEN_PATTERN) ?? []) {
37
+ let token = rawToken.trim();
38
+ if (token.length === 0) continue;
39
+ let negated = false;
40
+ if (token.startsWith("-") && token.length > 1) {
41
+ negated = true;
42
+ token = token.slice(1);
43
+ }
44
+ const separator = token.indexOf(":");
45
+ if (separator > 0) {
46
+ const operator = token.slice(0, separator).toLocaleLowerCase();
47
+ if (!/^[a-z]+$/.test(operator)) {
48
+ terms.push({ field: "text", value: unquote(token), negated });
49
+ continue;
50
+ }
51
+ if (!SUPPORTED_OPERATORS.has(operator)) {
52
+ unsupportedOperators.add(operator);
53
+ terms.push({
54
+ field: "unsupported",
55
+ value: unquote(token.slice(separator + 1)),
56
+ negated
57
+ });
58
+ continue;
59
+ }
60
+ terms.push({
61
+ field: operator,
62
+ value: normalizeTagValue(unquote(token.slice(separator + 1))),
63
+ negated
64
+ });
65
+ continue;
66
+ }
67
+ terms.push({ field: "text", value: unquote(token), negated });
68
+ }
69
+ return { terms, unsupportedOperators: [...unsupportedOperators] };
70
+ }
71
+ function matchesGraphQuery(node, query) {
72
+ for (const term of query.terms) {
73
+ const matched = matchesTerm(node, term);
74
+ if (term.negated ? matched : !matched) return false;
75
+ }
76
+ return true;
77
+ }
78
+ function normalizeGraphText(value) {
79
+ return value.normalize("NFKC").toLocaleLowerCase().replace(
80
+ /[ァ-ン]/g,
81
+ (character) => String.fromCharCode(character.charCodeAt(0) - 96)
82
+ );
83
+ }
84
+ function matchesTerm(node, term) {
85
+ const value = normalizeGraphText(term.value);
86
+ if (value.length === 0) return false;
87
+ switch (term.field) {
88
+ case "file":
89
+ return normalizeGraphText(node.title).includes(value);
90
+ case "path":
91
+ return normalizeGraphText(node.path).includes(value);
92
+ case "tag":
93
+ return node.tags.some((tag) => {
94
+ const normalized = normalizeGraphText(tag);
95
+ return normalized === value || normalized.startsWith(`${value}/`);
96
+ });
97
+ case "text":
98
+ return normalizeGraphText(node.title).includes(value) || normalizeGraphText(node.path).includes(value) || node.tags.some((tag) => normalizeGraphText(tag).includes(value));
99
+ case "unsupported":
100
+ return false;
101
+ }
102
+ }
103
+ function normalizeTagValue(value) {
104
+ return value.replace(/^#+/, "");
105
+ }
106
+ function unquote(value) {
107
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
108
+ return value.slice(1, -1);
109
+ }
110
+ return value;
111
+ }
112
+
113
+ // src/simulation.ts
114
+ var ALPHA_MIN = 1e-3;
115
+ var ALPHA_DECAY = 0.0228;
116
+ var VELOCITY_DECAY = 0.4;
117
+ var MAX_VELOCITY = 40;
118
+ var GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
119
+ function createGraphSimulation(input) {
120
+ const ids = [...new Set(input.nodeIds)].sort(
121
+ (left, right) => left.localeCompare(right)
122
+ );
123
+ const nodes = ids.map((id, index) => {
124
+ const previous = input.positions?.get(id);
125
+ if (previous && Number.isFinite(previous.x) && Number.isFinite(previous.y)) {
126
+ return {
127
+ id,
128
+ x: previous.x,
129
+ y: previous.y,
130
+ vx: 0,
131
+ vy: 0,
132
+ fx: null,
133
+ fy: null
134
+ };
135
+ }
136
+ if (id === input.centerId) {
137
+ return { id, x: 0, y: 0, vx: 0, vy: 0, fx: null, fy: null };
138
+ }
139
+ const radius = 18 + 13 * Math.sqrt(index);
140
+ const angle = index * GOLDEN_ANGLE + hash(id) % 360 * (Math.PI / 180);
141
+ return {
142
+ id,
143
+ x: Math.cos(angle) * radius,
144
+ y: Math.sin(angle) * radius,
145
+ vx: 0,
146
+ vy: 0,
147
+ fx: null,
148
+ fy: null
149
+ };
150
+ });
151
+ const indexById = new Map(nodes.map((node, index) => [node.id, index]));
152
+ const links = input.links.map((link) => ({
153
+ source: indexById.get(link.source),
154
+ target: indexById.get(link.target)
155
+ })).filter(
156
+ (link) => link.source !== void 0 && link.target !== void 0
157
+ );
158
+ let options = { ...input.options };
159
+ let alpha = 1;
160
+ let alphaTarget = 0;
161
+ let active = true;
162
+ function tick() {
163
+ if (!active && alpha < ALPHA_MIN) return false;
164
+ alpha += (alphaTarget - alpha) * ALPHA_DECAY;
165
+ applyCenterForce(nodes, options.centerForce, alpha);
166
+ applyLinkForce(nodes, links, options, alpha);
167
+ applyRepelForce(nodes, options.repelForce * alpha * 0.02);
168
+ for (const node of nodes) {
169
+ if (node.fx !== null && node.fy !== null) {
170
+ node.x = node.fx;
171
+ node.y = node.fy;
172
+ node.vx = 0;
173
+ node.vy = 0;
174
+ continue;
175
+ }
176
+ node.vx *= 1 - VELOCITY_DECAY;
177
+ node.vy *= 1 - VELOCITY_DECAY;
178
+ node.vx = clampVelocity(node.vx);
179
+ node.vy = clampVelocity(node.vy);
180
+ node.x += node.vx;
181
+ node.y += node.vy;
182
+ }
183
+ if (alpha < ALPHA_MIN) {
184
+ alpha = 0;
185
+ active = false;
186
+ }
187
+ return active;
188
+ }
189
+ return {
190
+ nodes,
191
+ tick,
192
+ reheat(value = 0.7) {
193
+ alpha = Math.max(alpha, value);
194
+ active = true;
195
+ },
196
+ setOptions(next) {
197
+ options = { ...next };
198
+ if (alpha < 0.05) {
199
+ alpha = 0.05;
200
+ active = true;
201
+ }
202
+ },
203
+ fix(id, x, y) {
204
+ const index = indexById.get(id);
205
+ const node = index === void 0 ? void 0 : nodes[index];
206
+ if (!node) return;
207
+ node.fx = x;
208
+ node.fy = y;
209
+ node.x = x;
210
+ node.y = y;
211
+ node.vx = 0;
212
+ node.vy = 0;
213
+ alpha = Math.max(alpha, 0.5);
214
+ active = true;
215
+ },
216
+ release(id) {
217
+ const index = indexById.get(id);
218
+ const node = index === void 0 ? void 0 : nodes[index];
219
+ if (!node) return;
220
+ node.fx = null;
221
+ node.fy = null;
222
+ alpha = Math.max(alpha, 0.45);
223
+ active = true;
224
+ },
225
+ stop() {
226
+ alpha = 0;
227
+ alphaTarget = 0;
228
+ active = false;
229
+ },
230
+ isActive() {
231
+ return active;
232
+ },
233
+ getAlpha() {
234
+ return alpha;
235
+ }
236
+ };
237
+ }
238
+ function applyCenterForce(nodes, centerForce, alpha) {
239
+ const strength = centerForce * 0.03 * alpha;
240
+ if (strength <= 0) return;
241
+ for (const node of nodes) {
242
+ node.vx -= node.x * strength;
243
+ node.vy -= node.y * strength;
244
+ }
245
+ }
246
+ function applyLinkForce(nodes, links, options, alpha) {
247
+ const strength = options.linkForce * 0.12 * alpha;
248
+ if (strength <= 0) return;
249
+ for (const link of links) {
250
+ const source = nodes[link.source];
251
+ const target = nodes[link.target];
252
+ if (!source || !target) continue;
253
+ const dx = target.x - source.x;
254
+ const dy = target.y - source.y;
255
+ const distance = Math.hypot(dx, dy);
256
+ if (distance < 1e-3) continue;
257
+ const force = (distance - options.linkDistance) / distance * strength;
258
+ source.vx += dx * force;
259
+ source.vy += dy * force;
260
+ target.vx -= dx * force;
261
+ target.vy -= dy * force;
262
+ }
263
+ }
264
+ function applyRepelForce(nodes, strength) {
265
+ if (strength <= 0 || nodes.length < 2) return;
266
+ if (nodes.length <= 250) {
267
+ applyExactRepulsion(nodes, strength);
268
+ return;
269
+ }
270
+ applyQuadtreeRepulsion(nodes, strength);
271
+ }
272
+ function applyExactRepulsion(nodes, strength) {
273
+ for (let left = 0; left < nodes.length; left += 1) {
274
+ const a = nodes[left];
275
+ if (!a) continue;
276
+ for (let right = left + 1; right < nodes.length; right += 1) {
277
+ const b = nodes[right];
278
+ if (!b) continue;
279
+ const dx = b.x - a.x;
280
+ const dy = b.y - a.y;
281
+ const distanceSquared = dx * dx + dy * dy + 1;
282
+ const force = strength / distanceSquared;
283
+ a.vx -= dx * force;
284
+ a.vy -= dy * force;
285
+ b.vx += dx * force;
286
+ b.vy += dy * force;
287
+ }
288
+ }
289
+ }
290
+ function applyQuadtreeRepulsion(nodes, strength) {
291
+ let minX = Number.POSITIVE_INFINITY;
292
+ let minY = Number.POSITIVE_INFINITY;
293
+ let maxX = Number.NEGATIVE_INFINITY;
294
+ let maxY = Number.NEGATIVE_INFINITY;
295
+ for (const node of nodes) {
296
+ minX = Math.min(minX, node.x);
297
+ minY = Math.min(minY, node.y);
298
+ maxX = Math.max(maxX, node.x);
299
+ maxY = Math.max(maxY, node.y);
300
+ }
301
+ const size = Math.max(maxX - minX, maxY - minY, 1) + 1;
302
+ const root = {
303
+ x: (minX + maxX - size) / 2,
304
+ y: (minY + maxY - size) / 2,
305
+ size,
306
+ mass: 0,
307
+ centerX: 0,
308
+ centerY: 0,
309
+ members: null,
310
+ children: null
311
+ };
312
+ for (let index = 0; index < nodes.length; index += 1) {
313
+ insertQuad(root, nodes, index, 0);
314
+ }
315
+ for (let index = 0; index < nodes.length; index += 1) {
316
+ const node = nodes[index];
317
+ if (node) applyQuadForce(root, nodes, node, index, strength);
318
+ }
319
+ }
320
+ function insertQuad(quad, nodes, index, depth) {
321
+ const node = nodes[index];
322
+ if (!node) return;
323
+ quad.centerX = (quad.centerX * quad.mass + node.x) / (quad.mass + 1);
324
+ quad.centerY = (quad.centerY * quad.mass + node.y) / (quad.mass + 1);
325
+ quad.mass += 1;
326
+ if (quad.children) {
327
+ insertIntoChild(quad, nodes, index, depth + 1);
328
+ return;
329
+ }
330
+ if (!quad.members) {
331
+ quad.members = [index];
332
+ return;
333
+ }
334
+ if (depth >= 16) {
335
+ quad.members.push(index);
336
+ return;
337
+ }
338
+ const members = quad.members;
339
+ quad.members = null;
340
+ const half = quad.size / 2;
341
+ quad.children = [
342
+ createQuad(quad.x, quad.y, half),
343
+ createQuad(quad.x + half, quad.y, half),
344
+ createQuad(quad.x, quad.y + half, half),
345
+ createQuad(quad.x + half, quad.y + half, half)
346
+ ];
347
+ for (const member of members) insertIntoChild(quad, nodes, member, depth + 1);
348
+ insertIntoChild(quad, nodes, index, depth + 1);
349
+ }
350
+ function insertIntoChild(quad, nodes, index, depth) {
351
+ const children = quad.children;
352
+ if (!children) return;
353
+ const node = nodes[index];
354
+ if (!node) return;
355
+ const half = quad.size / 2;
356
+ const right = node.x >= quad.x + half ? 1 : 0;
357
+ const bottom = node.y >= quad.y + half ? 2 : 0;
358
+ const child = children[right + bottom];
359
+ if (child) insertQuad(child, nodes, index, depth);
360
+ }
361
+ function createQuad(x, y, size) {
362
+ return {
363
+ x,
364
+ y,
365
+ size,
366
+ mass: 0,
367
+ centerX: 0,
368
+ centerY: 0,
369
+ members: null,
370
+ children: null
371
+ };
372
+ }
373
+ function applyQuadForce(quad, nodes, target, targetIndex, strength) {
374
+ if (quad.mass === 0) return;
375
+ const dx = quad.centerX - target.x;
376
+ const dy = quad.centerY - target.y;
377
+ const distanceSquared = dx * dx + dy * dy + 1;
378
+ if (!quad.children) {
379
+ for (const index of quad.members ?? []) {
380
+ if (index === targetIndex) continue;
381
+ const other = nodes[index];
382
+ if (other) applyPairRepulsion(target, other, strength);
383
+ }
384
+ return;
385
+ }
386
+ if (quad.size * quad.size < 0.81 * distanceSquared) {
387
+ const force = strength * quad.mass / distanceSquared;
388
+ target.vx -= dx * force;
389
+ target.vy -= dy * force;
390
+ return;
391
+ }
392
+ for (const child of quad.children) {
393
+ applyQuadForce(child, nodes, target, targetIndex, strength);
394
+ }
395
+ }
396
+ function applyPairRepulsion(a, b, strength) {
397
+ const dx = b.x - a.x;
398
+ const dy = b.y - a.y;
399
+ const distanceSquared = dx * dx + dy * dy + 1;
400
+ const force = strength / distanceSquared;
401
+ a.vx -= dx * force;
402
+ a.vy -= dy * force;
403
+ b.vx += dx * force;
404
+ b.vy += dy * force;
405
+ }
406
+ function clampVelocity(value) {
407
+ return Math.min(Math.max(value, -MAX_VELOCITY), MAX_VELOCITY);
408
+ }
409
+ function hash(value) {
410
+ let result = 2166136261;
411
+ for (let index = 0; index < value.length; index += 1) {
412
+ result ^= value.charCodeAt(index);
413
+ result = Math.imul(result, 16777619);
414
+ }
415
+ return result >>> 0;
416
+ }
417
+
418
+ // src/view.ts
419
+ function buildVisibleGraph(input) {
420
+ const { document: document2, settings, mode } = input;
421
+ const centerId = input.centerId ?? document2.centerId;
422
+ const nodeById = new Map(document2.nodes.map((node) => [node.id, node]));
423
+ const adjacency = buildAdjacency(document2);
424
+ const fullDegree = /* @__PURE__ */ new Map();
425
+ for (const link of document2.links) {
426
+ fullDegree.set(link.source, (fullDegree.get(link.source) ?? 0) + 1);
427
+ fullDegree.set(link.target, (fullDegree.get(link.target) ?? 0) + 1);
428
+ }
429
+ const search = parseGraphQuery(settings.search);
430
+ const groups = settings.groups.map((group) => ({
431
+ group,
432
+ query: parseGraphQuery(group.query)
433
+ }));
434
+ const unsupportedOperators = new Set(search.unsupportedOperators);
435
+ for (const entry of groups) {
436
+ for (const operator of entry.query.unsupportedOperators) {
437
+ unsupportedOperators.add(operator);
438
+ }
439
+ }
440
+ const candidates = mode === "local" ? collectLocalNodeIds(adjacency, centerId, settings.depth) : new Set(document2.nodes.map((node) => node.id));
441
+ const visibleNodes = [];
442
+ for (const id of candidates) {
443
+ const node = nodeById.get(id);
444
+ if (!node) continue;
445
+ if (node.kind === "tag" && !settings.showTags) continue;
446
+ if (node.kind === "attachment" && !settings.showAttachments) continue;
447
+ if (node.kind === "unresolved" && settings.existingOnly) continue;
448
+ if (node.kind === "note" && !settings.showOrphans && (fullDegree.get(node.id) ?? 0) === 0) {
449
+ continue;
450
+ }
451
+ if (settings.search.trim().length > 0 && !matchesGraphQuery(node, search)) {
452
+ if (mode !== "local" || id !== centerId) continue;
453
+ }
454
+ visibleNodes.push({
455
+ ...node,
456
+ degree: 0,
457
+ radius: 0,
458
+ color: "",
459
+ groupId: null
460
+ });
461
+ }
462
+ const visibleIds = new Set(visibleNodes.map((node) => node.id));
463
+ const links = document2.links.filter(
464
+ (link) => visibleIds.has(link.source) && visibleIds.has(link.target)
465
+ );
466
+ const degree = /* @__PURE__ */ new Map();
467
+ for (const link of links) {
468
+ degree.set(link.source, (degree.get(link.source) ?? 0) + 1);
469
+ degree.set(link.target, (degree.get(link.target) ?? 0) + 1);
470
+ }
471
+ visibleNodes.sort((left, right) => left.id.localeCompare(right.id));
472
+ for (const node of visibleNodes) {
473
+ node.degree = degree.get(node.id) ?? 0;
474
+ node.radius = 3 + Math.sqrt(node.degree) * 1.7;
475
+ const group = findGroup(node, groups);
476
+ node.groupId = group?.group.id ?? null;
477
+ node.color = group?.group.color ?? "";
478
+ }
479
+ return {
480
+ nodes: visibleNodes,
481
+ links,
482
+ centerId: centerId && visibleIds.has(centerId) ? centerId : null,
483
+ unsupportedOperators: [...unsupportedOperators]
484
+ };
485
+ }
486
+ function collectLocalNodeIds(adjacency, centerId, depth) {
487
+ const ids = /* @__PURE__ */ new Set();
488
+ if (!centerId) return ids;
489
+ ids.add(centerId);
490
+ let frontier = [centerId];
491
+ for (let level = 0; level < depth; level += 1) {
492
+ const next = [];
493
+ for (const id of frontier) {
494
+ for (const neighbor of adjacency.get(id) ?? []) {
495
+ if (ids.has(neighbor)) continue;
496
+ ids.add(neighbor);
497
+ next.push(neighbor);
498
+ }
499
+ }
500
+ if (next.length === 0) break;
501
+ frontier = next;
502
+ }
503
+ return ids;
504
+ }
505
+ function buildAdjacency(document2) {
506
+ const adjacency = /* @__PURE__ */ new Map();
507
+ for (const node of document2.nodes) adjacency.set(node.id, []);
508
+ for (const link of document2.links) {
509
+ adjacency.get(link.source)?.push(link.target);
510
+ adjacency.get(link.target)?.push(link.source);
511
+ }
512
+ return adjacency;
513
+ }
514
+ function findGroup(node, groups) {
515
+ for (const entry of groups) {
516
+ if (entry.group.query.trim().length === 0) continue;
517
+ if (entry.query.unsupportedOperators.length > 0) continue;
518
+ if (matchesGraphQuery(node, entry.query)) return entry;
519
+ }
520
+ return null;
521
+ }
522
+
523
+ // client.ts
524
+ var MIN_SCALE = 0.12;
525
+ var MAX_SCALE = 4;
526
+ var DRAG_THRESHOLD_PX = 4;
527
+ var ANIMATION_DURATION_MS = 3200;
528
+ function createGraphRenderer(options) {
529
+ const canvas = options.canvas;
530
+ const context = canvas.getContext("2d");
531
+ if (!context) {
532
+ return createNoopRenderer();
533
+ }
534
+ const ctx = context;
535
+ const callbacks = options;
536
+ let input = null;
537
+ let visible = {
538
+ nodes: [],
539
+ links: [],
540
+ centerId: null,
541
+ unsupportedOperators: []
542
+ };
543
+ let simulation = null;
544
+ let signature = "";
545
+ let camera = { x: 0, y: 0, scale: 1 };
546
+ let size = { width: 0, height: 0 };
547
+ let hoveredId = null;
548
+ let selectedId = null;
549
+ let drag = null;
550
+ const pointers = /* @__PURE__ */ new Map();
551
+ let pinch = null;
552
+ let frameHandle = null;
553
+ let paused = false;
554
+ let destroyed = false;
555
+ let animating = false;
556
+ let animationStart = 0;
557
+ let animationCutoff = 1;
558
+ let pendingFit = false;
559
+ let reveal = /* @__PURE__ */ new Map();
560
+ let colors = readColors(canvas);
561
+ const reducedMotion = options.reducedMotion ?? false;
562
+ const resizeObserver = new ResizeObserver(() => resize());
563
+ const intersectionObserver = new IntersectionObserver((entries) => {
564
+ paused = entries.every((entry) => !entry.isIntersecting);
565
+ if (!paused) requestFrame();
566
+ });
567
+ const mutationObserver = new MutationObserver(() => {
568
+ colors = readColors(canvas);
569
+ requestFrame();
570
+ });
571
+ const onVisibilityChange = () => {
572
+ paused = document.visibilityState === "hidden";
573
+ if (!paused) requestFrame();
574
+ };
575
+ resizeObserver.observe(canvas);
576
+ intersectionObserver.observe(canvas);
577
+ mutationObserver.observe(document.documentElement, {
578
+ attributes: true,
579
+ attributeFilter: ["data-rb-mode", "data-rb-theme", "class", "style"]
580
+ });
581
+ document.addEventListener("visibilitychange", onVisibilityChange);
582
+ canvas.addEventListener("pointerdown", onPointerDown);
583
+ canvas.addEventListener("pointermove", onPointerMove);
584
+ canvas.addEventListener("pointerup", onPointerUp);
585
+ canvas.addEventListener("pointercancel", onPointerUp);
586
+ canvas.addEventListener("pointerleave", onPointerLeave);
587
+ canvas.addEventListener("wheel", onWheel, { passive: false });
588
+ canvas.addEventListener("dblclick", onDoubleClick);
589
+ canvas.addEventListener("contextmenu", onContextMenu);
590
+ canvas.addEventListener("keydown", onKeyDown);
591
+ resize();
592
+ return {
593
+ update,
594
+ setSelected,
595
+ zoomBy,
596
+ resetView,
597
+ focusNode,
598
+ playAnimation,
599
+ stopAnimation,
600
+ isAnimating: () => animating,
601
+ destroy
602
+ };
603
+ function update(next) {
604
+ if (destroyed) return;
605
+ const previousMode = input?.mode;
606
+ const previousCenter = input?.centerId;
607
+ const previousDocument = input?.document;
608
+ input = next;
609
+ visible = buildVisibleGraph({
610
+ document: next.document,
611
+ settings: next.settings,
612
+ mode: next.mode,
613
+ centerId: next.centerId
614
+ });
615
+ selectedId = next.selectedId ?? null;
616
+ reveal = new Map(
617
+ buildRevealSchedule(visible.nodes).map((entry) => [entry.id, entry.at])
618
+ );
619
+ const nextSignature = visibleSignature(visible);
620
+ const positions = simulation ? new Map(
621
+ simulation.nodes.map((node) => [node.id, { x: node.x, y: node.y }])
622
+ ) : void 0;
623
+ if (nextSignature !== signature || !simulation) {
624
+ signature = nextSignature;
625
+ simulation = createGraphSimulation({
626
+ nodeIds: visible.nodes.map((node) => node.id),
627
+ links: visible.links,
628
+ options: forceOptions(next.settings),
629
+ positions,
630
+ centerId: visible.centerId
631
+ });
632
+ if (!previousDocument || previousDocument !== next.document || previousMode !== next.mode || previousCenter !== next.centerId) {
633
+ pendingFit = true;
634
+ fitView();
635
+ }
636
+ } else {
637
+ simulation.setOptions(forceOptions(next.settings));
638
+ }
639
+ if (!next.settings.animate) {
640
+ animating = false;
641
+ animationCutoff = 1;
642
+ }
643
+ requestFrame();
644
+ }
645
+ function setSelected(id) {
646
+ if (selectedId === id) return;
647
+ selectedId = id;
648
+ requestFrame();
649
+ }
650
+ function resize() {
651
+ const rect = canvas.getBoundingClientRect();
652
+ const width = Math.max(1, Math.round(rect.width));
653
+ const height = Math.max(1, Math.round(rect.height));
654
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
655
+ if (canvas.width !== Math.round(width * dpr) || canvas.height !== Math.round(height * dpr)) {
656
+ canvas.width = Math.round(width * dpr);
657
+ canvas.height = Math.round(height * dpr);
658
+ }
659
+ size = { width, height };
660
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
661
+ if (camera.x === 0 && camera.y === 0) {
662
+ camera = { x: width / 2, y: height / 2, scale: 1 };
663
+ }
664
+ requestFrame();
665
+ }
666
+ function fitView() {
667
+ if (visible.nodes.length === 0) {
668
+ camera = { x: size.width / 2, y: size.height / 2, scale: 1 };
669
+ return;
670
+ }
671
+ const positions = simulation?.nodes ?? [];
672
+ let minX = Number.POSITIVE_INFINITY;
673
+ let minY = Number.POSITIVE_INFINITY;
674
+ let maxX = Number.NEGATIVE_INFINITY;
675
+ let maxY = Number.NEGATIVE_INFINITY;
676
+ for (const node of positions) {
677
+ minX = Math.min(minX, node.x);
678
+ minY = Math.min(minY, node.y);
679
+ maxX = Math.max(maxX, node.x);
680
+ maxY = Math.max(maxY, node.y);
681
+ }
682
+ if (!Number.isFinite(minX)) {
683
+ camera = { x: size.width / 2, y: size.height / 2, scale: 1 };
684
+ return;
685
+ }
686
+ const boundsWidth = Math.max(maxX - minX, 80);
687
+ const boundsHeight = Math.max(maxY - minY, 80);
688
+ const scale = clamp(
689
+ Math.min(
690
+ (size.width - 80) / boundsWidth,
691
+ (size.height - 80) / boundsHeight
692
+ ),
693
+ 0.2,
694
+ 2
695
+ );
696
+ const centerX = (minX + maxX) / 2;
697
+ const centerY = (minY + maxY) / 2;
698
+ camera = {
699
+ x: size.width / 2 - centerX * scale,
700
+ y: size.height / 2 - centerY * scale,
701
+ scale
702
+ };
703
+ }
704
+ function resetView() {
705
+ pendingFit = false;
706
+ fitView();
707
+ requestFrame();
708
+ }
709
+ function focusNode(id) {
710
+ const node = simulation?.nodes.find((entry) => entry.id === id);
711
+ if (!node) return;
712
+ pendingFit = false;
713
+ camera = {
714
+ ...camera,
715
+ x: size.width / 2 - node.x * camera.scale,
716
+ y: size.height / 2 - node.y * camera.scale
717
+ };
718
+ requestFrame();
719
+ }
720
+ function zoomBy(factor, origin) {
721
+ pendingFit = false;
722
+ const center = origin ?? { x: size.width / 2, y: size.height / 2 };
723
+ const scale = clamp(camera.scale * factor, MIN_SCALE, MAX_SCALE);
724
+ if (scale === camera.scale) return;
725
+ const worldX = (center.x - camera.x) / camera.scale;
726
+ const worldY = (center.y - camera.y) / camera.scale;
727
+ camera = {
728
+ x: center.x - worldX * scale,
729
+ y: center.y - worldY * scale,
730
+ scale
731
+ };
732
+ requestFrame();
733
+ }
734
+ function playAnimation() {
735
+ if (reducedMotion || visible.nodes.length === 0) {
736
+ animationCutoff = 1;
737
+ animating = false;
738
+ callbacks.onAnimationChange?.(false);
739
+ requestFrame();
740
+ return;
741
+ }
742
+ animationStart = performance.now();
743
+ animationCutoff = 0;
744
+ animating = true;
745
+ callbacks.onAnimationChange?.(true);
746
+ requestFrame();
747
+ }
748
+ function stopAnimation() {
749
+ animating = false;
750
+ animationCutoff = 1;
751
+ callbacks.onAnimationChange?.(false);
752
+ requestFrame();
753
+ }
754
+ function requestFrame() {
755
+ if (destroyed || paused || frameHandle !== null) return;
756
+ frameHandle = requestAnimationFrame(frame);
757
+ }
758
+ function frame(time) {
759
+ frameHandle = null;
760
+ if (destroyed) return;
761
+ if (animating) {
762
+ const progress = clamp(
763
+ (time - animationStart) / ANIMATION_DURATION_MS,
764
+ 0,
765
+ 1
766
+ );
767
+ animationCutoff = progress;
768
+ if (progress >= 1) {
769
+ animating = false;
770
+ callbacks.onAnimationChange?.(false);
771
+ }
772
+ }
773
+ const active = simulation?.tick() ?? false;
774
+ if (pendingFit && simulation && !active) {
775
+ pendingFit = false;
776
+ fitView();
777
+ }
778
+ draw();
779
+ if (active || animating) requestFrame();
780
+ }
781
+ function draw() {
782
+ const dpr = Math.min(window.devicePixelRatio || 1, 2);
783
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
784
+ ctx.clearRect(0, 0, size.width, size.height);
785
+ ctx.fillStyle = colors.background;
786
+ ctx.fillRect(0, 0, size.width, size.height);
787
+ const related = relatedIds();
788
+ const dimOthers = related.size > 0;
789
+ const selected = selectedId;
790
+ const labelThreshold = 0.35 + (input?.settings.textFade ?? 0.35) * 1.1;
791
+ const labelAlpha = clamp((camera.scale - labelThreshold) / 0.4, 0, 1);
792
+ const nodes = simulation?.nodes ?? [];
793
+ const simulationById = new Map(nodes.map((node) => [node.id, node]));
794
+ const positions = /* @__PURE__ */ new Map();
795
+ for (const node of visible.nodes) {
796
+ const revealAt = reveal.get(node.id);
797
+ if (revealAt !== void 0 && revealAt > animationCutoff) {
798
+ continue;
799
+ }
800
+ const simulationNode = simulationById.get(node.id);
801
+ if (!simulationNode) continue;
802
+ const x = simulationNode.x;
803
+ const y = simulationNode.y;
804
+ positions.set(node.id, {
805
+ node,
806
+ x,
807
+ y,
808
+ radius: Math.max(2, node.radius * (input?.settings.nodeSize ?? 1))
809
+ });
810
+ }
811
+ const linkThickness = input?.settings.linkThickness ?? 1;
812
+ ctx.lineCap = "round";
813
+ for (const link of visible.links) {
814
+ const source = positions.get(link.source);
815
+ const target = positions.get(link.target);
816
+ if (!source || !target) continue;
817
+ const emphasized = related.has(link.source) && related.has(link.target);
818
+ const alpha = dimOthers ? emphasized ? 0.72 : 0.06 : 0.28;
819
+ const [sx, sy] = toScreen(source.x, source.y);
820
+ const [tx, ty] = toScreen(target.x, target.y);
821
+ if (!segmentVisible(sx, sy, tx, ty)) continue;
822
+ ctx.strokeStyle = withAlpha(colors.edge, alpha);
823
+ ctx.lineWidth = Math.max(
824
+ 0.5,
825
+ linkThickness * (emphasized ? 1.15 : 0.7) * Math.sqrt(camera.scale)
826
+ );
827
+ ctx.beginPath();
828
+ ctx.moveTo(sx, sy);
829
+ ctx.lineTo(tx, ty);
830
+ ctx.stroke();
831
+ if (input?.settings.arrows) {
832
+ drawArrow(sx, sy, tx, ty, alpha);
833
+ }
834
+ }
835
+ const labelBudget = visible.nodes.length > 900 ? 0 : visible.nodes.length > 500 ? 120 : 400;
836
+ let labelsDrawn = 0;
837
+ const labelBounds = [];
838
+ for (const entry of positions.values()) {
839
+ const { node, x, y, radius } = entry;
840
+ const [sx, sy] = toScreen(x, y);
841
+ const screenRadius = Math.max(1.5, radius * camera.scale);
842
+ if (sx < -screenRadius || sy < -screenRadius || sx > size.width + screenRadius || sy > size.height + screenRadius) {
843
+ continue;
844
+ }
845
+ const isRelated = related.has(node.id);
846
+ const alpha = dimOthers && !isRelated ? 0.15 : 0.92;
847
+ const isSelected = selected === node.id;
848
+ const isHovered = hoveredId === node.id;
849
+ if (isSelected || isHovered) {
850
+ ctx.beginPath();
851
+ ctx.arc(sx, sy, screenRadius + 3, 0, Math.PI * 2);
852
+ ctx.strokeStyle = withAlpha(colors.focus, isSelected ? 0.9 : 0.5);
853
+ ctx.lineWidth = 1.5;
854
+ ctx.stroke();
855
+ }
856
+ ctx.beginPath();
857
+ ctx.arc(sx, sy, screenRadius, 0, Math.PI * 2);
858
+ ctx.fillStyle = withAlpha(nodeColor(node), alpha);
859
+ ctx.fill();
860
+ ctx.strokeStyle = withAlpha(colors.background, 0.8);
861
+ ctx.lineWidth = 1;
862
+ ctx.stroke();
863
+ const canDrawLabel = labelAlpha > 0.02 && (labelBudget === 0 ? isSelected || isHovered || isRelated : labelsDrawn < labelBudget);
864
+ if (!canDrawLabel) continue;
865
+ labelsDrawn += 1;
866
+ const title = truncateLabel(node.title);
867
+ ctx.font = "11px ui-sans-serif, system-ui, sans-serif";
868
+ ctx.textBaseline = "middle";
869
+ ctx.textAlign = "left";
870
+ const labelWidth = ctx.measureText(title).width;
871
+ let labelX = sx + screenRadius + 6;
872
+ let left = labelX;
873
+ let right = labelX + labelWidth;
874
+ if (right > size.width - 4) {
875
+ labelX = sx - screenRadius - 6;
876
+ ctx.textAlign = "right";
877
+ left = labelX - labelWidth;
878
+ right = labelX;
879
+ }
880
+ const labelY = sy;
881
+ const top = labelY - 7;
882
+ const bottom = labelY + 7;
883
+ if (top < 4 || bottom > size.height - 4 || labelBounds.some(
884
+ (bounds) => left < bounds.right && right > bounds.left && top < bounds.bottom && bottom > bounds.top
885
+ )) {
886
+ continue;
887
+ }
888
+ labelBounds.push({ left, top, right, bottom });
889
+ ctx.strokeStyle = withAlpha(colors.background, 0.85 * labelAlpha * alpha);
890
+ ctx.lineWidth = 3;
891
+ ctx.strokeText(title, labelX, labelY);
892
+ ctx.fillStyle = withAlpha(colors.label, labelAlpha * alpha);
893
+ ctx.fillText(title, labelX, labelY);
894
+ }
895
+ }
896
+ function drawArrow(sx, sy, tx, ty, alpha) {
897
+ const angle = Math.atan2(ty - sy, tx - sx);
898
+ const length = 5;
899
+ const x = tx - Math.cos(angle) * 4;
900
+ const y = ty - Math.sin(angle) * 4;
901
+ ctx.fillStyle = withAlpha(colors.edge, Math.min(0.8, alpha + 0.1));
902
+ ctx.beginPath();
903
+ ctx.moveTo(x, y);
904
+ ctx.lineTo(
905
+ x - Math.cos(angle - 0.45) * length,
906
+ y - Math.sin(angle - 0.45) * length
907
+ );
908
+ ctx.lineTo(
909
+ x - Math.cos(angle + 0.45) * length,
910
+ y - Math.sin(angle + 0.45) * length
911
+ );
912
+ ctx.closePath();
913
+ ctx.fill();
914
+ }
915
+ function toScreen(x, y) {
916
+ return [camera.x + x * camera.scale, camera.y + y * camera.scale];
917
+ }
918
+ function toWorld(clientX, clientY) {
919
+ const rect = canvas.getBoundingClientRect();
920
+ return {
921
+ x: (clientX - rect.left - camera.x) / camera.scale,
922
+ y: (clientY - rect.top - camera.y) / camera.scale
923
+ };
924
+ }
925
+ function hitTest(clientX, clientY) {
926
+ if (!input) return null;
927
+ const rect = canvas.getBoundingClientRect();
928
+ const x = clientX - rect.left;
929
+ const y = clientY - rect.top;
930
+ let best = null;
931
+ const nodes = simulation?.nodes ?? [];
932
+ const simulationById = new Map(nodes.map((node) => [node.id, node]));
933
+ for (const node of visible.nodes) {
934
+ if ((reveal.get(node.id) ?? 0) > animationCutoff) continue;
935
+ const simulationNode = simulationById.get(node.id);
936
+ if (!simulationNode) continue;
937
+ const [sx, sy] = toScreen(simulationNode.x, simulationNode.y);
938
+ const radius = Math.max(
939
+ 6,
940
+ node.radius * (input.settings.nodeSize ?? 1) * camera.scale + 4
941
+ );
942
+ const distance = Math.hypot(x - sx, y - sy);
943
+ if (distance <= radius && (!best || distance < best.distance)) {
944
+ best = { node, distance };
945
+ }
946
+ }
947
+ return best?.node ?? null;
948
+ }
949
+ function onPointerDown(event) {
950
+ if (event.button !== 0 && event.pointerType === "mouse") return;
951
+ pendingFit = false;
952
+ canvas.focus({ preventScroll: true });
953
+ canvas.setPointerCapture(event.pointerId);
954
+ pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
955
+ if (pointers.size === 2) {
956
+ const [first, second] = [...pointers.values()];
957
+ if (first && second) {
958
+ pinch = {
959
+ distance: Math.hypot(second.x - first.x, second.y - first.y),
960
+ center: {
961
+ x: (first.x + second.x) / 2,
962
+ y: (first.y + second.y) / 2
963
+ }
964
+ };
965
+ if (drag?.kind === "node" && simulation) {
966
+ simulation.release(drag.id);
967
+ }
968
+ drag = null;
969
+ }
970
+ return;
971
+ }
972
+ const node = hitTest(event.clientX, event.clientY);
973
+ if (node) {
974
+ selectedId = node.id;
975
+ callbacks.onSelect?.(node);
976
+ drag = {
977
+ kind: "node",
978
+ id: node.id,
979
+ startX: event.clientX,
980
+ startY: event.clientY,
981
+ moved: false
982
+ };
983
+ requestFrame();
984
+ return;
985
+ }
986
+ drag = {
987
+ kind: "pan",
988
+ lastX: event.clientX,
989
+ lastY: event.clientY,
990
+ moved: false
991
+ };
992
+ }
993
+ function onPointerMove(event) {
994
+ if (pointers.has(event.pointerId)) {
995
+ pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
996
+ }
997
+ if (pinch && pointers.size >= 2) {
998
+ const [first, second] = [...pointers.values()];
999
+ if (!first || !second) return;
1000
+ const distance = Math.hypot(second.x - first.x, second.y - first.y);
1001
+ const center = {
1002
+ x: (first.x + second.x) / 2,
1003
+ y: (first.y + second.y) / 2
1004
+ };
1005
+ const rect = canvas.getBoundingClientRect();
1006
+ camera = {
1007
+ ...camera,
1008
+ x: camera.x + center.x - pinch.center.x,
1009
+ y: camera.y + center.y - pinch.center.y
1010
+ };
1011
+ if (pinch.distance > 0) {
1012
+ zoomBy(distance / pinch.distance, {
1013
+ x: center.x - rect.left,
1014
+ y: center.y - rect.top
1015
+ });
1016
+ }
1017
+ pinch = { distance, center };
1018
+ return;
1019
+ }
1020
+ if (drag?.kind === "node") {
1021
+ const moved = drag.moved || Math.hypot(event.clientX - drag.startX, event.clientY - drag.startY) >= DRAG_THRESHOLD_PX;
1022
+ if (!moved) return;
1023
+ drag.moved = true;
1024
+ const point = toWorld(event.clientX, event.clientY);
1025
+ simulation?.fix(drag.id, point.x, point.y);
1026
+ requestFrame();
1027
+ return;
1028
+ }
1029
+ if (drag?.kind === "pan") {
1030
+ const dx = event.clientX - drag.lastX;
1031
+ const dy = event.clientY - drag.lastY;
1032
+ if (Math.abs(dx) + Math.abs(dy) > 0) drag.moved = true;
1033
+ camera = { ...camera, x: camera.x + dx, y: camera.y + dy };
1034
+ drag.lastX = event.clientX;
1035
+ drag.lastY = event.clientY;
1036
+ requestFrame();
1037
+ return;
1038
+ }
1039
+ const node = hitTest(event.clientX, event.clientY);
1040
+ const nextHovered = node?.id ?? null;
1041
+ canvas.style.cursor = node ? "pointer" : "grab";
1042
+ if (hoveredId !== nextHovered) {
1043
+ hoveredId = nextHovered;
1044
+ callbacks.onHover?.(node);
1045
+ requestFrame();
1046
+ }
1047
+ }
1048
+ function onPointerUp(event) {
1049
+ pointers.delete(event.pointerId);
1050
+ if (canvas.hasPointerCapture(event.pointerId)) {
1051
+ canvas.releasePointerCapture(event.pointerId);
1052
+ }
1053
+ if (pointers.size < 2) pinch = null;
1054
+ const currentDrag = drag;
1055
+ drag = null;
1056
+ if (!currentDrag) return;
1057
+ if (currentDrag.kind === "node") {
1058
+ if (simulation) simulation.release(currentDrag.id);
1059
+ if (!currentDrag.moved) {
1060
+ const node = visible.nodes.find((entry) => entry.id === currentDrag.id);
1061
+ if (node) callbacks.onOpen?.(node);
1062
+ }
1063
+ } else if (!currentDrag.moved) {
1064
+ selectedId = null;
1065
+ callbacks.onSelect?.(null);
1066
+ }
1067
+ requestFrame();
1068
+ }
1069
+ function onPointerLeave() {
1070
+ if (drag) return;
1071
+ if (hoveredId !== null) {
1072
+ hoveredId = null;
1073
+ callbacks.onHover?.(null);
1074
+ requestFrame();
1075
+ }
1076
+ }
1077
+ function onWheel(event) {
1078
+ event.preventDefault();
1079
+ const rect = canvas.getBoundingClientRect();
1080
+ zoomBy(Math.exp(-event.deltaY * 15e-4), {
1081
+ x: event.clientX - rect.left,
1082
+ y: event.clientY - rect.top
1083
+ });
1084
+ }
1085
+ function onDoubleClick(event) {
1086
+ const node = hitTest(event.clientX, event.clientY);
1087
+ if (node) {
1088
+ callbacks.onOpen?.(node);
1089
+ return;
1090
+ }
1091
+ resetView();
1092
+ }
1093
+ function onContextMenu(event) {
1094
+ const node = hitTest(event.clientX, event.clientY);
1095
+ if (!node) return;
1096
+ event.preventDefault();
1097
+ callbacks.onContextMenu?.(node, event.clientX, event.clientY);
1098
+ }
1099
+ function onKeyDown(event) {
1100
+ const step = event.shiftKey ? 120 : 40;
1101
+ switch (event.key) {
1102
+ case "+":
1103
+ case "=":
1104
+ event.preventDefault();
1105
+ zoomBy(1.2);
1106
+ return;
1107
+ case "-":
1108
+ case "_":
1109
+ event.preventDefault();
1110
+ zoomBy(1 / 1.2);
1111
+ return;
1112
+ case "ArrowLeft":
1113
+ event.preventDefault();
1114
+ pendingFit = false;
1115
+ camera = { ...camera, x: camera.x + step };
1116
+ break;
1117
+ case "ArrowRight":
1118
+ event.preventDefault();
1119
+ pendingFit = false;
1120
+ camera = { ...camera, x: camera.x - step };
1121
+ break;
1122
+ case "ArrowUp":
1123
+ event.preventDefault();
1124
+ pendingFit = false;
1125
+ camera = { ...camera, y: camera.y + step };
1126
+ break;
1127
+ case "ArrowDown":
1128
+ event.preventDefault();
1129
+ pendingFit = false;
1130
+ camera = { ...camera, y: camera.y - step };
1131
+ break;
1132
+ case "Escape":
1133
+ selectedId = null;
1134
+ callbacks.onSelect?.(null);
1135
+ break;
1136
+ case "Enter": {
1137
+ const node = visible.nodes.find(
1138
+ (entry) => entry.id === (hoveredId ?? selectedId)
1139
+ ) ?? null;
1140
+ if (node) {
1141
+ event.preventDefault();
1142
+ callbacks.onOpen?.(node);
1143
+ }
1144
+ return;
1145
+ }
1146
+ case "n":
1147
+ case "N":
1148
+ case "p":
1149
+ case "P": {
1150
+ const index = visible.nodes.findIndex(
1151
+ (entry) => entry.id === selectedId
1152
+ );
1153
+ if (visible.nodes.length === 0) return;
1154
+ event.preventDefault();
1155
+ const direction = event.key.toLowerCase() === "n" ? 1 : -1;
1156
+ const next = visible.nodes[(index + direction + visible.nodes.length) % visible.nodes.length];
1157
+ if (next) {
1158
+ selectedId = next.id;
1159
+ callbacks.onSelect?.(next);
1160
+ focusNode(next.id);
1161
+ requestFrame();
1162
+ }
1163
+ return;
1164
+ }
1165
+ default:
1166
+ return;
1167
+ }
1168
+ requestFrame();
1169
+ }
1170
+ function relatedIds() {
1171
+ const origin = hoveredId;
1172
+ const related = /* @__PURE__ */ new Set();
1173
+ if (!origin) return related;
1174
+ related.add(origin);
1175
+ for (const link of visible.links) {
1176
+ if (link.source === origin) related.add(link.target);
1177
+ if (link.target === origin) related.add(link.source);
1178
+ }
1179
+ return related;
1180
+ }
1181
+ function nodeColor(node) {
1182
+ if (node.color) return node.color;
1183
+ switch (node.kind) {
1184
+ case "unresolved":
1185
+ case "attachment":
1186
+ return colors.muted;
1187
+ case "tag":
1188
+ return colors.link;
1189
+ case "note":
1190
+ return colors.accent;
1191
+ }
1192
+ }
1193
+ function segmentVisible(x1, y1, x2, y2) {
1194
+ return !(Math.max(x1, x2) < 0 || Math.max(y1, y2) < 0 || Math.min(x1, x2) > size.width || Math.min(y1, y2) > size.height);
1195
+ }
1196
+ function destroy() {
1197
+ if (destroyed) return;
1198
+ destroyed = true;
1199
+ if (frameHandle !== null) cancelAnimationFrame(frameHandle);
1200
+ frameHandle = null;
1201
+ simulation?.stop();
1202
+ resizeObserver.disconnect();
1203
+ intersectionObserver.disconnect();
1204
+ mutationObserver.disconnect();
1205
+ document.removeEventListener("visibilitychange", onVisibilityChange);
1206
+ canvas.removeEventListener("pointerdown", onPointerDown);
1207
+ canvas.removeEventListener("pointermove", onPointerMove);
1208
+ canvas.removeEventListener("pointerup", onPointerUp);
1209
+ canvas.removeEventListener("pointercancel", onPointerUp);
1210
+ canvas.removeEventListener("pointerleave", onPointerLeave);
1211
+ canvas.removeEventListener("wheel", onWheel);
1212
+ canvas.removeEventListener("dblclick", onDoubleClick);
1213
+ canvas.removeEventListener("contextmenu", onContextMenu);
1214
+ canvas.removeEventListener("keydown", onKeyDown);
1215
+ }
1216
+ }
1217
+ function forceOptions(settings) {
1218
+ return {
1219
+ centerForce: settings.centerForce,
1220
+ repelForce: settings.repelForce,
1221
+ linkForce: settings.linkForce,
1222
+ linkDistance: settings.linkDistance
1223
+ };
1224
+ }
1225
+ function visibleSignature(visible) {
1226
+ return `${visible.nodes.map((node) => node.id).join("\0")}|${visible.links.map((link) => `${link.source}>${link.target}`).join("\0")}`;
1227
+ }
1228
+ function readColors(canvas) {
1229
+ const style = getComputedStyle(canvas);
1230
+ return {
1231
+ background: css(style, "--rb-color-paper", "#ffffff"),
1232
+ accent: css(style, "--rb-color-accent", "#2563eb"),
1233
+ link: css(style, "--rb-color-link", "#2563eb"),
1234
+ ink: css(style, "--rb-color-ink", "#1f2937"),
1235
+ muted: css(style, "--rb-color-muted", "#6b7280"),
1236
+ edge: css(style, "--rb-color-border-strong", "#94a3b8"),
1237
+ label: css(style, "--rb-color-ink", "#1f2937"),
1238
+ focus: css(style, "--rb-color-focus", "#2563eb")
1239
+ };
1240
+ }
1241
+ function css(style, name, fallback) {
1242
+ const value = style.getPropertyValue(name).trim();
1243
+ return value.length > 0 ? value : fallback;
1244
+ }
1245
+ function withAlpha(color, alpha) {
1246
+ const clamped = clamp(alpha, 0, 1);
1247
+ if (color.startsWith("#")) {
1248
+ const hex = color.length === 4 ? color.slice(1).split("").map((character) => character + character).join("") : color.slice(1);
1249
+ if (hex.length === 6 && /^[0-9a-f]{6}$/i.test(hex)) {
1250
+ const red = Number.parseInt(hex.slice(0, 2), 16);
1251
+ const green = Number.parseInt(hex.slice(2, 4), 16);
1252
+ const blue = Number.parseInt(hex.slice(4, 6), 16);
1253
+ return `rgba(${red}, ${green}, ${blue}, ${clamped})`;
1254
+ }
1255
+ }
1256
+ if (color.startsWith("rgb(")) {
1257
+ return color.replace("rgb(", "rgba(").replace(")", `, ${clamped})`);
1258
+ }
1259
+ return color;
1260
+ }
1261
+ function truncateLabel(title) {
1262
+ return title.length > 34 ? `${title.slice(0, 33)}…` : title;
1263
+ }
1264
+ function clamp(value, min, max) {
1265
+ return Math.min(Math.max(value, min), max);
1266
+ }
1267
+ function createNoopRenderer() {
1268
+ return {
1269
+ update() {
1270
+ },
1271
+ setSelected() {
1272
+ },
1273
+ zoomBy() {
1274
+ },
1275
+ resetView() {
1276
+ },
1277
+ focusNode() {
1278
+ },
1279
+ playAnimation() {
1280
+ },
1281
+ stopAnimation() {
1282
+ },
1283
+ isAnimating: () => false,
1284
+ destroy() {
1285
+ }
1286
+ };
1287
+ }
1288
+ export {
1289
+ createGraphRenderer
1290
+ };