@andespindola/brainlink 0.1.0-beta.84 → 0.1.0-beta.85
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/README.md +2 -1
- package/dist/application/frontend/client-js.js +147 -76
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
84
84
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
85
85
|
- Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
|
|
86
86
|
- WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
|
|
87
|
+
- Graph zoom-out renders connected ecosystem clusters of up to 1000 notes before expanding into individual notes, keeping vaults visually oriented with a smaller render scope.
|
|
87
88
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
88
89
|
- Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
|
|
89
90
|
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
|
|
@@ -601,7 +602,7 @@ The graph UI shows:
|
|
|
601
602
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
602
603
|
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
603
604
|
- graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
|
|
604
|
-
-
|
|
605
|
+
- graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes with aggregated real links, then progressively raises the focused node budget as zoom increases so dense local areas keep nearby notes and links visible
|
|
605
606
|
|
|
606
607
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
607
608
|
|
|
@@ -21,14 +21,15 @@ const viewportPaddingPx = 280
|
|
|
21
21
|
const worldCoordinateLimit = 5_000_000
|
|
22
22
|
const transformCoordinateLimit = 20_000_000
|
|
23
23
|
const hoverHitTestIntervalMs = 64
|
|
24
|
-
const
|
|
24
|
+
const ecosystemGroupSize = 1000
|
|
25
|
+
const ecosystemClusterEdgeLimit = 520
|
|
26
|
+
const ecosystemClusterScaleThreshold = 0.08
|
|
25
27
|
const zoomRecoveryGuardMs = 4200
|
|
26
28
|
const zoomCapTargetViewportShare = 0.72
|
|
27
29
|
const meshEdgeScaleThreshold = 0.09
|
|
28
30
|
const meshEdgeMinBudget = 140
|
|
29
31
|
const meshEdgeMaxBudget = 1400
|
|
30
32
|
const layeredCoreScaleThreshold = 0.55
|
|
31
|
-
const massiveOverviewClusterScaleThreshold = 0.035
|
|
32
33
|
const dragNeighborhoodMaxAffected = 180
|
|
33
34
|
const dragSettleRounds = 3
|
|
34
35
|
const wheelZoomExponent = 0.0018
|
|
@@ -44,6 +45,7 @@ const state = {
|
|
|
44
45
|
renderNodes: [],
|
|
45
46
|
renderEdges: [],
|
|
46
47
|
renderClusters: [],
|
|
48
|
+
renderClusterEdges: [],
|
|
47
49
|
nodeDegrees: new Map(),
|
|
48
50
|
selected: null,
|
|
49
51
|
hovered: null,
|
|
@@ -65,7 +67,8 @@ const state = {
|
|
|
65
67
|
lastViewportKey: '',
|
|
66
68
|
visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
|
|
67
69
|
visibleEdgeByNode: new Map(),
|
|
68
|
-
|
|
70
|
+
ecosystemClusters: [],
|
|
71
|
+
ecosystemEdges: [],
|
|
69
72
|
macroCenter: { x: 0, y: 0 },
|
|
70
73
|
macroRepresentative: null,
|
|
71
74
|
primaryHub: null,
|
|
@@ -562,7 +565,11 @@ const recomputeVisibility = () => {
|
|
|
562
565
|
state.visibleEdges = limitedEdges
|
|
563
566
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
564
567
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
565
|
-
|
|
568
|
+
const ecosystemGraph = nodes.length > 1
|
|
569
|
+
? buildEcosystemGraph(nodes, limitedEdges)
|
|
570
|
+
: { clusters: [], edges: [] }
|
|
571
|
+
state.ecosystemClusters = ecosystemGraph.clusters
|
|
572
|
+
state.ecosystemEdges = ecosystemGraph.edges
|
|
566
573
|
const primaryHub = rankedHubNodes()[0] ?? null
|
|
567
574
|
state.primaryHub = primaryHub
|
|
568
575
|
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
@@ -679,68 +686,131 @@ const createVisibleEdgeLookup = edges => {
|
|
|
679
686
|
return lookup
|
|
680
687
|
}
|
|
681
688
|
|
|
682
|
-
const
|
|
683
|
-
if (
|
|
684
|
-
return
|
|
689
|
+
const ecosystemKeyForNode = node => {
|
|
690
|
+
if (typeof node.segment === 'string' && node.segment.trim()) {
|
|
691
|
+
return node.segment.trim()
|
|
685
692
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (!bounds) {
|
|
689
|
-
return []
|
|
693
|
+
if (typeof node.group === 'string' && node.group.trim()) {
|
|
694
|
+
return node.group.trim()
|
|
690
695
|
}
|
|
696
|
+
const pathParts = String(node.path || '')
|
|
697
|
+
.split('/')
|
|
698
|
+
.filter(part => part.trim())
|
|
699
|
+
.slice(0, 2)
|
|
700
|
+
return pathParts.length > 0 ? pathParts.join('/') : 'root'
|
|
701
|
+
}
|
|
691
702
|
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
703
|
+
const compareNodesForEcosystem = (left, right) => {
|
|
704
|
+
const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
|
|
705
|
+
if (keyComparison !== 0) return keyComparison
|
|
706
|
+
const leftDegree = state.nodeDegrees.get(left.id) ?? 0
|
|
707
|
+
const rightDegree = state.nodeDegrees.get(right.id) ?? 0
|
|
708
|
+
if (leftDegree !== rightDegree) return rightDegree - leftDegree
|
|
709
|
+
return String(left.title || left.id).localeCompare(String(right.title || right.id))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const selectEcosystemRepresentative = nodes => {
|
|
713
|
+
let representative = nodes[0] ?? null
|
|
714
|
+
let representativeScore = Number.NEGATIVE_INFINITY
|
|
695
715
|
|
|
696
716
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
697
717
|
const node = nodes[index]
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
718
|
+
const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
|
|
719
|
+
if (score > representativeScore) {
|
|
720
|
+
representative = node
|
|
721
|
+
representativeScore = score
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return representative
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const buildEcosystemCluster = (nodes, index) => {
|
|
729
|
+
const count = Math.max(nodes.length, 1)
|
|
730
|
+
const sum = nodes.reduce((accumulator, node) => ({
|
|
731
|
+
x: accumulator.x + node.x,
|
|
732
|
+
y: accumulator.y + node.y
|
|
733
|
+
}), { x: 0, y: 0 })
|
|
734
|
+
const representative = selectEcosystemRepresentative(nodes)
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
id: 'ecosystem-' + index,
|
|
738
|
+
x: sum.x / count,
|
|
739
|
+
y: sum.y / count,
|
|
740
|
+
count,
|
|
741
|
+
representative,
|
|
742
|
+
label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const buildEcosystemGraph = (nodes, edges) => {
|
|
747
|
+
if (nodes.length === 0) {
|
|
748
|
+
return { clusters: [], edges: [] }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
|
|
752
|
+
const clusters = []
|
|
753
|
+
const clusterByNodeId = new Map()
|
|
754
|
+
|
|
755
|
+
for (let offset = 0; offset < sortedNodes.length; offset += ecosystemGroupSize) {
|
|
756
|
+
const clusterNodes = sortedNodes.slice(offset, offset + ecosystemGroupSize)
|
|
757
|
+
const cluster = buildEcosystemCluster(clusterNodes, clusters.length)
|
|
758
|
+
clusters.push(cluster)
|
|
759
|
+
for (let index = 0; index < clusterNodes.length; index += 1) {
|
|
760
|
+
clusterByNodeId.set(clusterNodes[index].id, cluster)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const edgeByClusterPair = new Map()
|
|
765
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
766
|
+
const edge = edges[index]
|
|
767
|
+
const sourceCluster = clusterByNodeId.get(edge.source)
|
|
768
|
+
const targetCluster = clusterByNodeId.get(edge.target)
|
|
769
|
+
if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
|
|
770
|
+
continue
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const orderedIds = sourceCluster.id < targetCluster.id
|
|
774
|
+
? [sourceCluster.id, targetCluster.id]
|
|
775
|
+
: [targetCluster.id, sourceCluster.id]
|
|
776
|
+
const key = orderedIds.join(':')
|
|
777
|
+
const current = edgeByClusterPair.get(key)
|
|
703
778
|
if (current) {
|
|
704
|
-
current.
|
|
705
|
-
current.sumX += node.x
|
|
706
|
-
current.sumY += node.y
|
|
707
|
-
if (degree > current.degree) {
|
|
708
|
-
current.representative = node
|
|
709
|
-
current.degree = degree
|
|
710
|
-
}
|
|
779
|
+
current.weight += edgeWeight(edge)
|
|
711
780
|
continue
|
|
712
781
|
}
|
|
713
782
|
|
|
714
|
-
|
|
783
|
+
edgeByClusterPair.set(key, {
|
|
715
784
|
id: key,
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
representative: node,
|
|
720
|
-
degree
|
|
785
|
+
sourceCluster,
|
|
786
|
+
targetCluster,
|
|
787
|
+
weight: edgeWeight(edge)
|
|
721
788
|
})
|
|
722
789
|
}
|
|
723
790
|
|
|
724
|
-
|
|
725
|
-
.sort((left, right) => right.
|
|
726
|
-
.slice(0,
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
x: cluster.sumX / Math.max(cluster.count, 1),
|
|
730
|
-
y: cluster.sumY / Math.max(cluster.count, 1),
|
|
731
|
-
count: cluster.count,
|
|
732
|
-
representative: cluster.representative
|
|
733
|
-
}))
|
|
791
|
+
const aggregatedEdges = Array.from(edgeByClusterPair.values())
|
|
792
|
+
.sort((left, right) => right.weight - left.weight)
|
|
793
|
+
.slice(0, ecosystemClusterEdgeLimit)
|
|
794
|
+
|
|
795
|
+
return { clusters, edges: aggregatedEdges }
|
|
734
796
|
}
|
|
735
797
|
|
|
736
|
-
const
|
|
737
|
-
state.
|
|
798
|
+
const filterEcosystemClustersByViewport = viewport =>
|
|
799
|
+
state.ecosystemClusters.filter((cluster) =>
|
|
738
800
|
cluster.x >= viewport.minX &&
|
|
739
801
|
cluster.x <= viewport.maxX &&
|
|
740
802
|
cluster.y >= viewport.minY &&
|
|
741
803
|
cluster.y <= viewport.maxY
|
|
742
804
|
)
|
|
743
805
|
|
|
806
|
+
const ecosystemEdgesForClusters = clusters => {
|
|
807
|
+
const clusterIds = new Set(clusters.map(cluster => cluster.id))
|
|
808
|
+
return state.ecosystemEdges.filter(edge =>
|
|
809
|
+
clusterIds.has(edge.sourceCluster.id) &&
|
|
810
|
+
clusterIds.has(edge.targetCluster.id)
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
744
814
|
const edgeBudgetForCurrentFrame = () => {
|
|
745
815
|
const zoom = state.transform.scale
|
|
746
816
|
if (zoom < 0.12) return 380
|
|
@@ -775,14 +845,6 @@ const nodeBudgetForScale = (scale) => {
|
|
|
775
845
|
return renderNodeBudget
|
|
776
846
|
}
|
|
777
847
|
|
|
778
|
-
const massiveLowZoomNodeBudgetForScale = (scale) => {
|
|
779
|
-
if (scale < 0.004) return 780
|
|
780
|
-
if (scale < 0.01) return 860
|
|
781
|
-
if (scale < 0.02) return 900
|
|
782
|
-
if (scale < 0.035) return 900
|
|
783
|
-
return renderNodeBudget
|
|
784
|
-
}
|
|
785
|
-
|
|
786
848
|
const layerFocusForScale = (scale) => {
|
|
787
849
|
const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
|
|
788
850
|
const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
|
|
@@ -2316,6 +2378,7 @@ const computeRenderVisibility = () => {
|
|
|
2316
2378
|
}
|
|
2317
2379
|
state.lastViewportKey = viewportKey
|
|
2318
2380
|
state.renderVisibilityDirty = false
|
|
2381
|
+
state.renderClusterEdges = []
|
|
2319
2382
|
|
|
2320
2383
|
const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
|
|
2321
2384
|
|
|
@@ -2339,12 +2402,27 @@ const computeRenderVisibility = () => {
|
|
|
2339
2402
|
state.renderNodes = []
|
|
2340
2403
|
}
|
|
2341
2404
|
state.renderEdges = []
|
|
2405
|
+
state.renderClusterEdges = []
|
|
2406
|
+
return
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
|
|
2410
|
+
const viewportClusters = filterEcosystemClustersByViewport(viewport)
|
|
2411
|
+
.sort((left, right) => right.count - left.count)
|
|
2412
|
+
const clusters = viewportClusters.length > 0
|
|
2413
|
+
? viewportClusters
|
|
2414
|
+
: state.ecosystemClusters
|
|
2415
|
+
state.renderClusters = clusters
|
|
2416
|
+
state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
|
|
2417
|
+
state.renderNodes = []
|
|
2418
|
+
state.renderEdges = []
|
|
2342
2419
|
return
|
|
2343
2420
|
}
|
|
2344
2421
|
|
|
2345
2422
|
if (state.visibleNodes.length <= 2000) {
|
|
2346
2423
|
state.renderNodes = state.visibleNodes
|
|
2347
2424
|
state.renderClusters = []
|
|
2425
|
+
state.renderClusterEdges = []
|
|
2348
2426
|
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
2349
2427
|
state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
|
|
2350
2428
|
return
|
|
@@ -2352,30 +2430,6 @@ const computeRenderVisibility = () => {
|
|
|
2352
2430
|
|
|
2353
2431
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
2354
2432
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2355
|
-
if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
|
|
2356
|
-
const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
|
|
2357
|
-
const overviewClusters = filterOverviewClustersByViewport(viewport)
|
|
2358
|
-
.sort((left, right) => right.count - left.count)
|
|
2359
|
-
.slice(0, overviewLimit)
|
|
2360
|
-
if (overviewClusters.length > 0) {
|
|
2361
|
-
const overviewNodes = representativeNodesFromClusters(
|
|
2362
|
-
overviewClusters,
|
|
2363
|
-
overviewLimit
|
|
2364
|
-
)
|
|
2365
|
-
const anchoredNodes = includeHubPreviewNeighborhood(
|
|
2366
|
-
overviewNodes,
|
|
2367
|
-
Math.min(renderNodeBudget, overviewLimit)
|
|
2368
|
-
)
|
|
2369
|
-
const enriched = enrichSampleWithNeighbors(anchoredNodes)
|
|
2370
|
-
const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
|
|
2371
|
-
const previewIds = new Set(previewNodes.map((node) => node.id))
|
|
2372
|
-
const previewEdges = collectVisibleEdgesForNodes(previewIds)
|
|
2373
|
-
state.renderClusters = []
|
|
2374
|
-
state.renderNodes = previewNodes
|
|
2375
|
-
state.renderEdges = previewEdges
|
|
2376
|
-
return
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
2433
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
2380
2434
|
const sampleLimit = nodeBudgetForScale(state.transform.scale)
|
|
2381
2435
|
const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
|
|
@@ -2421,6 +2475,7 @@ const computeRenderVisibility = () => {
|
|
|
2421
2475
|
}
|
|
2422
2476
|
|
|
2423
2477
|
state.renderClusters = []
|
|
2478
|
+
state.renderClusterEdges = []
|
|
2424
2479
|
state.renderNodes = sampledNodes
|
|
2425
2480
|
state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
|
|
2426
2481
|
return
|
|
@@ -2430,6 +2485,7 @@ const computeRenderVisibility = () => {
|
|
|
2430
2485
|
const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
|
|
2431
2486
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
2432
2487
|
state.renderClusters = []
|
|
2488
|
+
state.renderClusterEdges = []
|
|
2433
2489
|
state.renderNodes = sampled
|
|
2434
2490
|
state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
|
|
2435
2491
|
return
|
|
@@ -2439,11 +2495,13 @@ const computeRenderVisibility = () => {
|
|
|
2439
2495
|
const clusters = clusterViewportNodes(viewportNodes)
|
|
2440
2496
|
if (clusters.length > 0) {
|
|
2441
2497
|
state.renderClusters = []
|
|
2498
|
+
state.renderClusterEdges = []
|
|
2442
2499
|
state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
|
|
2443
2500
|
state.renderEdges = []
|
|
2444
2501
|
return
|
|
2445
2502
|
}
|
|
2446
2503
|
state.renderClusters = []
|
|
2504
|
+
state.renderClusterEdges = []
|
|
2447
2505
|
const stride = viewportNodeStride()
|
|
2448
2506
|
const picked = []
|
|
2449
2507
|
|
|
@@ -2467,6 +2525,7 @@ const computeRenderVisibility = () => {
|
|
|
2467
2525
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2468
2526
|
state.renderNodes = fallbackNodes
|
|
2469
2527
|
state.renderClusters = []
|
|
2528
|
+
state.renderClusterEdges = []
|
|
2470
2529
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2471
2530
|
return
|
|
2472
2531
|
}
|
|
@@ -2482,6 +2541,7 @@ const computeRenderVisibility = () => {
|
|
|
2482
2541
|
const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
|
|
2483
2542
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2484
2543
|
state.renderClusters = []
|
|
2544
|
+
state.renderClusterEdges = []
|
|
2485
2545
|
state.renderNodes = fallbackNodes
|
|
2486
2546
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2487
2547
|
}
|
|
@@ -2598,6 +2658,17 @@ const render = now => {
|
|
|
2598
2658
|
ctx.translate(state.transform.x, state.transform.y)
|
|
2599
2659
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2600
2660
|
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
2661
|
+
if (state.renderClusterEdges.length > 0) {
|
|
2662
|
+
ctx.beginPath()
|
|
2663
|
+
for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
|
|
2664
|
+
const edge = state.renderClusterEdges[index]
|
|
2665
|
+
ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
|
|
2666
|
+
ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
|
|
2667
|
+
}
|
|
2668
|
+
ctx.lineWidth = 1.2 / safeScale
|
|
2669
|
+
ctx.strokeStyle = 'rgba(153, 165, 181, 0.22)'
|
|
2670
|
+
ctx.stroke()
|
|
2671
|
+
}
|
|
2601
2672
|
state.renderClusters.forEach(cluster => {
|
|
2602
2673
|
const isMacro = cluster.id === 'macro-galaxy'
|
|
2603
2674
|
const radiusPx = isMacro
|
package/package.json
CHANGED