@andespindola/brainlink 0.1.0-beta.57 → 0.1.0-beta.59

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.
@@ -19,6 +19,9 @@ const hoverHitTestIntervalMs = 64
19
19
  const overviewClusterMaxCount = 1400
20
20
  const zoomRecoveryGuardMs = 1500
21
21
  const zoomCapTargetViewportShare = 0.72
22
+ const meshEdgeScaleThreshold = 0.09
23
+ const meshEdgeMinBudget = 140
24
+ const meshEdgeMaxBudget = 1400
22
25
  const state = {
23
26
  graph: { nodes: [], edges: [] },
24
27
  nodes: [],
@@ -595,6 +598,122 @@ const collectVisibleEdgesForNodes = nodeIds => {
595
598
  return collected
596
599
  }
597
600
 
601
+ const edgePairKey = (source, target) =>
602
+ source < target ? source + '|' + target : target + '|' + source
603
+
604
+ const meshNeighborBuckets = (nodes, cellSize) => {
605
+ const buckets = new Map()
606
+
607
+ for (let index = 0; index < nodes.length; index += 1) {
608
+ const node = nodes[index]
609
+ const cellX = Math.floor(node.x / cellSize)
610
+ const cellY = Math.floor(node.y / cellSize)
611
+ const key = cellX + ':' + cellY
612
+ const bucket = buckets.get(key)
613
+ if (bucket) {
614
+ bucket.push(node)
615
+ } else {
616
+ buckets.set(key, [node])
617
+ }
618
+ }
619
+
620
+ return buckets
621
+ }
622
+
623
+ const meshCandidatesForNode = (node, buckets, cellSize) => {
624
+ const cellX = Math.floor(node.x / cellSize)
625
+ const cellY = Math.floor(node.y / cellSize)
626
+ const candidates = []
627
+
628
+ for (let offsetX = -1; offsetX <= 1; offsetX += 1) {
629
+ for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
630
+ const bucket = buckets.get((cellX + offsetX) + ':' + (cellY + offsetY))
631
+ if (!bucket) continue
632
+ for (let index = 0; index < bucket.length; index += 1) {
633
+ const candidate = bucket[index]
634
+ if (candidate.id !== node.id) {
635
+ candidates.push(candidate)
636
+ }
637
+ }
638
+ }
639
+ }
640
+
641
+ return candidates
642
+ }
643
+
644
+ const buildMeshEdgesForNodes = (nodes, existingEdges) => {
645
+ if (nodes.length < 2 || state.transform.scale < meshEdgeScaleThreshold) {
646
+ return []
647
+ }
648
+
649
+ const existingKeys = new Set()
650
+ for (let index = 0; index < existingEdges.length; index += 1) {
651
+ const edge = existingEdges[index]
652
+ if (edge.target) {
653
+ existingKeys.add(edgePairKey(edge.source, edge.target))
654
+ }
655
+ }
656
+
657
+ const desiredBudget = Math.min(
658
+ meshEdgeMaxBudget,
659
+ Math.max(meshEdgeMinBudget, Math.floor(edgeBudgetForCurrentFrame() * 0.62))
660
+ )
661
+ const perNodeNeighborCount =
662
+ state.transform.scale >= 1.05 ? 4
663
+ : state.transform.scale >= 0.62 ? 3
664
+ : 2
665
+ const cellSize = Math.max(120, 280 / Math.max(state.transform.scale, 0.0001))
666
+ const maxDistance = 980
667
+ const maxDistanceSquared = maxDistance * maxDistance
668
+ const buckets = meshNeighborBuckets(nodes, cellSize)
669
+ const meshEdges = []
670
+ const meshKeys = new Set()
671
+
672
+ for (let index = 0; index < nodes.length && meshEdges.length < desiredBudget; index += 1) {
673
+ const node = nodes[index]
674
+ const candidates = meshCandidatesForNode(node, buckets, cellSize)
675
+ .map((candidate) => ({
676
+ node: candidate,
677
+ distanceSquared: (candidate.x - node.x) ** 2 + (candidate.y - node.y) ** 2
678
+ }))
679
+ .filter((candidate) => candidate.distanceSquared <= maxDistanceSquared)
680
+ .sort((left, right) => left.distanceSquared - right.distanceSquared)
681
+
682
+ let linked = 0
683
+ for (let candidateIndex = 0; candidateIndex < candidates.length && linked < perNodeNeighborCount && meshEdges.length < desiredBudget; candidateIndex += 1) {
684
+ const candidate = candidates[candidateIndex].node
685
+ const key = edgePairKey(node.id, candidate.id)
686
+ if (existingKeys.has(key) || meshKeys.has(key)) {
687
+ continue
688
+ }
689
+
690
+ meshKeys.add(key)
691
+ meshEdges.push({
692
+ source: node.id,
693
+ target: candidate.id,
694
+ targetTitle: candidate.title,
695
+ weight: 1,
696
+ priority: 'normal',
697
+ sourceNode: node,
698
+ targetNode: candidate,
699
+ inferred: true
700
+ })
701
+ linked += 1
702
+ }
703
+ }
704
+
705
+ return meshEdges
706
+ }
707
+
708
+ const withMeshEdges = (nodes, edges) => {
709
+ if (nodes.length === 0 || state.visibleNodes.length <= largeGraphNodeThreshold || state.transform.scale < meshEdgeScaleThreshold) {
710
+ return edges
711
+ }
712
+
713
+ const meshEdges = buildMeshEdgesForNodes(nodes, edges)
714
+ return meshEdges.length > 0 ? edges.concat(meshEdges) : edges
715
+ }
716
+
598
717
  const fallbackViewportNodes = () => {
599
718
  const nodes = []
600
719
  const maxNodes = Math.min(renderNodeBudget, 220)
@@ -710,9 +829,9 @@ const ensureHubNodesInRenderedSet = (nodes) => {
710
829
  }
711
830
 
712
831
  const zoomCapByNodeCount = (nodeCount) => {
713
- if (nodeCount > 50000) return 0.88
714
- if (nodeCount > 20000) return 1.15
715
- if (nodeCount > 6000) return 1.65
832
+ if (nodeCount > 50000) return 2.6
833
+ if (nodeCount > 20000) return 2.35
834
+ if (nodeCount > 6000) return 2.1
716
835
  if (nodeCount > 2000) return 2.2
717
836
  return zoomRange.max
718
837
  }
@@ -731,7 +850,9 @@ const zoomCapByHubDistance = (distance) => {
731
850
 
732
851
  const currentZoomMax = () => {
733
852
  const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
734
- const capped = Math.min(zoomCapByNodeCount(nodeCount), zoomCapByHubDistance(state.hubNeighborDistance))
853
+ const hubDistanceCap = zoomCapByHubDistance(state.hubNeighborDistance)
854
+ const minimumUsefulCap = nodeCount > massiveGraphNodeThreshold ? 1.9 : nodeCount > largeGraphNodeThreshold ? 1.35 : 0.8
855
+ const capped = Math.min(zoomCapByNodeCount(nodeCount), Math.max(minimumUsefulCap, hubDistanceCap))
735
856
  return Math.max(zoomRange.min * 2, capped)
736
857
  }
737
858
 
@@ -1066,7 +1187,12 @@ const hitNode = point => {
1066
1187
  if (state.renderClusters.length > 0) {
1067
1188
  return null
1068
1189
  }
1069
- if (state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.9) {
1190
+ const hitScaleFloor = state.nodes.length > massiveGraphNodeThreshold
1191
+ ? 0.2
1192
+ : state.nodes.length > largeGraphNodeThreshold
1193
+ ? 0.34
1194
+ : 0
1195
+ if (state.transform.scale < hitScaleFloor) {
1070
1196
  return null
1071
1197
  }
1072
1198
 
@@ -1225,7 +1351,7 @@ const computeRenderVisibility = () => {
1225
1351
  state.renderNodes = state.visibleNodes
1226
1352
  state.renderClusters = []
1227
1353
  const ids = new Set(state.renderNodes.map((node) => node.id))
1228
- state.renderEdges = collectVisibleEdgesForNodes(ids)
1354
+ state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
1229
1355
  return
1230
1356
  }
1231
1357
 
@@ -1249,7 +1375,7 @@ const computeRenderVisibility = () => {
1249
1375
 
1250
1376
  state.renderClusters = []
1251
1377
  state.renderNodes = sampledNodes
1252
- state.renderEdges = sampledEdges
1378
+ state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
1253
1379
  return
1254
1380
  }
1255
1381
 
@@ -1258,7 +1384,7 @@ const computeRenderVisibility = () => {
1258
1384
  const sampledIds = new Set(sampled.map((node) => node.id))
1259
1385
  state.renderClusters = []
1260
1386
  state.renderNodes = sampled
1261
- state.renderEdges = collectVisibleEdgesForNodes(sampledIds)
1387
+ state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
1262
1388
  return
1263
1389
  }
1264
1390
 
@@ -1294,7 +1420,7 @@ const computeRenderVisibility = () => {
1294
1420
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
1295
1421
  state.renderNodes = fallbackNodes
1296
1422
  state.renderClusters = []
1297
- state.renderEdges = collectVisibleEdgesForNodes(fallbackIds)
1423
+ state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
1298
1424
  return
1299
1425
  }
1300
1426
 
@@ -1303,14 +1429,14 @@ const computeRenderVisibility = () => {
1303
1429
  const edges = collectVisibleEdgesForNodes(nodeIds)
1304
1430
 
1305
1431
  state.renderNodes = normalizedNodes
1306
- state.renderEdges = edges
1432
+ state.renderEdges = withMeshEdges(normalizedNodes, edges)
1307
1433
 
1308
1434
  if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
1309
1435
  const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
1310
1436
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
1311
1437
  state.renderClusters = []
1312
1438
  state.renderNodes = fallbackNodes
1313
- state.renderEdges = collectVisibleEdgesForNodes(fallbackIds)
1439
+ state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
1314
1440
  }
1315
1441
  }
1316
1442
 
@@ -1420,11 +1546,18 @@ const render = now => {
1420
1546
  if (drawEdges) {
1421
1547
  state.renderEdges.forEach(edge => {
1422
1548
  const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
1549
+ const inferredEdge = Boolean(edge.inferred)
1423
1550
  ctx.beginPath()
1424
1551
  ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
1425
1552
  ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
1426
- ctx.strokeStyle = selectedEdge ? graphTheme.edgeActive : graphTheme.edge
1427
- ctx.lineWidth = (selectedEdge ? 1.8 : 1) + Math.min(edgeWeight(edge) - 1, 8) * 0.22
1553
+ ctx.strokeStyle = selectedEdge
1554
+ ? graphTheme.edgeActive
1555
+ : inferredEdge
1556
+ ? 'rgba(203, 213, 225, 0.1)'
1557
+ : graphTheme.edge
1558
+ ctx.lineWidth = inferredEdge
1559
+ ? 0.82
1560
+ : (selectedEdge ? 1.8 : 1) + Math.min(edgeWeight(edge) - 1, 8) * 0.22
1428
1561
  ctx.stroke()
1429
1562
  })
1430
1563
  }
@@ -1478,6 +1611,7 @@ const render = now => {
1478
1611
  const shouldDrawLabels =
1479
1612
  isSelected ||
1480
1613
  isHovered ||
1614
+ (state.nodes.length > largeGraphNodeThreshold && state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) ||
1481
1615
  (state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
1482
1616
  if (shouldDrawLabels) {
1483
1617
  ctx.fillStyle = graphTheme.label
@@ -1721,7 +1855,7 @@ const bindEvents = () => {
1721
1855
  const point = worldPoint(event)
1722
1856
  const now = performance.now()
1723
1857
  const canHoverHitTest =
1724
- !(state.nodes.length > massiveGraphNodeThreshold && state.transform.scale < 0.12)
1858
+ !(state.nodes.length > massiveGraphNodeThreshold && state.transform.scale < 0.06)
1725
1859
  const shouldHitTest = canHoverHitTest &&
1726
1860
  (state.pointer.down || now - state.lastHoverHitAt >= hoverHitTestIntervalMs)
1727
1861
  if (shouldHitTest) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.57",
3
+ "version": "0.1.0-beta.59",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",