@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/index.js ADDED
@@ -0,0 +1,894 @@
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
+ function hasDatedNodes(nodes) {
30
+ return nodes.some((node) => node.date !== null);
31
+ }
32
+
33
+ // src/query.ts
34
+ var SUPPORTED_OPERATORS = /* @__PURE__ */ new Set(["file", "path", "tag"]);
35
+ var TOKEN_PATTERN = /"[^"]*"|\S+/g;
36
+ function parseGraphQuery(input) {
37
+ const terms = [];
38
+ const unsupportedOperators = /* @__PURE__ */ new Set();
39
+ for (const rawToken of input.match(TOKEN_PATTERN) ?? []) {
40
+ let token = rawToken.trim();
41
+ if (token.length === 0) continue;
42
+ let negated = false;
43
+ if (token.startsWith("-") && token.length > 1) {
44
+ negated = true;
45
+ token = token.slice(1);
46
+ }
47
+ const separator = token.indexOf(":");
48
+ if (separator > 0) {
49
+ const operator = token.slice(0, separator).toLocaleLowerCase();
50
+ if (!/^[a-z]+$/.test(operator)) {
51
+ terms.push({ field: "text", value: unquote(token), negated });
52
+ continue;
53
+ }
54
+ if (!SUPPORTED_OPERATORS.has(operator)) {
55
+ unsupportedOperators.add(operator);
56
+ terms.push({
57
+ field: "unsupported",
58
+ value: unquote(token.slice(separator + 1)),
59
+ negated
60
+ });
61
+ continue;
62
+ }
63
+ terms.push({
64
+ field: operator,
65
+ value: normalizeTagValue(unquote(token.slice(separator + 1))),
66
+ negated
67
+ });
68
+ continue;
69
+ }
70
+ terms.push({ field: "text", value: unquote(token), negated });
71
+ }
72
+ return { terms, unsupportedOperators: [...unsupportedOperators] };
73
+ }
74
+ function matchesGraphQuery(node, query) {
75
+ for (const term of query.terms) {
76
+ const matched = matchesTerm(node, term);
77
+ if (term.negated ? matched : !matched) return false;
78
+ }
79
+ return true;
80
+ }
81
+ function normalizeGraphText(value) {
82
+ return value.normalize("NFKC").toLocaleLowerCase().replace(
83
+ /[ァ-ン]/g,
84
+ (character) => String.fromCharCode(character.charCodeAt(0) - 96)
85
+ );
86
+ }
87
+ function matchesTerm(node, term) {
88
+ const value = normalizeGraphText(term.value);
89
+ if (value.length === 0) return false;
90
+ switch (term.field) {
91
+ case "file":
92
+ return normalizeGraphText(node.title).includes(value);
93
+ case "path":
94
+ return normalizeGraphText(node.path).includes(value);
95
+ case "tag":
96
+ return node.tags.some((tag) => {
97
+ const normalized = normalizeGraphText(tag);
98
+ return normalized === value || normalized.startsWith(`${value}/`);
99
+ });
100
+ case "text":
101
+ return normalizeGraphText(node.title).includes(value) || normalizeGraphText(node.path).includes(value) || node.tags.some((tag) => normalizeGraphText(tag).includes(value));
102
+ case "unsupported":
103
+ return false;
104
+ }
105
+ }
106
+ function normalizeTagValue(value) {
107
+ return value.replace(/^#+/, "");
108
+ }
109
+ function unquote(value) {
110
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
111
+ return value.slice(1, -1);
112
+ }
113
+ return value;
114
+ }
115
+
116
+ // src/document.ts
117
+ function buildGraphDocument(entries, options = {}) {
118
+ const includeTags = options.includeTags ?? true;
119
+ const includeAttachments = options.includeAttachments ?? true;
120
+ const includeUnresolved = options.includeUnresolved ?? true;
121
+ const entryBySlug = new Map(entries.map((entry) => [entry.slug, entry]));
122
+ const nodes = /* @__PURE__ */ new Map();
123
+ const links = /* @__PURE__ */ new Map();
124
+ for (const entry of entries) {
125
+ nodes.set(entry.slug, {
126
+ id: entry.slug,
127
+ kind: "note",
128
+ slug: entry.slug,
129
+ title: entry.title,
130
+ permalink: entry.permalink,
131
+ path: entry.slug,
132
+ tags: uniqueTags(entry.tags ?? []),
133
+ date: normalizeDate(entry.date ?? null)
134
+ });
135
+ }
136
+ for (const entry of entries) {
137
+ for (const tag of uniqueTags(entry.tags ?? [])) {
138
+ if (!includeTags) break;
139
+ const id = `tag:${normalizeGraphText(tag)}`;
140
+ if (!nodes.has(id)) {
141
+ nodes.set(id, {
142
+ id,
143
+ kind: "tag",
144
+ slug: null,
145
+ title: `#${tag}`,
146
+ permalink: null,
147
+ path: `#${tag}`,
148
+ tags: [tag],
149
+ date: null
150
+ });
151
+ }
152
+ addLink(links, entry.slug, id);
153
+ }
154
+ const assets = uniqueAssets(entry.assets ?? []);
155
+ for (const link of entry.links ?? []) {
156
+ if (link.kind === "note" && link.slug !== null) {
157
+ if (entryBySlug.has(link.slug) && link.slug !== entry.slug) {
158
+ addLink(links, entry.slug, link.slug);
159
+ }
160
+ continue;
161
+ }
162
+ if ((link.kind === "attachment" || link.kind === "image") && includeAttachments) {
163
+ const asset = findAsset(assets, link.raw);
164
+ if (!asset) continue;
165
+ const id = `attachment:${asset.path}`;
166
+ if (!nodes.has(id)) {
167
+ nodes.set(id, {
168
+ id,
169
+ kind: "attachment",
170
+ slug: null,
171
+ title: basename(asset.path),
172
+ permalink: null,
173
+ path: asset.path,
174
+ tags: [],
175
+ date: null
176
+ });
177
+ }
178
+ addLink(links, entry.slug, id);
179
+ continue;
180
+ }
181
+ if (includeUnresolved && link.raw.trim().length > 0) {
182
+ const key = normalizeGraphText(link.raw);
183
+ const id = `unresolved:${key}`;
184
+ if (!nodes.has(id)) {
185
+ nodes.set(id, {
186
+ id,
187
+ kind: "unresolved",
188
+ slug: null,
189
+ title: link.raw,
190
+ permalink: null,
191
+ path: link.raw,
192
+ tags: [],
193
+ date: null
194
+ });
195
+ }
196
+ addLink(links, entry.slug, id);
197
+ }
198
+ }
199
+ }
200
+ const nodeList = [...nodes.values()].sort(compareNode);
201
+ const centerId = options.centerSlug && entryBySlug.has(options.centerSlug) ? options.centerSlug : entries[0]?.slug ?? null;
202
+ return {
203
+ nodes: nodeList,
204
+ links: [...links.values()].sort(
205
+ (left, right) => left.source.localeCompare(right.source) || left.target.localeCompare(right.target)
206
+ ),
207
+ centerId
208
+ };
209
+ }
210
+ function sliceGraphDocument(document, centerId, depth, maxNodes = 320) {
211
+ const nodeById = new Map(document.nodes.map((node) => [node.id, node]));
212
+ if (!nodeById.has(centerId)) {
213
+ return {
214
+ document: { nodes: [], links: [], centerId: null },
215
+ truncated: false
216
+ };
217
+ }
218
+ const adjacency = /* @__PURE__ */ new Map();
219
+ for (const node of document.nodes) adjacency.set(node.id, []);
220
+ for (const link of document.links) {
221
+ adjacency.get(link.source)?.push(link.target);
222
+ adjacency.get(link.target)?.push(link.source);
223
+ }
224
+ for (const neighbors of adjacency.values()) {
225
+ neighbors.sort((left, right) => left.localeCompare(right));
226
+ }
227
+ const selected = /* @__PURE__ */ new Set([centerId]);
228
+ const limit = Math.max(1, Math.floor(maxNodes));
229
+ let frontier = [centerId];
230
+ let truncated = false;
231
+ for (let level = 0; level < Math.max(0, depth); level += 1) {
232
+ const next = [];
233
+ for (const id of frontier) {
234
+ for (const neighbor of adjacency.get(id) ?? []) {
235
+ if (selected.has(neighbor)) continue;
236
+ if (selected.size >= limit) {
237
+ truncated = true;
238
+ break;
239
+ }
240
+ selected.add(neighbor);
241
+ next.push(neighbor);
242
+ }
243
+ if (truncated) break;
244
+ }
245
+ if (truncated || next.length === 0) break;
246
+ frontier = next;
247
+ }
248
+ const nodes = document.nodes.filter((node) => selected.has(node.id)).sort(compareNode);
249
+ const links = document.links.filter(
250
+ (link) => selected.has(link.source) && selected.has(link.target)
251
+ );
252
+ return {
253
+ document: { nodes, links, centerId },
254
+ truncated
255
+ };
256
+ }
257
+ function addLink(links, source, target) {
258
+ if (source === target) return;
259
+ links.set(`${source}\0${target}`, { source, target });
260
+ }
261
+ function compareNode(left, right) {
262
+ return kindOrder(left.kind) - kindOrder(right.kind) || left.id.localeCompare(right.id);
263
+ }
264
+ function kindOrder(kind) {
265
+ switch (kind) {
266
+ case "note":
267
+ return 0;
268
+ case "unresolved":
269
+ return 1;
270
+ case "attachment":
271
+ return 2;
272
+ case "tag":
273
+ return 3;
274
+ }
275
+ }
276
+ function uniqueTags(tags) {
277
+ const seen = /* @__PURE__ */ new Map();
278
+ for (const raw of tags) {
279
+ const tag = raw.trim().replace(/^#+/, "");
280
+ if (tag.length === 0) continue;
281
+ const key = normalizeGraphText(tag);
282
+ if (!seen.has(key)) seen.set(key, tag);
283
+ }
284
+ return [...seen.values()].sort((left, right) => left.localeCompare(right));
285
+ }
286
+ function uniqueAssets(assets) {
287
+ const seen = /* @__PURE__ */ new Map();
288
+ for (const asset of assets) {
289
+ seen.set(asset.path, asset);
290
+ }
291
+ return [...seen.values()];
292
+ }
293
+ function findAsset(assets, raw) {
294
+ const value = raw.replace(/^\.\//, "");
295
+ return assets.find(
296
+ (asset) => asset.path === value || asset.sourcePath === value
297
+ ) ?? assets.find(
298
+ (asset) => asset.path.endsWith(`/${value}`) || asset.sourcePath?.endsWith(`/${value}`) === true
299
+ ) ?? null;
300
+ }
301
+ function basename(path) {
302
+ const segments = path.split("/");
303
+ return segments[segments.length - 1] ?? path;
304
+ }
305
+ function normalizeDate(value) {
306
+ if (!value) return null;
307
+ const time = Date.parse(value);
308
+ return Number.isFinite(time) ? new Date(time).toISOString() : null;
309
+ }
310
+
311
+ // src/settings.ts
312
+ var GRAPH_SETTINGS_RANGES = {
313
+ depth: { min: 0, max: 5, step: 1 },
314
+ textFade: { min: 0, max: 1, step: 0.05 },
315
+ nodeSize: { min: 0.5, max: 2.5, step: 0.1 },
316
+ linkThickness: { min: 0.25, max: 3, step: 0.05 },
317
+ centerForce: { min: 0, max: 1, step: 0.05 },
318
+ repelForce: { min: 0, max: 2e3, step: 10 },
319
+ linkForce: { min: 0, max: 1, step: 0.05 },
320
+ linkDistance: { min: 30, max: 500, step: 5 }
321
+ };
322
+ var DEFAULT_GRAPH_SETTINGS = Object.freeze({
323
+ search: "",
324
+ showTags: true,
325
+ showAttachments: true,
326
+ existingOnly: false,
327
+ showOrphans: true,
328
+ depth: 2,
329
+ arrows: false,
330
+ textFade: 0.35,
331
+ nodeSize: 1,
332
+ linkThickness: 1,
333
+ animate: false,
334
+ centerForce: 0.35,
335
+ repelForce: 600,
336
+ linkForce: 0.7,
337
+ linkDistance: 100,
338
+ groups: []
339
+ });
340
+ var GROUP_COLORS = [
341
+ "#e07a5f",
342
+ "#3d85c6",
343
+ "#6aa84f",
344
+ "#c27ba0",
345
+ "#e6b422",
346
+ "#7c6ee6"
347
+ ];
348
+ var MAX_GROUPS = 24;
349
+ function mergeGraphSettings(base, patch) {
350
+ return normalizeGraphSettings({ ...base, ...patch });
351
+ }
352
+ function normalizeGraphSettings(value) {
353
+ const source = value !== null && typeof value === "object" ? value : {};
354
+ return {
355
+ search: text(source.search, DEFAULT_GRAPH_SETTINGS.search, 200),
356
+ showTags: flag(source.showTags, DEFAULT_GRAPH_SETTINGS.showTags),
357
+ showAttachments: flag(
358
+ source.showAttachments,
359
+ DEFAULT_GRAPH_SETTINGS.showAttachments
360
+ ),
361
+ existingOnly: flag(
362
+ source.existingOnly,
363
+ DEFAULT_GRAPH_SETTINGS.existingOnly
364
+ ),
365
+ showOrphans: flag(source.showOrphans, DEFAULT_GRAPH_SETTINGS.showOrphans),
366
+ depth: integer(source.depth, "depth", DEFAULT_GRAPH_SETTINGS.depth),
367
+ arrows: flag(source.arrows, DEFAULT_GRAPH_SETTINGS.arrows),
368
+ textFade: number(
369
+ source.textFade,
370
+ "textFade",
371
+ DEFAULT_GRAPH_SETTINGS.textFade
372
+ ),
373
+ nodeSize: number(
374
+ source.nodeSize,
375
+ "nodeSize",
376
+ DEFAULT_GRAPH_SETTINGS.nodeSize
377
+ ),
378
+ linkThickness: number(
379
+ source.linkThickness,
380
+ "linkThickness",
381
+ DEFAULT_GRAPH_SETTINGS.linkThickness
382
+ ),
383
+ animate: flag(source.animate, DEFAULT_GRAPH_SETTINGS.animate),
384
+ centerForce: number(
385
+ source.centerForce,
386
+ "centerForce",
387
+ DEFAULT_GRAPH_SETTINGS.centerForce
388
+ ),
389
+ repelForce: number(
390
+ source.repelForce,
391
+ "repelForce",
392
+ DEFAULT_GRAPH_SETTINGS.repelForce
393
+ ),
394
+ linkForce: number(
395
+ source.linkForce,
396
+ "linkForce",
397
+ DEFAULT_GRAPH_SETTINGS.linkForce
398
+ ),
399
+ linkDistance: number(
400
+ source.linkDistance,
401
+ "linkDistance",
402
+ DEFAULT_GRAPH_SETTINGS.linkDistance
403
+ ),
404
+ groups: groups(source.groups)
405
+ };
406
+ }
407
+ function readStoredGraphSettings(storage, key) {
408
+ try {
409
+ const raw = storage.getItem(key);
410
+ if (!raw) return null;
411
+ return normalizeGraphSettings(JSON.parse(raw));
412
+ } catch {
413
+ return null;
414
+ }
415
+ }
416
+ function writeStoredGraphSettings(storage, key, settings) {
417
+ try {
418
+ storage.setItem(key, JSON.stringify(settings));
419
+ } catch {
420
+ return;
421
+ }
422
+ }
423
+ function flag(value, fallback) {
424
+ return typeof value === "boolean" ? value : fallback;
425
+ }
426
+ function number(value, key, fallback) {
427
+ const range = GRAPH_SETTINGS_RANGES[key];
428
+ if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
429
+ return clamp(value, range.min, range.max);
430
+ }
431
+ function integer(value, key, fallback) {
432
+ return Math.round(number(value, key, fallback));
433
+ }
434
+ function text(value, fallback, maxLength) {
435
+ return typeof value === "string" ? value.slice(0, maxLength) : fallback;
436
+ }
437
+ function groups(value) {
438
+ if (!Array.isArray(value)) return [];
439
+ const result = [];
440
+ const seen = /* @__PURE__ */ new Set();
441
+ for (const item of value) {
442
+ if (result.length >= MAX_GROUPS) break;
443
+ if (item === null || typeof item !== "object") continue;
444
+ const source = item;
445
+ if (typeof source.id !== "string" || source.id.length === 0) continue;
446
+ if (seen.has(source.id)) continue;
447
+ const query = text(source.query, "", 200);
448
+ if (query.trim().length === 0) continue;
449
+ seen.add(source.id);
450
+ result.push({
451
+ id: source.id.slice(0, 64),
452
+ query,
453
+ color: color(source.color, result.length)
454
+ });
455
+ }
456
+ return result;
457
+ }
458
+ function color(value, index) {
459
+ if (typeof value === "string" && /^#[0-9a-f]{6}$/i.test(value)) {
460
+ return value.toLocaleLowerCase();
461
+ }
462
+ return GROUP_COLORS[index % GROUP_COLORS.length] ?? "#6aa84f";
463
+ }
464
+ function clamp(value, min, max) {
465
+ return Math.min(Math.max(value, min), max);
466
+ }
467
+
468
+ // src/simulation.ts
469
+ var ALPHA_MIN = 1e-3;
470
+ var ALPHA_DECAY = 0.0228;
471
+ var VELOCITY_DECAY = 0.4;
472
+ var MAX_VELOCITY = 40;
473
+ var GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
474
+ function createGraphSimulation(input) {
475
+ const ids = [...new Set(input.nodeIds)].sort(
476
+ (left, right) => left.localeCompare(right)
477
+ );
478
+ const nodes = ids.map((id, index) => {
479
+ const previous = input.positions?.get(id);
480
+ if (previous && Number.isFinite(previous.x) && Number.isFinite(previous.y)) {
481
+ return {
482
+ id,
483
+ x: previous.x,
484
+ y: previous.y,
485
+ vx: 0,
486
+ vy: 0,
487
+ fx: null,
488
+ fy: null
489
+ };
490
+ }
491
+ if (id === input.centerId) {
492
+ return { id, x: 0, y: 0, vx: 0, vy: 0, fx: null, fy: null };
493
+ }
494
+ const radius = 18 + 13 * Math.sqrt(index);
495
+ const angle = index * GOLDEN_ANGLE + hash(id) % 360 * (Math.PI / 180);
496
+ return {
497
+ id,
498
+ x: Math.cos(angle) * radius,
499
+ y: Math.sin(angle) * radius,
500
+ vx: 0,
501
+ vy: 0,
502
+ fx: null,
503
+ fy: null
504
+ };
505
+ });
506
+ const indexById = new Map(nodes.map((node, index) => [node.id, index]));
507
+ const links = input.links.map((link) => ({
508
+ source: indexById.get(link.source),
509
+ target: indexById.get(link.target)
510
+ })).filter(
511
+ (link) => link.source !== void 0 && link.target !== void 0
512
+ );
513
+ let options = { ...input.options };
514
+ let alpha = 1;
515
+ let alphaTarget = 0;
516
+ let active = true;
517
+ function tick() {
518
+ if (!active && alpha < ALPHA_MIN) return false;
519
+ alpha += (alphaTarget - alpha) * ALPHA_DECAY;
520
+ applyCenterForce(nodes, options.centerForce, alpha);
521
+ applyLinkForce(nodes, links, options, alpha);
522
+ applyRepelForce(nodes, options.repelForce * alpha * 0.02);
523
+ for (const node of nodes) {
524
+ if (node.fx !== null && node.fy !== null) {
525
+ node.x = node.fx;
526
+ node.y = node.fy;
527
+ node.vx = 0;
528
+ node.vy = 0;
529
+ continue;
530
+ }
531
+ node.vx *= 1 - VELOCITY_DECAY;
532
+ node.vy *= 1 - VELOCITY_DECAY;
533
+ node.vx = clampVelocity(node.vx);
534
+ node.vy = clampVelocity(node.vy);
535
+ node.x += node.vx;
536
+ node.y += node.vy;
537
+ }
538
+ if (alpha < ALPHA_MIN) {
539
+ alpha = 0;
540
+ active = false;
541
+ }
542
+ return active;
543
+ }
544
+ return {
545
+ nodes,
546
+ tick,
547
+ reheat(value = 0.7) {
548
+ alpha = Math.max(alpha, value);
549
+ active = true;
550
+ },
551
+ setOptions(next) {
552
+ options = { ...next };
553
+ if (alpha < 0.05) {
554
+ alpha = 0.05;
555
+ active = true;
556
+ }
557
+ },
558
+ fix(id, x, y) {
559
+ const index = indexById.get(id);
560
+ const node = index === void 0 ? void 0 : nodes[index];
561
+ if (!node) return;
562
+ node.fx = x;
563
+ node.fy = y;
564
+ node.x = x;
565
+ node.y = y;
566
+ node.vx = 0;
567
+ node.vy = 0;
568
+ alpha = Math.max(alpha, 0.5);
569
+ active = true;
570
+ },
571
+ release(id) {
572
+ const index = indexById.get(id);
573
+ const node = index === void 0 ? void 0 : nodes[index];
574
+ if (!node) return;
575
+ node.fx = null;
576
+ node.fy = null;
577
+ alpha = Math.max(alpha, 0.45);
578
+ active = true;
579
+ },
580
+ stop() {
581
+ alpha = 0;
582
+ alphaTarget = 0;
583
+ active = false;
584
+ },
585
+ isActive() {
586
+ return active;
587
+ },
588
+ getAlpha() {
589
+ return alpha;
590
+ }
591
+ };
592
+ }
593
+ function applyCenterForce(nodes, centerForce, alpha) {
594
+ const strength = centerForce * 0.03 * alpha;
595
+ if (strength <= 0) return;
596
+ for (const node of nodes) {
597
+ node.vx -= node.x * strength;
598
+ node.vy -= node.y * strength;
599
+ }
600
+ }
601
+ function applyLinkForce(nodes, links, options, alpha) {
602
+ const strength = options.linkForce * 0.12 * alpha;
603
+ if (strength <= 0) return;
604
+ for (const link of links) {
605
+ const source = nodes[link.source];
606
+ const target = nodes[link.target];
607
+ if (!source || !target) continue;
608
+ const dx = target.x - source.x;
609
+ const dy = target.y - source.y;
610
+ const distance = Math.hypot(dx, dy);
611
+ if (distance < 1e-3) continue;
612
+ const force = (distance - options.linkDistance) / distance * strength;
613
+ source.vx += dx * force;
614
+ source.vy += dy * force;
615
+ target.vx -= dx * force;
616
+ target.vy -= dy * force;
617
+ }
618
+ }
619
+ function applyRepelForce(nodes, strength) {
620
+ if (strength <= 0 || nodes.length < 2) return;
621
+ if (nodes.length <= 250) {
622
+ applyExactRepulsion(nodes, strength);
623
+ return;
624
+ }
625
+ applyQuadtreeRepulsion(nodes, strength);
626
+ }
627
+ function applyExactRepulsion(nodes, strength) {
628
+ for (let left = 0; left < nodes.length; left += 1) {
629
+ const a = nodes[left];
630
+ if (!a) continue;
631
+ for (let right = left + 1; right < nodes.length; right += 1) {
632
+ const b = nodes[right];
633
+ if (!b) continue;
634
+ const dx = b.x - a.x;
635
+ const dy = b.y - a.y;
636
+ const distanceSquared = dx * dx + dy * dy + 1;
637
+ const force = strength / distanceSquared;
638
+ a.vx -= dx * force;
639
+ a.vy -= dy * force;
640
+ b.vx += dx * force;
641
+ b.vy += dy * force;
642
+ }
643
+ }
644
+ }
645
+ function applyQuadtreeRepulsion(nodes, strength) {
646
+ let minX = Number.POSITIVE_INFINITY;
647
+ let minY = Number.POSITIVE_INFINITY;
648
+ let maxX = Number.NEGATIVE_INFINITY;
649
+ let maxY = Number.NEGATIVE_INFINITY;
650
+ for (const node of nodes) {
651
+ minX = Math.min(minX, node.x);
652
+ minY = Math.min(minY, node.y);
653
+ maxX = Math.max(maxX, node.x);
654
+ maxY = Math.max(maxY, node.y);
655
+ }
656
+ const size = Math.max(maxX - minX, maxY - minY, 1) + 1;
657
+ const root = {
658
+ x: (minX + maxX - size) / 2,
659
+ y: (minY + maxY - size) / 2,
660
+ size,
661
+ mass: 0,
662
+ centerX: 0,
663
+ centerY: 0,
664
+ members: null,
665
+ children: null
666
+ };
667
+ for (let index = 0; index < nodes.length; index += 1) {
668
+ insertQuad(root, nodes, index, 0);
669
+ }
670
+ for (let index = 0; index < nodes.length; index += 1) {
671
+ const node = nodes[index];
672
+ if (node) applyQuadForce(root, nodes, node, index, strength);
673
+ }
674
+ }
675
+ function insertQuad(quad, nodes, index, depth) {
676
+ const node = nodes[index];
677
+ if (!node) return;
678
+ quad.centerX = (quad.centerX * quad.mass + node.x) / (quad.mass + 1);
679
+ quad.centerY = (quad.centerY * quad.mass + node.y) / (quad.mass + 1);
680
+ quad.mass += 1;
681
+ if (quad.children) {
682
+ insertIntoChild(quad, nodes, index, depth + 1);
683
+ return;
684
+ }
685
+ if (!quad.members) {
686
+ quad.members = [index];
687
+ return;
688
+ }
689
+ if (depth >= 16) {
690
+ quad.members.push(index);
691
+ return;
692
+ }
693
+ const members = quad.members;
694
+ quad.members = null;
695
+ const half = quad.size / 2;
696
+ quad.children = [
697
+ createQuad(quad.x, quad.y, half),
698
+ createQuad(quad.x + half, quad.y, half),
699
+ createQuad(quad.x, quad.y + half, half),
700
+ createQuad(quad.x + half, quad.y + half, half)
701
+ ];
702
+ for (const member of members) insertIntoChild(quad, nodes, member, depth + 1);
703
+ insertIntoChild(quad, nodes, index, depth + 1);
704
+ }
705
+ function insertIntoChild(quad, nodes, index, depth) {
706
+ const children = quad.children;
707
+ if (!children) return;
708
+ const node = nodes[index];
709
+ if (!node) return;
710
+ const half = quad.size / 2;
711
+ const right = node.x >= quad.x + half ? 1 : 0;
712
+ const bottom = node.y >= quad.y + half ? 2 : 0;
713
+ const child = children[right + bottom];
714
+ if (child) insertQuad(child, nodes, index, depth);
715
+ }
716
+ function createQuad(x, y, size) {
717
+ return {
718
+ x,
719
+ y,
720
+ size,
721
+ mass: 0,
722
+ centerX: 0,
723
+ centerY: 0,
724
+ members: null,
725
+ children: null
726
+ };
727
+ }
728
+ function applyQuadForce(quad, nodes, target, targetIndex, strength) {
729
+ if (quad.mass === 0) return;
730
+ const dx = quad.centerX - target.x;
731
+ const dy = quad.centerY - target.y;
732
+ const distanceSquared = dx * dx + dy * dy + 1;
733
+ if (!quad.children) {
734
+ for (const index of quad.members ?? []) {
735
+ if (index === targetIndex) continue;
736
+ const other = nodes[index];
737
+ if (other) applyPairRepulsion(target, other, strength);
738
+ }
739
+ return;
740
+ }
741
+ if (quad.size * quad.size < 0.81 * distanceSquared) {
742
+ const force = strength * quad.mass / distanceSquared;
743
+ target.vx -= dx * force;
744
+ target.vy -= dy * force;
745
+ return;
746
+ }
747
+ for (const child of quad.children) {
748
+ applyQuadForce(child, nodes, target, targetIndex, strength);
749
+ }
750
+ }
751
+ function applyPairRepulsion(a, b, strength) {
752
+ const dx = b.x - a.x;
753
+ const dy = b.y - a.y;
754
+ const distanceSquared = dx * dx + dy * dy + 1;
755
+ const force = strength / distanceSquared;
756
+ a.vx -= dx * force;
757
+ a.vy -= dy * force;
758
+ b.vx += dx * force;
759
+ b.vy += dy * force;
760
+ }
761
+ function clampVelocity(value) {
762
+ return Math.min(Math.max(value, -MAX_VELOCITY), MAX_VELOCITY);
763
+ }
764
+ function hash(value) {
765
+ let result = 2166136261;
766
+ for (let index = 0; index < value.length; index += 1) {
767
+ result ^= value.charCodeAt(index);
768
+ result = Math.imul(result, 16777619);
769
+ }
770
+ return result >>> 0;
771
+ }
772
+
773
+ // src/view.ts
774
+ function buildVisibleGraph(input) {
775
+ const { document, settings, mode } = input;
776
+ const centerId = input.centerId ?? document.centerId;
777
+ const nodeById = new Map(document.nodes.map((node) => [node.id, node]));
778
+ const adjacency = buildAdjacency(document);
779
+ const fullDegree = /* @__PURE__ */ new Map();
780
+ for (const link of document.links) {
781
+ fullDegree.set(link.source, (fullDegree.get(link.source) ?? 0) + 1);
782
+ fullDegree.set(link.target, (fullDegree.get(link.target) ?? 0) + 1);
783
+ }
784
+ const search = parseGraphQuery(settings.search);
785
+ const groups2 = settings.groups.map((group) => ({
786
+ group,
787
+ query: parseGraphQuery(group.query)
788
+ }));
789
+ const unsupportedOperators = new Set(search.unsupportedOperators);
790
+ for (const entry of groups2) {
791
+ for (const operator of entry.query.unsupportedOperators) {
792
+ unsupportedOperators.add(operator);
793
+ }
794
+ }
795
+ const candidates = mode === "local" ? collectLocalNodeIds(adjacency, centerId, settings.depth) : new Set(document.nodes.map((node) => node.id));
796
+ const visibleNodes = [];
797
+ for (const id of candidates) {
798
+ const node = nodeById.get(id);
799
+ if (!node) continue;
800
+ if (node.kind === "tag" && !settings.showTags) continue;
801
+ if (node.kind === "attachment" && !settings.showAttachments) continue;
802
+ if (node.kind === "unresolved" && settings.existingOnly) continue;
803
+ if (node.kind === "note" && !settings.showOrphans && (fullDegree.get(node.id) ?? 0) === 0) {
804
+ continue;
805
+ }
806
+ if (settings.search.trim().length > 0 && !matchesGraphQuery(node, search)) {
807
+ if (mode !== "local" || id !== centerId) continue;
808
+ }
809
+ visibleNodes.push({
810
+ ...node,
811
+ degree: 0,
812
+ radius: 0,
813
+ color: "",
814
+ groupId: null
815
+ });
816
+ }
817
+ const visibleIds = new Set(visibleNodes.map((node) => node.id));
818
+ const links = document.links.filter(
819
+ (link) => visibleIds.has(link.source) && visibleIds.has(link.target)
820
+ );
821
+ const degree = /* @__PURE__ */ new Map();
822
+ for (const link of links) {
823
+ degree.set(link.source, (degree.get(link.source) ?? 0) + 1);
824
+ degree.set(link.target, (degree.get(link.target) ?? 0) + 1);
825
+ }
826
+ visibleNodes.sort((left, right) => left.id.localeCompare(right.id));
827
+ for (const node of visibleNodes) {
828
+ node.degree = degree.get(node.id) ?? 0;
829
+ node.radius = 3 + Math.sqrt(node.degree) * 1.7;
830
+ const group = findGroup(node, groups2);
831
+ node.groupId = group?.group.id ?? null;
832
+ node.color = group?.group.color ?? "";
833
+ }
834
+ return {
835
+ nodes: visibleNodes,
836
+ links,
837
+ centerId: centerId && visibleIds.has(centerId) ? centerId : null,
838
+ unsupportedOperators: [...unsupportedOperators]
839
+ };
840
+ }
841
+ function collectLocalNodeIds(adjacency, centerId, depth) {
842
+ const ids = /* @__PURE__ */ new Set();
843
+ if (!centerId) return ids;
844
+ ids.add(centerId);
845
+ let frontier = [centerId];
846
+ for (let level = 0; level < depth; level += 1) {
847
+ const next = [];
848
+ for (const id of frontier) {
849
+ for (const neighbor of adjacency.get(id) ?? []) {
850
+ if (ids.has(neighbor)) continue;
851
+ ids.add(neighbor);
852
+ next.push(neighbor);
853
+ }
854
+ }
855
+ if (next.length === 0) break;
856
+ frontier = next;
857
+ }
858
+ return ids;
859
+ }
860
+ function buildAdjacency(document) {
861
+ const adjacency = /* @__PURE__ */ new Map();
862
+ for (const node of document.nodes) adjacency.set(node.id, []);
863
+ for (const link of document.links) {
864
+ adjacency.get(link.source)?.push(link.target);
865
+ adjacency.get(link.target)?.push(link.source);
866
+ }
867
+ return adjacency;
868
+ }
869
+ function findGroup(node, groups2) {
870
+ for (const entry of groups2) {
871
+ if (entry.group.query.trim().length === 0) continue;
872
+ if (entry.query.unsupportedOperators.length > 0) continue;
873
+ if (matchesGraphQuery(node, entry.query)) return entry;
874
+ }
875
+ return null;
876
+ }
877
+ export {
878
+ DEFAULT_GRAPH_SETTINGS,
879
+ GRAPH_SETTINGS_RANGES,
880
+ buildGraphDocument,
881
+ buildRevealSchedule,
882
+ buildVisibleGraph,
883
+ collectLocalNodeIds,
884
+ createGraphSimulation,
885
+ hasDatedNodes,
886
+ matchesGraphQuery,
887
+ mergeGraphSettings,
888
+ normalizeGraphSettings,
889
+ normalizeGraphText,
890
+ parseGraphQuery,
891
+ readStoredGraphSettings,
892
+ sliceGraphDocument,
893
+ writeStoredGraphSettings
894
+ };