@andespindola/brainlink 0.1.0-beta.76 → 0.1.0-beta.77

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.
@@ -579,6 +579,14 @@ const nodeBudgetForScale = (scale) => {
579
579
  return renderNodeBudget
580
580
  }
581
581
 
582
+ const massiveLowZoomNodeBudgetForScale = (scale) => {
583
+ if (scale < 0.004) return 780
584
+ if (scale < 0.01) return 860
585
+ if (scale < 0.02) return 900
586
+ if (scale < 0.035) return 900
587
+ return renderNodeBudget
588
+ }
589
+
582
590
  const layerFocusForScale = (scale) => {
583
591
  const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
584
592
  const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
@@ -1119,6 +1127,7 @@ const includeHubPreviewNeighborhood = (nodes, limit) => {
1119
1127
  const maxNodes = Math.max(1, Math.min(renderNodeBudget, limit))
1120
1128
  const merged = [...nodes]
1121
1129
  const ids = new Set(merged.map((node) => node.id))
1130
+ const protectedIds = new Set()
1122
1131
 
1123
1132
  if (!ids.has(hub.id)) {
1124
1133
  if (merged.length < maxNodes) {
@@ -1133,6 +1142,7 @@ const includeHubPreviewNeighborhood = (nodes, limit) => {
1133
1142
  }
1134
1143
  }
1135
1144
  }
1145
+ protectedIds.add(hub.id)
1136
1146
 
1137
1147
  const hubEdges = [...(state.visibleEdgeByNode.get(hub.id) ?? [])]
1138
1148
  .filter((edge) => edge.target && (edge.source === hub.id || edge.target === hub.id))
@@ -1161,8 +1171,31 @@ const includeHubPreviewNeighborhood = (nodes, limit) => {
1161
1171
  continue
1162
1172
  }
1163
1173
 
1164
- ids.add(otherId)
1165
- merged.push(otherNode)
1174
+ if (merged.length < maxNodes) {
1175
+ ids.add(otherId)
1176
+ merged.push(otherNode)
1177
+ protectedIds.add(otherId)
1178
+ continue
1179
+ }
1180
+
1181
+ const replaceIndex = (() => {
1182
+ for (let cursor = merged.length - 1; cursor >= 0; cursor -= 1) {
1183
+ const candidateId = merged[cursor]?.id
1184
+ if (candidateId && !protectedIds.has(candidateId)) {
1185
+ return cursor
1186
+ }
1187
+ }
1188
+ return -1
1189
+ })()
1190
+ if (replaceIndex >= 0) {
1191
+ const replacedId = merged[replaceIndex]?.id
1192
+ if (replacedId) {
1193
+ ids.delete(replacedId)
1194
+ }
1195
+ merged[replaceIndex] = otherNode
1196
+ ids.add(otherId)
1197
+ protectedIds.add(otherId)
1198
+ }
1166
1199
  }
1167
1200
 
1168
1201
  return merged
@@ -1283,7 +1316,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
1283
1316
  if (nodeCount <= 600) return { min: 0.12, max: 0.72 }
1284
1317
  if (nodeCount <= 2000) return { min: 0.08, max: 0.52 }
1285
1318
  if (nodeCount <= 6000) return { min: 0.06, max: 0.32 }
1286
- return { min: 0.0008, max: 0.24 }
1319
+ return { min: 0.0012, max: 0.24 }
1287
1320
  }
1288
1321
 
1289
1322
  const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
@@ -1882,16 +1915,26 @@ const computeRenderVisibility = () => {
1882
1915
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
1883
1916
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1884
1917
  if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
1918
+ const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
1885
1919
  const overviewClusters = filterOverviewClustersByViewport(viewport)
1886
1920
  .sort((left, right) => right.count - left.count)
1887
- .slice(0, Math.min(renderNodeBudget, clusterBudgetForScale(state.transform.scale)))
1921
+ .slice(0, overviewLimit)
1888
1922
  if (overviewClusters.length > 0) {
1889
- state.renderClusters = []
1890
- state.renderNodes = representativeNodesFromClusters(
1923
+ const overviewNodes = representativeNodesFromClusters(
1891
1924
  overviewClusters,
1892
- Math.min(renderNodeBudget, clusterBudgetForScale(state.transform.scale))
1925
+ overviewLimit
1926
+ )
1927
+ const anchoredNodes = includeHubPreviewNeighborhood(
1928
+ overviewNodes,
1929
+ Math.min(renderNodeBudget, overviewLimit)
1893
1930
  )
1894
- state.renderEdges = []
1931
+ const enriched = enrichSampleWithNeighbors(anchoredNodes)
1932
+ const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
1933
+ const previewIds = new Set(previewNodes.map((node) => node.id))
1934
+ const previewEdges = collectVisibleEdgesForNodes(previewIds)
1935
+ state.renderClusters = []
1936
+ state.renderNodes = previewNodes
1937
+ state.renderEdges = previewEdges
1895
1938
  return
1896
1939
  }
1897
1940
  }
@@ -2099,13 +2142,15 @@ const render = now => {
2099
2142
  state.offscreenFrameCount = 0
2100
2143
  }
2101
2144
  const minimumEdgeScale =
2102
- state.renderNodes.length > 1300
2103
- ? 0.12
2104
- : state.renderNodes.length > 900
2105
- ? 0.085
2106
- : state.renderNodes.length > 500
2107
- ? 0.05
2108
- : 0
2145
+ state.nodes.length > massiveGraphNodeThreshold
2146
+ ? 0
2147
+ : state.renderNodes.length > 1300
2148
+ ? 0.12
2149
+ : state.renderNodes.length > 900
2150
+ ? 0.085
2151
+ : state.renderNodes.length > 500
2152
+ ? 0.05
2153
+ : 0
2109
2154
  const drawEdges =
2110
2155
  state.renderClusters.length === 0 &&
2111
2156
  state.transform.scale >= minimumEdgeScale
@@ -2288,8 +2333,28 @@ const selectNodeById = id => {
2288
2333
  }
2289
2334
 
2290
2335
  const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
2336
+ const resolveZoomFactor = () => {
2337
+ if (state.nodes.length <= massiveGraphNodeThreshold) {
2338
+ return factor
2339
+ }
2340
+
2341
+ const scale = state.transform.scale
2342
+ if (factor > 1) {
2343
+ if (scale < 0.006) return Math.max(factor, 1.48)
2344
+ if (scale < 0.02) return Math.max(factor, 1.34)
2345
+ if (scale < 0.08) return Math.max(factor, 1.22)
2346
+ return factor
2347
+ }
2348
+
2349
+ if (scale < 0.006) return Math.min(factor, 0.68)
2350
+ if (scale < 0.02) return Math.min(factor, 0.78)
2351
+ if (scale < 0.08) return Math.min(factor, 0.86)
2352
+ return factor
2353
+ }
2354
+
2291
2355
  state.lastManualZoomAt = performance.now()
2292
- const nextScale = clampScale(state.transform.scale * factor)
2356
+ const effectiveFactor = resolveZoomFactor()
2357
+ const nextScale = clampScale(state.transform.scale * effectiveFactor)
2293
2358
  if (nextScale === state.transform.scale) {
2294
2359
  return
2295
2360
  }
@@ -2366,11 +2431,11 @@ const bindEvents = () => {
2366
2431
  })
2367
2432
  elements.zoomIn.addEventListener('click', () => {
2368
2433
  const rect = canvas.getBoundingClientRect()
2369
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14)
2434
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14, 'button')
2370
2435
  })
2371
2436
  elements.zoomOut.addEventListener('click', () => {
2372
2437
  const rect = canvas.getBoundingClientRect()
2373
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88)
2438
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88, 'button')
2374
2439
  })
2375
2440
  if (elements.fit) {
2376
2441
  elements.fit.addEventListener('click', () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.76",
3
+ "version": "0.1.0-beta.77",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",