@andespindola/brainlink 0.1.0-beta.65 → 0.1.0-beta.67

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.
@@ -64,7 +64,8 @@ const state = {
64
64
  filterWorker: null,
65
65
  filterReady: false,
66
66
  lastHoverHitAt: 0,
67
- lastManualZoomAt: 0
67
+ lastManualZoomAt: 0,
68
+ lastZoomFocus: { x: 0, y: 0, at: 0 }
68
69
  }
69
70
 
70
71
  const byId = id => document.getElementById(id)
@@ -573,7 +574,7 @@ const nodeBudgetForScale = (scale) => {
573
574
  const layerFocusForScale = (scale) => {
574
575
  const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
575
576
  const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
576
- const shellWidth = Math.max(0.16, 0.34 - normalized * 0.2)
577
+ const shellWidth = Math.max(0.24, 0.46 - normalized * 0.16)
577
578
  const coreRadius = Math.max(0.06, 0.1 + normalized * 0.22)
578
579
  const coreRatio = Math.max(0.2, Math.min(0.72, 0.24 + normalized * 0.48))
579
580
 
@@ -604,7 +605,7 @@ const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
604
605
  ...item,
605
606
  normalized: item.distance / maxDistance
606
607
  }))
607
- const desired = Math.max(220, Math.min(sourceNodes.length, targetCount * 2))
608
+ const desired = Math.max(260, Math.min(sourceNodes.length, targetCount * 2))
608
609
  const coreTarget = Math.max(36, Math.min(desired - 8, Math.floor(desired * focus.coreRatio)))
609
610
  const shellTarget = Math.max(12, desired - coreTarget)
610
611
  const shellHalf = focus.shellWidth / 2
@@ -653,6 +654,60 @@ const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
653
654
  return merged.length > 0 ? merged : sourceNodes
654
655
  }
655
656
 
657
+ const viewportCenterWorldPoint = () => {
658
+ const viewport = worldViewportBounds()
659
+ return {
660
+ x: (viewport.minX + viewport.maxX) / 2,
661
+ y: (viewport.minY + viewport.maxY) / 2
662
+ }
663
+ }
664
+
665
+ const mergeUniqueNodes = (leftNodes, rightNodes, limit) => {
666
+ const merged = []
667
+ const ids = new Set()
668
+
669
+ const push = (node) => {
670
+ if (!node || ids.has(node.id) || merged.length >= limit) {
671
+ return
672
+ }
673
+ ids.add(node.id)
674
+ merged.push(node)
675
+ }
676
+
677
+ for (let index = 0; index < leftNodes.length && merged.length < limit; index += 1) {
678
+ push(leftNodes[index])
679
+ }
680
+ for (let index = 0; index < rightNodes.length && merged.length < limit; index += 1) {
681
+ push(rightNodes[index])
682
+ }
683
+
684
+ return merged
685
+ }
686
+
687
+ const selectAccessBridgeNodes = (sourceNodes, limit) => {
688
+ if (limit <= 0 || sourceNodes.length === 0) {
689
+ return []
690
+ }
691
+
692
+ const now = performance.now()
693
+ const recentZoomFocus =
694
+ now - state.lastZoomFocus.at <= 1200
695
+ ? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
696
+ : null
697
+ const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
698
+ return [...sourceNodes]
699
+ .sort((left, right) => {
700
+ const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
701
+ const rightDistance = Math.hypot(right.x - anchor.x, right.y - anchor.y)
702
+ if (leftDistance !== rightDistance) return leftDistance - rightDistance
703
+ const leftDegree = state.nodeDegrees.get(left.id) ?? 0
704
+ const rightDegree = state.nodeDegrees.get(right.id) ?? 0
705
+ if (leftDegree !== rightDegree) return rightDegree - leftDegree
706
+ return left.id.localeCompare(right.id)
707
+ })
708
+ .slice(0, limit)
709
+ }
710
+
656
711
  const edgeIdentityKey = edge => {
657
712
  if (!edge.target) return ''
658
713
  const pair = edge.source < edge.target
@@ -1183,9 +1238,17 @@ const focusPrimaryHub = () => {
1183
1238
  markRenderDirty()
1184
1239
  }
1185
1240
 
1241
+ const layoutDensityScaleForNodeCount = (nodeCount) => {
1242
+ if (nodeCount > 50000) return 0.56
1243
+ if (nodeCount > 20000) return 0.64
1244
+ if (nodeCount > 6000) return 0.76
1245
+ return 1
1246
+ }
1247
+
1186
1248
  const createLayout = graph => {
1187
1249
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
1188
1250
  const edgeRows = Array.isArray(graph.edges) ? graph.edges : []
1251
+ const densityScale = layoutDensityScaleForNodeCount(nodeRows.length)
1189
1252
  const nodes = nodeRows.map(node => {
1190
1253
  if (Array.isArray(node)) {
1191
1254
  const [id, title, x, y, group, segment] = node
@@ -1196,8 +1259,8 @@ const createLayout = graph => {
1196
1259
  tags: [],
1197
1260
  group: typeof group === 'string' ? group : 'root',
1198
1261
  segment: typeof segment === 'string' ? segment : 'root',
1199
- x: Number.isFinite(x) ? x : 0,
1200
- y: Number.isFinite(y) ? y : 0,
1262
+ x: Number.isFinite(x) ? x * densityScale : 0,
1263
+ y: Number.isFinite(y) ? y * densityScale : 0,
1201
1264
  vx: 0,
1202
1265
  vy: 0
1203
1266
  }
@@ -1207,8 +1270,8 @@ const createLayout = graph => {
1207
1270
  ...node,
1208
1271
  path: typeof node.path === 'string' ? node.path : '',
1209
1272
  tags: Array.isArray(node.tags) ? node.tags : [],
1210
- x: Number.isFinite(node.x) ? node.x : 0,
1211
- y: Number.isFinite(node.y) ? node.y : 0,
1273
+ x: Number.isFinite(node.x) ? node.x * densityScale : 0,
1274
+ y: Number.isFinite(node.y) ? node.y * densityScale : 0,
1212
1275
  vx: Number.isFinite(node.vx) ? node.vx : 0,
1213
1276
  vy: Number.isFinite(node.vy) ? node.vy : 0
1214
1277
  }
@@ -1672,9 +1735,15 @@ const computeRenderVisibility = () => {
1672
1735
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1673
1736
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
1674
1737
  const layeredNodes = selectLayeredNodesForScale(sourceNodes, sampleLimit)
1675
- const sampled = layeredNodes.length > sampleLimit
1676
- ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), layeredNodes)
1677
- : layeredNodes.slice(0, Math.min(layeredNodes.length, renderNodeBudget))
1738
+ const bridgeLimit = Math.max(24, Math.min(180, Math.floor(sampleLimit * 0.26)))
1739
+ const bridgedNodes = mergeUniqueNodes(
1740
+ layeredNodes,
1741
+ selectAccessBridgeNodes(sourceNodes, bridgeLimit),
1742
+ Math.min(renderNodeBudget, sampleLimit + bridgeLimit)
1743
+ )
1744
+ const sampled = bridgedNodes.length > sampleLimit
1745
+ ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), bridgedNodes)
1746
+ : bridgedNodes.slice(0, Math.min(bridgedNodes.length, renderNodeBudget))
1678
1747
  const sampledIds = new Set(sampled.map((node) => node.id))
1679
1748
  let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
1680
1749
  let sampledNodes = ensureHubNodesInRenderedSet(sampled)
@@ -2044,6 +2113,11 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
2044
2113
  }
2045
2114
  const worldX = (screenX - state.transform.x) / state.transform.scale
2046
2115
  const worldY = (screenY - state.transform.y) / state.transform.scale
2116
+ state.lastZoomFocus = {
2117
+ x: worldX,
2118
+ y: worldY,
2119
+ at: performance.now()
2120
+ }
2047
2121
  state.transform.scale = clampScale(nextScale)
2048
2122
  state.transform.x = clampTransformCoordinate(screenX - worldX * nextScale)
2049
2123
  state.transform.y = clampTransformCoordinate(screenY - worldY * nextScale)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.65",
3
+ "version": "0.1.0-beta.67",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",