@andespindola/brainlink 0.1.0-beta.115 → 0.1.0-beta.116

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 CHANGED
@@ -84,11 +84,11 @@ 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 keeps the same Bloom-like scene model for every graph size: nodes remain part of one flat exploration scene while rendering budgets decide how much visible detail is drawn.
87
+ - Graph rendering uses a rebuilt Bloom-like flat scene: no macro galaxy, no recursive subgraphs, no cluster replacement layer and no synthetic 3D projection.
88
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.
89
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).
90
90
  - Graph coordinates are visually compacted across graph sizes so reset starts from a stable fitted scene and zoom-in progressively reveals local detail.
91
- - Zoomed-out graph LOD samples visible nodes and priority links without switching to nested subgraphs.
91
+ - Zoomed-out graph LOD samples visible nodes and priority links without switching to nested subgraphs or cluster markers.
92
92
  - Graph reset fits the full graph scene instead of starting in a separate macro overview mode.
93
93
  - Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
94
94
  - Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
@@ -604,7 +604,7 @@ The graph UI shows:
604
604
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
605
605
  - adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
606
606
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
607
- - large graph LOD keeps the same scene model as smaller graphs: visible nodes are sampled near the viewport/focus, real links are prioritized, and no recursive graph-of-graphs or synthetic 3D projection is applied
607
+ - large graph LOD keeps the same scene model as smaller graphs: visible nodes are sampled near the viewport/focus, real links are prioritized, and no macro galaxy, recursive graph-of-graphs, cluster replacement layer or synthetic 3D projection is applied
608
608
 
609
609
  The server indexes before starting by default. Use `--no-index` to skip that step:
610
610
 
@@ -7,25 +7,13 @@ const largeGraphEdgeRenderLimit = 120000
7
7
  const renderNodeBudget = 900
8
8
  const zoomedMassiveRenderNodeBudget = 2200
9
9
  const renderEdgeBudget = 2400
10
- const clusterActivationNodeThreshold = 600
11
- const clusterZoomThreshold = 0.18
12
- const macroGalaxyZoomThreshold = 0.012
13
- const macroGalaxyEnterHysteresis = 0.86
14
- const macroGalaxyExitHysteresis = 1.24
15
- const galaxyDiscoveryEnabled = false
16
10
  const massiveAutoFitMacroScale = 0.018
17
- const defaultMacroScale = 0.018
18
- const clusterCellPixelSize = 64
19
11
  const minNodePixelRadius = 2.3
20
12
  const viewportPaddingPx = 280
21
13
  const worldCoordinateLimit = 5_000_000
22
14
  const transformCoordinateLimit = 20_000_000
23
15
  const hoverHitTestIntervalMs = 64
24
- const ecosystemLevelNodeCap = 999
25
- const ecosystemActivationNodeThreshold = 1000
26
- const ecosystemSubgraphScaleThreshold = 0.18
27
16
  const zoomRecoveryGuardMs = 4200
28
- const zoomCapTargetViewportShare = 0.72
29
17
  const meshEdgeScaleThreshold = 0.09
30
18
  const meshEdgeMinBudget = 140
31
19
  const meshEdgeMaxBudget = 1400
@@ -54,8 +42,6 @@ const state = {
54
42
  visibleEdges: [],
55
43
  renderNodes: [],
56
44
  renderEdges: [],
57
- renderClusters: [],
58
- renderClusterEdges: [],
59
45
  renderNodeDepthProjectionById: new Map(),
60
46
  nodeDegrees: new Map(),
61
47
  selected: null,
@@ -81,26 +67,12 @@ const state = {
81
67
  lastViewportKey: '',
82
68
  visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
83
69
  visibleEdgeByNode: new Map(),
84
- ecosystemClusters: [],
85
- ecosystemClustersBySize: new Map(),
86
- ecosystemNodeClusterBySize: new Map(),
87
- ecosystemLevelSizes: [],
88
- ecosystemLevelIndexBySize: new Map(),
89
- ecosystemHubNodeIds: new Set(),
90
- ecosystemExpansionLevels: [],
91
- ecosystemBaseSize: ecosystemLevelNodeCap,
92
- ecosystemHubCluster: null,
93
- macroCenter: { x: 0, y: 0 },
94
- macroRepresentative: null,
95
70
  primaryHub: null,
96
- hubNeighborDistance: Number.POSITIVE_INFINITY,
97
71
  filterWorker: null,
98
72
  filterReady: false,
99
73
  lastHoverHitAt: 0,
100
74
  lastManualZoomAt: 0,
101
75
  lastZoomFocus: { x: 0, y: 0, at: 0 },
102
- macroViewActive: false,
103
- ecosystemViewActive: false,
104
76
  depthProjectionActive: false,
105
77
  zoomTransition: {
106
78
  active: false,
@@ -532,47 +504,6 @@ const filteredNodes = () => {
532
504
  return withPersistentHubNodes(localFilteredNodes(query))
533
505
  }
534
506
 
535
- const resolveMacroRepresentative = (nodes) => {
536
- if (nodes.length === 0) {
537
- return null
538
- }
539
-
540
- const hubCandidate = state.primaryHub && nodes.some(node => node.id === state.primaryHub.id)
541
- ? state.primaryHub
542
- : null
543
- let best = hubCandidate ?? nodes[0]
544
- let bestDegree = state.nodeDegrees.get(best.id) ?? 0
545
-
546
- for (let index = 1; index < nodes.length; index += 1) {
547
- const node = nodes[index]
548
- const degree = state.nodeDegrees.get(node.id) ?? 0
549
- if (degree > bestDegree) {
550
- best = node
551
- bestDegree = degree
552
- }
553
- }
554
-
555
- return best
556
- }
557
-
558
- const nearestHubNeighborDistance = (hub, nodes) => {
559
- if (!hub || nodes.length <= 1) {
560
- return Number.POSITIVE_INFINITY
561
- }
562
-
563
- let minimum = Number.POSITIVE_INFINITY
564
- for (let index = 0; index < nodes.length; index += 1) {
565
- const node = nodes[index]
566
- if (node.id === hub.id) continue
567
- const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
568
- if (distance < minimum) {
569
- minimum = distance
570
- }
571
- }
572
-
573
- return minimum
574
- }
575
-
576
507
  const isDominantHub = (hub, nodeCount = state.visibleNodes.length) => {
577
508
  if (!hub || nodeCount <= 0) {
578
509
  return false
@@ -600,25 +531,6 @@ const recomputeVisibility = () => {
600
531
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
601
532
  const primaryHub = rankedHubNodes()[0] ?? null
602
533
  state.primaryHub = primaryHub
603
- state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
604
- const bounds = graphBounds(nodes)
605
- const macroHub = isDominantHub(primaryHub, nodes.length) ? primaryHub : null
606
- state.macroCenter = bounds
607
- ? {
608
- x: macroHub ? macroHub.x : (bounds.minX + bounds.maxX) / 2,
609
- y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
610
- }
611
- : { x: 0, y: 0 }
612
- state.ecosystemClusters = []
613
- state.ecosystemClustersBySize = new Map()
614
- state.ecosystemNodeClusterBySize = new Map()
615
- state.ecosystemLevelSizes = []
616
- state.ecosystemLevelIndexBySize = new Map()
617
- state.ecosystemHubNodeIds = new Set()
618
- state.ecosystemExpansionLevels = []
619
- state.ecosystemBaseSize = ecosystemLevelNodeCap
620
- state.ecosystemHubCluster = null
621
- state.macroRepresentative = resolveMacroRepresentative(nodes)
622
534
  markRenderDirty()
623
535
  }
624
536
 
@@ -729,18 +641,6 @@ const isClusterInViewport = (cluster, viewport) =>
729
641
  cluster.y >= viewport.minY &&
730
642
  cluster.y <= viewport.maxY
731
643
 
732
- const ecosystemFocusPoint = () => {
733
- const cursorPoint = cursorWorldPoint()
734
- if (cursorPoint) {
735
- return cursorPoint
736
- }
737
- const now = performance.now()
738
- if (now - state.lastZoomFocus.at <= 1800) {
739
- return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
740
- }
741
- return viewportCenterWorldPoint()
742
- }
743
-
744
644
  const edgeBudgetForCurrentFrame = () => {
745
645
  const zoom = state.transform.scale
746
646
  if (zoom < 0.12) return 380
@@ -896,30 +796,6 @@ const visibilityScaleBucket = (scale) => {
896
796
  return Math.round(safeScale * 180_000)
897
797
  }
898
798
 
899
- const shouldRenderEcosystemClusterView = (nodeCount, scale) => {
900
- state.ecosystemViewActive = false
901
- return false
902
- }
903
-
904
- const shouldRenderMacroGalaxyView = () => {
905
- if (!galaxyDiscoveryEnabled) {
906
- state.macroViewActive = false
907
- return false
908
- }
909
- if (state.visibleNodes.length <= 1) {
910
- state.macroViewActive = false
911
- return false
912
- }
913
-
914
- const enterThreshold = macroGalaxyZoomThreshold * macroGalaxyEnterHysteresis
915
- const exitThreshold = macroGalaxyZoomThreshold * macroGalaxyExitHysteresis
916
- const shouldRender = state.macroViewActive
917
- ? state.transform.scale <= exitThreshold
918
- : state.transform.scale <= enterThreshold
919
- state.macroViewActive = shouldRender
920
- return shouldRender
921
- }
922
-
923
799
  const mergeUniqueNodes = (leftNodes, rightNodes, limit) => {
924
800
  const merged = []
925
801
  const ids = new Set()
@@ -1358,7 +1234,7 @@ const drawGraphLabels = nodes => {
1358
1234
  }
1359
1235
 
1360
1236
  const drawAcceleratedGraph = (width, height, drawEdges) => {
1361
- if (!webGlRenderer || state.renderClusters.length > 0) {
1237
+ if (!webGlRenderer) {
1362
1238
  return false
1363
1239
  }
1364
1240
 
@@ -1728,18 +1604,6 @@ const zoomCapByNodeCount = (nodeCount) => {
1728
1604
  return zoomRange.max
1729
1605
  }
1730
1606
 
1731
- const zoomCapByHubDistance = (distance) => {
1732
- if (!Number.isFinite(distance) || distance <= 0) {
1733
- return zoomRange.max
1734
- }
1735
-
1736
- const rect = canvas.getBoundingClientRect()
1737
- const viewportWidth = Math.max(rect.width, 320)
1738
- const viewportHeight = Math.max(rect.height, 320)
1739
- const reference = Math.max(220, Math.min(viewportWidth, viewportHeight) * zoomCapTargetViewportShare)
1740
- return Math.max(0.3, Math.min(zoomRange.max, reference / distance))
1741
- }
1742
-
1743
1607
  const currentZoomMax = () => {
1744
1608
  const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
1745
1609
  return Math.max(zoomRange.min * 2, zoomCapByNodeCount(nodeCount))
@@ -1748,7 +1612,7 @@ const currentZoomMax = () => {
1748
1612
  const zoomFloorByNodeCount = (nodeCount) => {
1749
1613
  if (nodeCount > massiveGraphNodeThreshold) return 0.018
1750
1614
  if (nodeCount > largeGraphNodeThreshold) return 0.0032
1751
- if (nodeCount > ecosystemActivationNodeThreshold) return 0.001
1615
+ if (nodeCount > 1000) return 0.001
1752
1616
  return zoomRange.min
1753
1617
  }
1754
1618
 
@@ -1860,56 +1724,6 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
1860
1724
  return { min: 0.0085, max: 0.36 }
1861
1725
  }
1862
1726
 
1863
- const macroFaceToFaceScale = (nodeCount, hubDistance) => {
1864
- if (!Number.isFinite(hubDistance) || hubDistance <= 0 || nodeCount <= ecosystemActivationNodeThreshold) {
1865
- return 0
1866
- }
1867
-
1868
- const rect = canvas.getBoundingClientRect()
1869
- const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
1870
- const share = nodeCount > massiveGraphNodeThreshold ? 0.2 : 0.17
1871
- const targetPx = Math.max(24, viewportReference * share)
1872
- return targetPx / hubDistance
1873
- }
1874
-
1875
- const nearestClusterNeighborDistance = (clusters) => {
1876
- if (!Array.isArray(clusters) || clusters.length < 2) {
1877
- return Number.POSITIVE_INFINITY
1878
- }
1879
-
1880
- let nearestDistance = Number.POSITIVE_INFINITY
1881
- for (let index = 0; index < clusters.length; index += 1) {
1882
- const source = clusters[index]
1883
- for (let neighborIndex = index + 1; neighborIndex < clusters.length; neighborIndex += 1) {
1884
- const target = clusters[neighborIndex]
1885
- const distance = Math.hypot(source.x - target.x, source.y - target.y)
1886
- if (distance > 0 && distance < nearestDistance) {
1887
- nearestDistance = distance
1888
- }
1889
- }
1890
- }
1891
-
1892
- return nearestDistance
1893
- }
1894
-
1895
- const macroEcosystemFaceScale = (nodeCount) => {
1896
- if (nodeCount <= ecosystemActivationNodeThreshold) {
1897
- return 0
1898
- }
1899
-
1900
- const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
1901
- const siblingClusters = baseClusters.filter(cluster => !cluster.isHub)
1902
- const nearestDistance = nearestClusterNeighborDistance(siblingClusters)
1903
- if (!Number.isFinite(nearestDistance) || nearestDistance <= 0) {
1904
- return 0
1905
- }
1906
-
1907
- const rect = canvas.getBoundingClientRect()
1908
- const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
1909
- const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.28 : 0.24
1910
- const targetPx = Math.max(30, viewportReference * targetShare)
1911
- return targetPx / nearestDistance
1912
- }
1913
1727
  const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
1914
1728
  const rect = canvas.getBoundingClientRect()
1915
1729
  const width = Math.max(rect.width, 320)
@@ -1942,21 +1756,9 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
1942
1756
  const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
1943
1757
  const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
1944
1758
  const baselineScale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
1945
- const macroScale = nodes.length > massiveGraphNodeThreshold ? massiveAutoFitMacroScale : defaultMacroScale
1946
- const scale = options.macro && nodes.length > 1
1947
- ? clampScale(Math.min(baselineScale, macroScale))
1948
- : nodes.length > massiveGraphNodeThreshold
1949
- ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
1950
- : baselineScale
1951
- const macroFloorScale = options.macro
1952
- ? clampScale(Math.max(
1953
- macroFaceToFaceScale(nodes.length, state.hubNeighborDistance),
1954
- macroEcosystemFaceScale(nodes.length)
1955
- ))
1956
- : 0
1957
- const resolvedScale = options.macro
1958
- ? clampScale(Math.max(scale, macroFloorScale))
1959
- : scale
1759
+ const resolvedScale = nodes.length > massiveGraphNodeThreshold
1760
+ ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
1761
+ : baselineScale
1960
1762
  const hubCenter =
1961
1763
  options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
1962
1764
  ? state.primaryHub
@@ -2347,9 +2149,6 @@ const settleNeighborhoodAroundNode = (dragNode) => {
2347
2149
 
2348
2150
  const hitNode = point => {
2349
2151
  computeRenderVisibility()
2350
- if (state.renderClusters.length > 0) {
2351
- return null
2352
- }
2353
2152
  const hitScaleFloor = state.nodes.length > massiveGraphNodeThreshold
2354
2153
  ? 0.2
2355
2154
  : state.nodes.length > largeGraphNodeThreshold
@@ -2377,27 +2176,6 @@ const baseNodeRadius = node => {
2377
2176
 
2378
2177
  const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
2379
2178
 
2380
- const clusterRadiusPx = cluster => {
2381
- if (cluster.id === 'macro-galaxy') {
2382
- return 10
2383
- }
2384
- if (cluster.isHub) {
2385
- return 3.8
2386
- }
2387
- if (String(cluster.id).startsWith('ecosystem-')) {
2388
- const size = Math.max(1, Math.min(ecosystemLevelNodeCap, cluster.size || cluster.count || 1))
2389
- const sizeBias = 0.56 + Math.log10(size + 1) * 0.28
2390
- const densityBias = Math.log10((cluster.count || 1) + 1) * 0.12
2391
- const radius = Math.max(0.62, Math.min(2.4, sizeBias + densityBias))
2392
- const depthScale = Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
2393
- return Math.max(0.56, Math.min(3.2, radius * depthScale))
2394
- }
2395
- return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
2396
- }
2397
-
2398
- const clusterOpacity = cluster =>
2399
- Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
2400
-
2401
2179
  const refreshRenderNodeDepthProjection = () => {
2402
2180
  state.renderNodeDepthProjectionById = new Map()
2403
2181
  }
@@ -2460,68 +2238,6 @@ const viewportNodeStride = () => {
2460
2238
  return 8
2461
2239
  }
2462
2240
 
2463
- const shouldRenderClusters = viewportNodes =>
2464
- state.transform.scale <= clusterZoomThreshold && viewportNodes.length >= clusterActivationNodeThreshold
2465
-
2466
- const clusterViewportNodes = viewportNodes => {
2467
- if (!shouldRenderClusters(viewportNodes)) {
2468
- return []
2469
- }
2470
-
2471
- const worldCellSize = Math.max(clusterCellPixelSize / Math.max(state.transform.scale, 0.0001), 1)
2472
- const buckets = new Map()
2473
-
2474
- for (let index = 0; index < viewportNodes.length; index += 1) {
2475
- const node = viewportNodes[index]
2476
- const keyX = Math.floor(node.x / worldCellSize)
2477
- const keyY = Math.floor(node.y / worldCellSize)
2478
- const key = keyX + ':' + keyY
2479
- const current = buckets.get(key)
2480
- if (current) {
2481
- current.count += 1
2482
- current.sumX += node.x
2483
- current.sumY += node.y
2484
- if ((state.nodeDegrees.get(node.id) ?? 0) > current.degree) {
2485
- current.representative = node
2486
- current.degree = state.nodeDegrees.get(node.id) ?? 0
2487
- }
2488
- continue
2489
- }
2490
-
2491
- buckets.set(key, {
2492
- id: key,
2493
- count: 1,
2494
- sumX: node.x,
2495
- sumY: node.y,
2496
- representative: node,
2497
- degree: state.nodeDegrees.get(node.id) ?? 0
2498
- })
2499
- }
2500
-
2501
- return Array.from(buckets.values())
2502
- .sort((left, right) => right.count - left.count)
2503
- .slice(0, Math.min(renderNodeBudget, 900))
2504
- .map((cluster) => ({
2505
- id: cluster.id,
2506
- x: cluster.sumX / Math.max(cluster.count, 1),
2507
- y: cluster.sumY / Math.max(cluster.count, 1),
2508
- count: cluster.count,
2509
- representative: cluster.representative
2510
- }))
2511
- }
2512
-
2513
- const representativeNodesFromClusters = (clusters, limit) => {
2514
- const representatives = clusters
2515
- .map((cluster) => cluster.representative)
2516
- .filter((node) => Boolean(node))
2517
- const merged = mergeUniqueNodes(
2518
- representatives,
2519
- state.renderNodes ?? [],
2520
- Math.max(1, limit)
2521
- )
2522
- return ensureHubNodesInRenderedSet(merged)
2523
- }
2524
-
2525
2241
  const computeRenderVisibility = () => {
2526
2242
  if (!hasValidTransform()) {
2527
2243
  fitView({ useFiltered: true })
@@ -2539,40 +2255,9 @@ const computeRenderVisibility = () => {
2539
2255
  }
2540
2256
  state.lastViewportKey = viewportKey
2541
2257
  state.renderVisibilityDirty = false
2542
- state.renderClusterEdges = []
2543
-
2544
- const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
2545
-
2546
- if (shouldRenderMacroGalaxy) {
2547
- const viewportNodes = viewportNodesFromSpatialIndex(viewport)
2548
- const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
2549
- const representative = state.primaryHub ?? state.macroRepresentative ?? sourceNodes[0] ?? null
2550
- if (representative) {
2551
- state.renderClusters = [
2552
- {
2553
- id: 'macro-galaxy',
2554
- x: state.macroCenter.x,
2555
- y: state.macroCenter.y,
2556
- count: sourceNodes.length,
2557
- representative
2558
- }
2559
- ]
2560
- state.renderNodes = [representative]
2561
- } else {
2562
- state.renderClusters = []
2563
- state.renderNodes = []
2564
- }
2565
- state.renderEdges = []
2566
- state.renderClusterEdges = []
2567
- return
2568
- }
2569
-
2570
- state.ecosystemViewActive = false
2571
2258
 
2572
2259
  if (state.visibleNodes.length <= 2000) {
2573
2260
  state.renderNodes = state.visibleNodes
2574
- state.renderClusters = []
2575
- state.renderClusterEdges = []
2576
2261
  const ids = new Set(state.renderNodes.map((node) => node.id))
2577
2262
  state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
2578
2263
  return
@@ -2623,9 +2308,6 @@ const computeRenderVisibility = () => {
2623
2308
  const sampledWithHubsIds = new Set(sampledNodes.map((node) => node.id))
2624
2309
  sampledEdges = collectVisibleEdgesForNodes(sampledWithHubsIds)
2625
2310
  }
2626
-
2627
- state.renderClusters = []
2628
- state.renderClusterEdges = []
2629
2311
  state.renderNodes = sampledNodes
2630
2312
  state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
2631
2313
  return
@@ -2634,24 +2316,12 @@ const computeRenderVisibility = () => {
2634
2316
  if (state.transform.scale <= 0.0015) {
2635
2317
  const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
2636
2318
  const sampledIds = new Set(sampled.map((node) => node.id))
2637
- state.renderClusters = []
2638
- state.renderClusterEdges = []
2639
2319
  state.renderNodes = sampled
2640
2320
  state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
2641
2321
  return
2642
2322
  }
2643
2323
 
2644
2324
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
2645
- const clusters = clusterViewportNodes(viewportNodes)
2646
- if (clusters.length > 0) {
2647
- state.renderClusters = []
2648
- state.renderClusterEdges = []
2649
- state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
2650
- state.renderEdges = []
2651
- return
2652
- }
2653
- state.renderClusters = []
2654
- state.renderClusterEdges = []
2655
2325
  const stride = viewportNodeStride()
2656
2326
  const picked = []
2657
2327
 
@@ -2674,8 +2344,6 @@ const computeRenderVisibility = () => {
2674
2344
  const fallbackNodes = fallbackViewportNodes()
2675
2345
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
2676
2346
  state.renderNodes = fallbackNodes
2677
- state.renderClusters = []
2678
- state.renderClusterEdges = []
2679
2347
  state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
2680
2348
  return
2681
2349
  }
@@ -2690,8 +2358,6 @@ const computeRenderVisibility = () => {
2690
2358
  if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
2691
2359
  const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
2692
2360
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
2693
- state.renderClusters = []
2694
- state.renderClusterEdges = []
2695
2361
  state.renderNodes = fallbackNodes
2696
2362
  state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
2697
2363
  }
@@ -2802,69 +2468,9 @@ const render = now => {
2802
2468
  : state.renderNodes.length > 500
2803
2469
  ? 0.05
2804
2470
  : 0
2805
- const drawEdges =
2806
- state.renderClusters.length === 0 &&
2807
- state.transform.scale >= minimumEdgeScale
2471
+ const drawEdges = state.transform.scale >= minimumEdgeScale
2808
2472
  if (drawAcceleratedGraph(width, height, drawEdges)) {
2809
2473
  // WebGL handles the dense node/edge layer; the 2D canvas remains the interaction overlay.
2810
- } else if (state.renderClusters.length > 0) {
2811
- ctx.save()
2812
- ctx.translate(state.transform.x, state.transform.y)
2813
- ctx.scale(state.transform.scale, state.transform.scale)
2814
- const orderedClusters = [...state.renderClusters]
2815
- const safeScale = Math.max(state.transform.scale, 0.0001)
2816
- if (state.renderClusterEdges.length > 0) {
2817
- for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
2818
- const edge = state.renderClusterEdges[index]
2819
- const edgeOpacity = Math.min(clusterOpacity(edge.sourceCluster), clusterOpacity(edge.targetCluster))
2820
- if (edgeOpacity <= 0.01) {
2821
- continue
2822
- }
2823
- const widthScale = 1
2824
- ctx.beginPath()
2825
- ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
2826
- ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
2827
- ctx.lineWidth = (1.2 * widthScale) / safeScale
2828
- ctx.strokeStyle = 'rgba(153, 165, 181, ' + (edge.inferred ? 0.14 : 0.22) * edgeOpacity + ')'
2829
- ctx.stroke()
2830
- }
2831
- }
2832
- orderedClusters.forEach(cluster => {
2833
- const isMacro = cluster.id === 'macro-galaxy'
2834
- const isEcosystem = String(cluster.id).startsWith('ecosystem-')
2835
- const isHub = Boolean(cluster.isHub)
2836
- const opacity = clusterOpacity(cluster)
2837
- if (opacity <= 0.01) {
2838
- return
2839
- }
2840
- const radiusPx = clusterRadiusPx(cluster)
2841
- const radius = radiusPx / safeScale
2842
- const haloRadius = (radiusPx + (isMacro ? 8 : isHub ? 4 : isEcosystem ? 1.1 : 4)) / safeScale
2843
- ctx.globalAlpha = opacity
2844
- if (isHub || !isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
2845
- ctx.beginPath()
2846
- ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
2847
- ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
2848
- ctx.fill()
2849
- }
2850
- ctx.beginPath()
2851
- ctx.arc(cluster.x, cluster.y, radius, 0, Math.PI * 2)
2852
- ctx.fillStyle = isMacro ? '#f3f7fb' : graphTheme.node
2853
- ctx.fill()
2854
- ctx.lineWidth = (isEcosystem && !isHub ? 0.7 : 1.4) / safeScale
2855
- ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
2856
- ctx.stroke()
2857
- if (isMacro && cluster.representative?.title) {
2858
- ctx.fillStyle = '#edf2f7'
2859
- ctx.font = 12 / safeScale + 'px Inter, system-ui, sans-serif'
2860
- ctx.textAlign = 'center'
2861
- ctx.textBaseline = 'top'
2862
- ctx.fillText(cluster.representative.title.slice(0, 28), cluster.x, cluster.y + (radiusPx + 9) / safeScale)
2863
- }
2864
- ctx.globalAlpha = 1
2865
- // Keep cluster markers minimal and faster to draw on large graphs.
2866
- })
2867
- ctx.restore()
2868
2474
  } else {
2869
2475
  ctx.save()
2870
2476
  ctx.translate(state.transform.x, state.transform.y)
@@ -2875,7 +2481,7 @@ const render = now => {
2875
2481
  drawGraphNodes()
2876
2482
  ctx.restore()
2877
2483
  }
2878
- if (state.renderNodes.length === 0 && state.renderClusters.length === 0) {
2484
+ if (state.renderNodes.length === 0) {
2879
2485
  ctx.fillStyle = '#99a5b5'
2880
2486
  ctx.font = '12px Inter, system-ui, sans-serif'
2881
2487
  ctx.textAlign = 'center'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.115",
3
+ "version": "0.1.0-beta.116",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",