@andespindola/brainlink 0.1.0-beta.58 → 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)
@@ -1232,7 +1351,7 @@ const computeRenderVisibility = () => {
1232
1351
  state.renderNodes = state.visibleNodes
1233
1352
  state.renderClusters = []
1234
1353
  const ids = new Set(state.renderNodes.map((node) => node.id))
1235
- state.renderEdges = collectVisibleEdgesForNodes(ids)
1354
+ state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
1236
1355
  return
1237
1356
  }
1238
1357
 
@@ -1256,7 +1375,7 @@ const computeRenderVisibility = () => {
1256
1375
 
1257
1376
  state.renderClusters = []
1258
1377
  state.renderNodes = sampledNodes
1259
- state.renderEdges = sampledEdges
1378
+ state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
1260
1379
  return
1261
1380
  }
1262
1381
 
@@ -1265,7 +1384,7 @@ const computeRenderVisibility = () => {
1265
1384
  const sampledIds = new Set(sampled.map((node) => node.id))
1266
1385
  state.renderClusters = []
1267
1386
  state.renderNodes = sampled
1268
- state.renderEdges = collectVisibleEdgesForNodes(sampledIds)
1387
+ state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
1269
1388
  return
1270
1389
  }
1271
1390
 
@@ -1301,7 +1420,7 @@ const computeRenderVisibility = () => {
1301
1420
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
1302
1421
  state.renderNodes = fallbackNodes
1303
1422
  state.renderClusters = []
1304
- state.renderEdges = collectVisibleEdgesForNodes(fallbackIds)
1423
+ state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
1305
1424
  return
1306
1425
  }
1307
1426
 
@@ -1310,14 +1429,14 @@ const computeRenderVisibility = () => {
1310
1429
  const edges = collectVisibleEdgesForNodes(nodeIds)
1311
1430
 
1312
1431
  state.renderNodes = normalizedNodes
1313
- state.renderEdges = edges
1432
+ state.renderEdges = withMeshEdges(normalizedNodes, edges)
1314
1433
 
1315
1434
  if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
1316
1435
  const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
1317
1436
  const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
1318
1437
  state.renderClusters = []
1319
1438
  state.renderNodes = fallbackNodes
1320
- state.renderEdges = collectVisibleEdgesForNodes(fallbackIds)
1439
+ state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
1321
1440
  }
1322
1441
  }
1323
1442
 
@@ -1427,11 +1546,18 @@ const render = now => {
1427
1546
  if (drawEdges) {
1428
1547
  state.renderEdges.forEach(edge => {
1429
1548
  const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
1549
+ const inferredEdge = Boolean(edge.inferred)
1430
1550
  ctx.beginPath()
1431
1551
  ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
1432
1552
  ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
1433
- ctx.strokeStyle = selectedEdge ? graphTheme.edgeActive : graphTheme.edge
1434
- 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
1435
1561
  ctx.stroke()
1436
1562
  })
1437
1563
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.58",
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",