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

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.
@@ -6,7 +6,8 @@ const massiveGraphNodeThreshold = 20000
6
6
  const largeGraphEdgeRenderLimit = 120000
7
7
  const renderNodeBudget = 900
8
8
  const zoomedMassiveRenderNodeBudget = 2200
9
- const renderEdgeBudget = 2400
9
+ const massiveOverviewRenderNodeBudget = 1800
10
+ const massiveOverviewScaleThreshold = 0.065
10
11
  const massiveAutoFitMacroScale = 0.018
11
12
  const minNodePixelRadius = 2.3
12
13
  const viewportPaddingPx = 280
@@ -17,7 +18,6 @@ const zoomRecoveryGuardMs = 4200
17
18
  const meshEdgeScaleThreshold = 0.09
18
19
  const meshEdgeMinBudget = 140
19
20
  const meshEdgeMaxBudget = 1400
20
- const layeredCoreScaleThreshold = 0.55
21
21
  const dragNeighborhoodMaxAffected = 180
22
22
  const dragSettleRounds = 3
23
23
  const wheelZoomExponent = 0.0009
@@ -42,7 +42,6 @@ const state = {
42
42
  visibleEdges: [],
43
43
  renderNodes: [],
44
44
  renderEdges: [],
45
- renderNodeDepthProjectionById: new Map(),
46
45
  nodeDegrees: new Map(),
47
46
  selected: null,
48
47
  hovered: null,
@@ -73,7 +72,6 @@ const state = {
73
72
  lastHoverHitAt: 0,
74
73
  lastManualZoomAt: 0,
75
74
  lastZoomFocus: { x: 0, y: 0, at: 0 },
76
- depthProjectionActive: false,
77
75
  zoomTransition: {
78
76
  active: false,
79
77
  source: 'generic',
@@ -635,12 +633,6 @@ const createVisibleEdgeLookup = edges => {
635
633
  return lookup
636
634
  }
637
635
 
638
- const isClusterInViewport = (cluster, viewport) =>
639
- cluster.x >= viewport.minX &&
640
- cluster.x <= viewport.maxX &&
641
- cluster.y >= viewport.minY &&
642
- cluster.y <= viewport.maxY
643
-
644
636
  const edgeBudgetForCurrentFrame = () => {
645
637
  const zoom = state.transform.scale
646
638
  if (zoom < 0.12) return 380
@@ -652,112 +644,24 @@ const edgeBudgetForCurrentFrame = () => {
652
644
  return 7600
653
645
  }
654
646
 
655
- const clusterBudgetForScale = (scale) => {
656
- if (scale < 0.008) return 90
657
- if (scale < 0.014) return 150
658
- if (scale < 0.022) return 240
659
- if (scale < 0.035) return 360
660
- return 520
661
- }
662
-
663
647
  const nodeBudgetForScale = (scale) => {
664
- if (scale < 0.035) return 220
665
- if (scale < 0.06) return 360
666
- if (scale < 0.09) return 520
667
- if (scale < 0.14) return 720
668
648
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
649
+ if (scale < massiveOverviewScaleThreshold) return massiveOverviewRenderNodeBudget
650
+ if (scale < 0.09) return 1600
651
+ if (scale < 0.14) return 1800
669
652
  if (scale < 0.28) return renderNodeBudget
670
653
  if (scale < 0.45) return 1100
671
654
  if (scale < 0.7) return 1400
672
655
  if (scale < 1.05) return 1800
673
656
  return zoomedMassiveRenderNodeBudget
674
657
  }
658
+ if (scale < 0.035) return 220
659
+ if (scale < 0.06) return 360
660
+ if (scale < 0.09) return 520
661
+ if (scale < 0.14) return 720
675
662
  return renderNodeBudget
676
663
  }
677
664
 
678
- const layerFocusForScale = (scale) => {
679
- const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
680
- const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
681
- const shellWidth = Math.max(0.24, 0.46 - normalized * 0.16)
682
- const coreRadius = Math.max(0.06, 0.1 + normalized * 0.22)
683
- const coreRatio = Math.max(0.2, Math.min(0.72, 0.24 + normalized * 0.48))
684
-
685
- return { shellCenter, shellWidth, coreRadius, coreRatio }
686
- }
687
-
688
- const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
689
- const hub = state.primaryHub
690
- if (!hub || sourceNodes.length <= 1200 || state.visibleNodes.length <= massiveGraphNodeThreshold) {
691
- return sourceNodes
692
- }
693
-
694
- let maxDistance = 0
695
- const distances = sourceNodes.map((node) => {
696
- const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
697
- if (distance > maxDistance) {
698
- maxDistance = distance
699
- }
700
- return { node, distance }
701
- })
702
-
703
- if (maxDistance <= 0.001) {
704
- return sourceNodes
705
- }
706
-
707
- const focus = layerFocusForScale(state.transform.scale)
708
- const normalizedRows = distances.map((item) => ({
709
- ...item,
710
- normalized: item.distance / maxDistance
711
- }))
712
- const desired = Math.max(260, Math.min(sourceNodes.length, targetCount * 2))
713
- const coreTarget = Math.max(36, Math.min(desired - 8, Math.floor(desired * focus.coreRatio)))
714
- const shellTarget = Math.max(12, desired - coreTarget)
715
- const shellHalf = focus.shellWidth / 2
716
-
717
- const coreNodes = normalizedRows
718
- .filter((item) => item.normalized <= focus.coreRadius)
719
- .sort((left, right) => {
720
- const leftScore = state.nodeDegrees.get(left.node.id) ?? 0
721
- const rightScore = state.nodeDegrees.get(right.node.id) ?? 0
722
- if (leftScore !== rightScore) return rightScore - leftScore
723
- return left.node.id.localeCompare(right.node.id)
724
- })
725
- .slice(0, coreTarget)
726
- .map((item) => item.node)
727
-
728
- const shellNodes = normalizedRows
729
- .sort((left, right) => {
730
- const leftDelta = Math.abs(left.normalized - focus.shellCenter)
731
- const rightDelta = Math.abs(right.normalized - focus.shellCenter)
732
- const leftInside = leftDelta <= shellHalf ? 0 : 1
733
- const rightInside = rightDelta <= shellHalf ? 0 : 1
734
- if (leftInside !== rightInside) return leftInside - rightInside
735
- if (leftDelta !== rightDelta) return leftDelta - rightDelta
736
- const leftScore = state.nodeDegrees.get(left.node.id) ?? 0
737
- const rightScore = state.nodeDegrees.get(right.node.id) ?? 0
738
- if (leftScore !== rightScore) return rightScore - leftScore
739
- return left.node.id.localeCompare(right.node.id)
740
- })
741
- .slice(0, shellTarget)
742
- .map((item) => item.node)
743
-
744
- const merged = []
745
- const ids = new Set()
746
- const pushUnique = (node) => {
747
- if (!node || ids.has(node.id)) return
748
- ids.add(node.id)
749
- merged.push(node)
750
- }
751
-
752
- if (state.transform.scale >= layeredCoreScaleThreshold) {
753
- pushUnique(hub)
754
- }
755
- for (let index = 0; index < coreNodes.length; index += 1) pushUnique(coreNodes[index])
756
- for (let index = 0; index < shellNodes.length; index += 1) pushUnique(shellNodes[index])
757
-
758
- return merged.length > 0 ? merged : sourceNodes
759
- }
760
-
761
665
  const viewportCenterWorldPoint = () => {
762
666
  const viewport = worldViewportBounds()
763
667
  return {
@@ -857,31 +761,6 @@ const selectStableSampleNodes = (sourceNodes, limit) => {
857
761
  .slice(0, limit)
858
762
  }
859
763
 
860
- const selectAccessBridgeNodes = (sourceNodes, limit) => {
861
- if (limit <= 0 || sourceNodes.length === 0) {
862
- return []
863
- }
864
-
865
- const now = performance.now()
866
- const cursorPoint = cursorWorldPoint()
867
- const recentZoomFocus =
868
- now - state.lastZoomFocus.at <= 1200
869
- ? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
870
- : null
871
- const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
872
- return [...sourceNodes]
873
- .sort((left, right) => {
874
- const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
875
- const rightDistance = Math.hypot(right.x - anchor.x, right.y - anchor.y)
876
- if (leftDistance !== rightDistance) return leftDistance - rightDistance
877
- const leftDegree = state.nodeDegrees.get(left.id) ?? 0
878
- const rightDegree = state.nodeDegrees.get(right.id) ?? 0
879
- if (leftDegree !== rightDegree) return rightDegree - leftDegree
880
- return left.id.localeCompare(right.id)
881
- })
882
- .slice(0, limit)
883
- }
884
-
885
764
  const edgeIdentityKey = edge => {
886
765
  if (!edge.target) return ''
887
766
  const pair = edge.source < edge.target
@@ -1002,8 +881,8 @@ const edgeWidthFor = (edge, selectedEdge) => {
1002
881
  const drawGraphEdge = (edge) => {
1003
882
  const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
1004
883
  ctx.beginPath()
1005
- ctx.moveTo(nodeRenderX(edge.sourceNode), nodeRenderY(edge.sourceNode))
1006
- ctx.lineTo(nodeRenderX(edge.targetNode), nodeRenderY(edge.targetNode))
884
+ ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
885
+ ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
1007
886
  ctx.strokeStyle = edgeStrokeFor(edge, selectedEdge)
1008
887
  ctx.lineWidth = edgeWidthFor(edge, selectedEdge)
1009
888
  ctx.stroke()
@@ -1017,8 +896,8 @@ const drawEdgeBatch = (edges, options) => {
1017
896
  ctx.beginPath()
1018
897
  for (let index = 0; index < edges.length; index += 1) {
1019
898
  const edge = edges[index]
1020
- ctx.moveTo(nodeRenderX(edge.sourceNode), nodeRenderY(edge.sourceNode))
1021
- ctx.lineTo(nodeRenderX(edge.targetNode), nodeRenderY(edge.targetNode))
899
+ ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
900
+ ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
1022
901
  }
1023
902
  ctx.strokeStyle = options.strokeStyle
1024
903
  ctx.lineWidth = options.lineWidth
@@ -1073,13 +952,11 @@ const shouldDrawNodeLabels = (node, isSelected, isHovered) =>
1073
952
  (state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
1074
953
 
1075
954
  const drawSingleNode = (node, options = { drawLabel: true }) => {
1076
- const radius = nodeRadius(node) * nodeRenderScale(node)
1077
- const x = nodeRenderX(node)
1078
- const y = nodeRenderY(node)
1079
- const opacity = nodeRenderOpacity(node)
955
+ const radius = nodeRadius(node)
956
+ const x = node.x
957
+ const y = node.y
1080
958
  const isSelected = state.selected?.id === node.id
1081
959
  const isHovered = state.hovered?.id === node.id
1082
- ctx.globalAlpha = opacity
1083
960
  ctx.beginPath()
1084
961
  ctx.arc(x, y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
1085
962
  ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
@@ -1093,7 +970,7 @@ const drawSingleNode = (node, options = { drawLabel: true }) => {
1093
970
  ctx.stroke()
1094
971
 
1095
972
  if (options.drawLabel && shouldDrawNodeLabels(node, isSelected, isHovered)) {
1096
- ctx.globalAlpha = Math.max(0.58, opacity)
973
+ ctx.globalAlpha = 1
1097
974
  ctx.fillStyle = graphTheme.label
1098
975
  ctx.font = '12px Inter, system-ui, sans-serif'
1099
976
  ctx.textAlign = 'center'
@@ -1112,10 +989,10 @@ const drawNodeBatch = (nodes) => {
1112
989
  if (drawHalos) {
1113
990
  for (let index = 0; index < nodes.length; index += 1) {
1114
991
  const node = nodes[index]
1115
- const radius = nodeRadius(node) * nodeRenderScale(node)
1116
- const x = nodeRenderX(node)
1117
- const y = nodeRenderY(node)
1118
- ctx.globalAlpha = Math.max(0.2, nodeRenderOpacity(node) * 0.5)
992
+ const radius = nodeRadius(node)
993
+ const x = node.x
994
+ const y = node.y
995
+ ctx.globalAlpha = 0.5
1119
996
  ctx.beginPath()
1120
997
  ctx.arc(x, y, radius + 3, 0, Math.PI * 2)
1121
998
  ctx.fillStyle = graphTheme.nodeHalo
@@ -1126,11 +1003,10 @@ const drawNodeBatch = (nodes) => {
1126
1003
 
1127
1004
  for (let index = 0; index < nodes.length; index += 1) {
1128
1005
  const node = nodes[index]
1129
- const radius = nodeRadius(node) * nodeRenderScale(node)
1130
- const x = nodeRenderX(node)
1131
- const y = nodeRenderY(node)
1132
- const opacity = nodeRenderOpacity(node)
1133
- ctx.globalAlpha = opacity
1006
+ const radius = nodeRadius(node)
1007
+ const x = node.x
1008
+ const y = node.y
1009
+ ctx.globalAlpha = 1
1134
1010
  ctx.beginPath()
1135
1011
  ctx.arc(x, y, radius, 0, Math.PI * 2)
1136
1012
  ctx.fillStyle = graphTheme.node
@@ -1167,10 +1043,10 @@ const drawGraphNodes = () => {
1167
1043
  ctx.textBaseline = 'top'
1168
1044
  for (let index = 0; index < regularNodes.length; index += 1) {
1169
1045
  const node = regularNodes[index]
1170
- const x = nodeRenderX(node)
1171
- const y = nodeRenderY(node)
1172
- const radius = nodeRadius(node) * nodeRenderScale(node)
1173
- ctx.globalAlpha = Math.max(0.6, nodeRenderOpacity(node))
1046
+ const x = node.x
1047
+ const y = node.y
1048
+ const radius = nodeRadius(node)
1049
+ ctx.globalAlpha = 1
1174
1050
  ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
1175
1051
  }
1176
1052
  ctx.globalAlpha = 1
@@ -1224,10 +1100,10 @@ const drawGraphLabels = nodes => {
1224
1100
  ctx.textBaseline = 'top'
1225
1101
  for (let index = 0; index < nodes.length; index += 1) {
1226
1102
  const node = nodes[index]
1227
- const x = nodeRenderX(node)
1228
- const y = nodeRenderY(node)
1229
- const radius = nodeRadius(node) * nodeRenderScale(node)
1230
- ctx.globalAlpha = Math.max(0.6, nodeRenderOpacity(node))
1103
+ const x = node.x
1104
+ const y = node.y
1105
+ const radius = nodeRadius(node)
1106
+ ctx.globalAlpha = 1
1231
1107
  ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
1232
1108
  }
1233
1109
  ctx.globalAlpha = 1
@@ -1435,6 +1311,11 @@ const sampleVisibleNodes = (limit = renderNodeBudget, sourceNodes = state.visibl
1435
1311
  return nodes
1436
1312
  }
1437
1313
 
1314
+ const sampleMassiveOverviewNodes = (limit) => {
1315
+ const sampled = sampleVisibleNodes(limit, state.visibleNodes)
1316
+ return ensureHubNodesInRenderedSet(sampled)
1317
+ }
1318
+
1438
1319
  const enrichSampleWithNeighbors = (nodes) => {
1439
1320
  if (nodes.length === 0) {
1440
1321
  return {
@@ -1724,7 +1605,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
1724
1605
  return { min: 0.0085, max: 0.36 }
1725
1606
  }
1726
1607
 
1727
- const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
1608
+ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
1728
1609
  const rect = canvas.getBoundingClientRect()
1729
1610
  const width = Math.max(rect.width, 320)
1730
1611
  const height = Math.max(rect.height, 320)
@@ -1777,12 +1658,12 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
1777
1658
  markRenderDirty()
1778
1659
  }
1779
1660
 
1780
- const resetView = () => fitView({ useFiltered: false, macro: false, preferHubCenter: false })
1661
+ const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
1781
1662
 
1782
1663
  const focusPrimaryHub = () => {
1783
1664
  const hub = state.primaryHub
1784
1665
  if (!hub) {
1785
- fitView({ useFiltered: true, macro: false, preferHubCenter: true })
1666
+ fitView({ useFiltered: true, preferHubCenter: true })
1786
1667
  return
1787
1668
  }
1788
1669
 
@@ -2161,9 +2042,9 @@ const hitNode = point => {
2161
2042
  const nodes = state.renderNodes
2162
2043
  for (let index = nodes.length - 1; index >= 0; index -= 1) {
2163
2044
  const node = nodes[index]
2164
- const radius = nodeRadius(node) * nodeRenderScale(node)
2165
- const x = nodeRenderX(node)
2166
- const y = nodeRenderY(node)
2045
+ const radius = nodeRadius(node)
2046
+ const x = node.x
2047
+ const y = node.y
2167
2048
  if (Math.hypot(point.x - x, point.y - y) <= radius + 5) return node
2168
2049
  }
2169
2050
  return null
@@ -2176,15 +2057,6 @@ const baseNodeRadius = node => {
2176
2057
 
2177
2058
  const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
2178
2059
 
2179
- const refreshRenderNodeDepthProjection = () => {
2180
- state.renderNodeDepthProjectionById = new Map()
2181
- }
2182
-
2183
- const projectedNode = node => state.renderNodeDepthProjectionById.get(node.id) ?? null
2184
- const nodeRenderX = node => projectedNode(node)?.x ?? node.x
2185
- const nodeRenderY = node => projectedNode(node)?.y ?? node.y
2186
- const nodeRenderScale = node => projectedNode(node)?.scale ?? 1
2187
- const nodeRenderOpacity = node => projectedNode(node)?.opacity ?? 1
2188
2060
  const worldViewportBounds = () => {
2189
2061
  const width = Math.max(state.viewport.width, 320)
2190
2062
  const height = Math.max(state.viewport.height, 320)
@@ -2264,9 +2136,17 @@ const computeRenderVisibility = () => {
2264
2136
  }
2265
2137
 
2266
2138
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
2267
- const viewportNodes = viewportNodesFromSpatialIndex(viewport)
2268
- const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
2269
2139
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
2140
+ if (state.transform.scale < massiveOverviewScaleThreshold) {
2141
+ const overviewNodes = sampleMassiveOverviewNodes(sampleLimit)
2142
+ const overviewIds = new Set(overviewNodes.map((node) => node.id))
2143
+ state.renderNodes = overviewNodes
2144
+ state.renderEdges = withMeshEdges(overviewNodes, collectVisibleEdgesForNodes(overviewIds))
2145
+ return
2146
+ }
2147
+
2148
+ const viewportNodes = viewportNodesFromSpatialIndex(viewport)
2149
+ const sourceNodes = viewportNodes.length > 0 ? viewportNodes : sampleMassiveOverviewNodes(sampleLimit)
2270
2150
  const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
2271
2151
  const carryViewport = expandViewportBounds(viewport, carryMargin)
2272
2152
  const carryOverLimit = Math.max(180, Math.min(sampleLimit, Math.floor(sampleLimit * 0.5)))
@@ -2441,10 +2321,11 @@ const render = now => {
2441
2321
 
2442
2322
  computeRenderVisibility()
2443
2323
  tick(delta, now)
2444
- refreshRenderNodeDepthProjection()
2445
2324
  const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
2446
2325
  const manualZoomGuardActive = now - state.lastManualZoomAt < zoomRecoveryGuardMs
2447
- const allowViewportAutoRecovery = state.nodes.length <= massiveGraphNodeThreshold
2326
+ const allowViewportAutoRecovery =
2327
+ state.nodes.length <= massiveGraphNodeThreshold ||
2328
+ state.transform.scale >= massiveOverviewScaleThreshold
2448
2329
  if (allowViewportAutoRecovery && !hasVisibleNodeOnScreen && state.renderNodes.length > 0 && !manualZoomGuardActive) {
2449
2330
  state.offscreenFrameCount += 1
2450
2331
  if (state.offscreenFrameCount >= 22 && !state.recoveringViewport) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.116",
3
+ "version": "0.1.0-beta.117",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",