@andespindola/brainlink 0.1.0-beta.66 → 0.1.0-beta.68

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)
@@ -653,17 +654,11 @@ const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
653
654
  return merged.length > 0 ? merged : sourceNodes
654
655
  }
655
656
 
656
- const cursorWorldPoint = () => {
657
- if (!state.cursor.inCanvas) {
658
- return null
659
- }
660
-
661
- const rect = canvas.getBoundingClientRect()
662
- const screenX = state.cursor.x - rect.left
663
- const screenY = state.cursor.y - rect.top
657
+ const viewportCenterWorldPoint = () => {
658
+ const viewport = worldViewportBounds()
664
659
  return {
665
- x: (screenX - state.transform.x) / state.transform.scale,
666
- y: (screenY - state.transform.y) / state.transform.scale
660
+ x: (viewport.minX + viewport.maxX) / 2,
661
+ y: (viewport.minY + viewport.maxY) / 2
667
662
  }
668
663
  }
669
664
 
@@ -689,13 +684,49 @@ const mergeUniqueNodes = (leftNodes, rightNodes, limit) => {
689
684
  return merged
690
685
  }
691
686
 
687
+ const selectStableSampleNodes = (sourceNodes, limit) => {
688
+ if (sourceNodes.length <= limit) {
689
+ return sourceNodes
690
+ }
691
+
692
+ const now = performance.now()
693
+ const recentZoomFocus =
694
+ now - state.lastZoomFocus.at <= 1500
695
+ ? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
696
+ : null
697
+ const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
698
+ const previousIds = new Set(state.renderNodes.map((node) => node.id))
699
+
700
+ return [...sourceNodes]
701
+ .sort((left, right) => {
702
+ const leftWasVisible = previousIds.has(left.id) ? 1 : 0
703
+ const rightWasVisible = previousIds.has(right.id) ? 1 : 0
704
+ if (leftWasVisible !== rightWasVisible) return rightWasVisible - leftWasVisible
705
+
706
+ const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
707
+ const rightDistance = Math.hypot(right.x - anchor.x, right.y - anchor.y)
708
+ if (leftDistance !== rightDistance) return leftDistance - rightDistance
709
+
710
+ const leftDegree = state.nodeDegrees.get(left.id) ?? 0
711
+ const rightDegree = state.nodeDegrees.get(right.id) ?? 0
712
+ if (leftDegree !== rightDegree) return rightDegree - leftDegree
713
+
714
+ return left.id.localeCompare(right.id)
715
+ })
716
+ .slice(0, limit)
717
+ }
718
+
692
719
  const selectAccessBridgeNodes = (sourceNodes, limit) => {
693
720
  if (limit <= 0 || sourceNodes.length === 0) {
694
721
  return []
695
722
  }
696
723
 
697
- const cursor = cursorWorldPoint()
698
- const anchor = cursor ?? state.primaryHub ?? state.macroCenter ?? { x: 0, y: 0 }
724
+ const now = performance.now()
725
+ const recentZoomFocus =
726
+ now - state.lastZoomFocus.at <= 1200
727
+ ? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
728
+ : null
729
+ const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
699
730
  return [...sourceNodes]
700
731
  .sort((left, right) => {
701
732
  const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
@@ -1239,9 +1270,17 @@ const focusPrimaryHub = () => {
1239
1270
  markRenderDirty()
1240
1271
  }
1241
1272
 
1273
+ const layoutDensityScaleForNodeCount = (nodeCount) => {
1274
+ if (nodeCount > 50000) return 0.56
1275
+ if (nodeCount > 20000) return 0.64
1276
+ if (nodeCount > 6000) return 0.76
1277
+ return 1
1278
+ }
1279
+
1242
1280
  const createLayout = graph => {
1243
1281
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
1244
1282
  const edgeRows = Array.isArray(graph.edges) ? graph.edges : []
1283
+ const densityScale = layoutDensityScaleForNodeCount(nodeRows.length)
1245
1284
  const nodes = nodeRows.map(node => {
1246
1285
  if (Array.isArray(node)) {
1247
1286
  const [id, title, x, y, group, segment] = node
@@ -1252,8 +1291,8 @@ const createLayout = graph => {
1252
1291
  tags: [],
1253
1292
  group: typeof group === 'string' ? group : 'root',
1254
1293
  segment: typeof segment === 'string' ? segment : 'root',
1255
- x: Number.isFinite(x) ? x : 0,
1256
- y: Number.isFinite(y) ? y : 0,
1294
+ x: Number.isFinite(x) ? x * densityScale : 0,
1295
+ y: Number.isFinite(y) ? y * densityScale : 0,
1257
1296
  vx: 0,
1258
1297
  vy: 0
1259
1298
  }
@@ -1263,8 +1302,8 @@ const createLayout = graph => {
1263
1302
  ...node,
1264
1303
  path: typeof node.path === 'string' ? node.path : '',
1265
1304
  tags: Array.isArray(node.tags) ? node.tags : [],
1266
- x: Number.isFinite(node.x) ? node.x : 0,
1267
- y: Number.isFinite(node.y) ? node.y : 0,
1305
+ x: Number.isFinite(node.x) ? node.x * densityScale : 0,
1306
+ y: Number.isFinite(node.y) ? node.y * densityScale : 0,
1268
1307
  vx: Number.isFinite(node.vx) ? node.vx : 0,
1269
1308
  vy: Number.isFinite(node.vy) ? node.vy : 0
1270
1309
  }
@@ -1734,9 +1773,10 @@ const computeRenderVisibility = () => {
1734
1773
  selectAccessBridgeNodes(sourceNodes, bridgeLimit),
1735
1774
  Math.min(renderNodeBudget, sampleLimit + bridgeLimit)
1736
1775
  )
1737
- const sampled = bridgedNodes.length > sampleLimit
1738
- ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), bridgedNodes)
1739
- : bridgedNodes.slice(0, Math.min(bridgedNodes.length, renderNodeBudget))
1776
+ const sampled = selectStableSampleNodes(
1777
+ bridgedNodes,
1778
+ Math.min(sampleLimit, renderNodeBudget)
1779
+ )
1740
1780
  const sampledIds = new Set(sampled.map((node) => node.id))
1741
1781
  let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
1742
1782
  let sampledNodes = ensureHubNodesInRenderedSet(sampled)
@@ -2106,6 +2146,11 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
2106
2146
  }
2107
2147
  const worldX = (screenX - state.transform.x) / state.transform.scale
2108
2148
  const worldY = (screenY - state.transform.y) / state.transform.scale
2149
+ state.lastZoomFocus = {
2150
+ x: worldX,
2151
+ y: worldY,
2152
+ at: performance.now()
2153
+ }
2109
2154
  state.transform.scale = clampScale(nextScale)
2110
2155
  state.transform.x = clampTransformCoordinate(screenX - worldX * nextScale)
2111
2156
  state.transform.y = clampTransformCoordinate(screenY - worldY * nextScale)
@@ -2122,7 +2167,7 @@ const wheelZoomFactor = event => {
2122
2167
  return 1
2123
2168
  }
2124
2169
 
2125
- const baseStep = Math.max(0.03, Math.min(0.2, absoluteDelta / 680))
2170
+ const baseStep = Math.max(0.012, Math.min(0.11, absoluteDelta / 980))
2126
2171
  const adjustedStep = baseStep * (isModifierZoom ? 1.24 : 1)
2127
2172
 
2128
2173
  return event.deltaY < 0 ? 1 + adjustedStep : 1 / (1 + adjustedStep)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.66",
3
+ "version": "0.1.0-beta.68",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",