@andespindola/brainlink 0.1.0-beta.46 → 0.1.0-beta.48

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.
@@ -7,6 +7,8 @@ const renderNodeBudget = 900
7
7
  const renderEdgeBudget = 2400
8
8
  const clusterActivationNodeThreshold = 600
9
9
  const clusterZoomThreshold = 0.18
10
+ const macroGalaxyZoomThreshold = 0.012
11
+ const massiveAutoFitMacroScale = 0.006
10
12
  const clusterCellPixelSize = 64
11
13
  const minNodePixelRadius = 2.3
12
14
  const viewportPaddingPx = 280
@@ -45,6 +47,8 @@ const state = {
45
47
  visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
46
48
  visibleEdgeByNode: new Map(),
47
49
  overviewClusters: [],
50
+ macroCenter: { x: 0, y: 0 },
51
+ macroRepresentative: null,
48
52
  filterWorker: null,
49
53
  filterReady: false,
50
54
  lastHoverHitAt: 0
@@ -245,6 +249,26 @@ const filteredNodes = () => {
245
249
  return withPersistentHubNodes(localFilteredNodes(query))
246
250
  }
247
251
 
252
+ const resolveMacroRepresentative = (nodes) => {
253
+ if (nodes.length === 0) {
254
+ return null
255
+ }
256
+
257
+ let best = nodes[0]
258
+ let bestDegree = state.nodeDegrees.get(best.id) ?? 0
259
+
260
+ for (let index = 1; index < nodes.length; index += 1) {
261
+ const node = nodes[index]
262
+ const degree = state.nodeDegrees.get(node.id) ?? 0
263
+ if (degree > bestDegree) {
264
+ best = node
265
+ bestDegree = degree
266
+ }
267
+ }
268
+
269
+ return best
270
+ }
271
+
248
272
  const recomputeVisibility = () => {
249
273
  const nodes = filteredNodes()
250
274
  const ids = new Set(nodes.map(node => node.id))
@@ -260,6 +284,14 @@ const recomputeVisibility = () => {
260
284
  state.visibleNodeSpatial = createSpatialIndex(nodes)
261
285
  state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
262
286
  state.overviewClusters = nodes.length > massiveGraphNodeThreshold ? buildOverviewClusters(nodes) : []
287
+ const bounds = graphBounds(nodes)
288
+ state.macroCenter = bounds
289
+ ? {
290
+ x: (bounds.minX + bounds.maxX) / 2,
291
+ y: (bounds.minY + bounds.maxY) / 2
292
+ }
293
+ : { x: 0, y: 0 }
294
+ state.macroRepresentative = resolveMacroRepresentative(nodes)
263
295
  markRenderDirty()
264
296
  }
265
297
 
@@ -500,17 +532,17 @@ const fallbackViewportNodes = () => {
500
532
  return nodes
501
533
  }
502
534
 
503
- const sampleVisibleNodes = (limit = renderNodeBudget) => {
504
- if (state.visibleNodes.length === 0 || limit <= 0) {
535
+ const sampleVisibleNodes = (limit = renderNodeBudget, sourceNodes = state.visibleNodes) => {
536
+ if (sourceNodes.length === 0 || limit <= 0) {
505
537
  return []
506
538
  }
507
539
 
508
540
  const nodes = []
509
- const maxNodes = Math.min(Math.max(limit, 1), state.visibleNodes.length)
510
- const step = Math.max(1, Math.ceil(state.visibleNodes.length / maxNodes))
541
+ const maxNodes = Math.min(Math.max(limit, 1), sourceNodes.length)
542
+ const step = Math.max(1, Math.ceil(sourceNodes.length / maxNodes))
511
543
 
512
- for (let index = 0; index < state.visibleNodes.length && nodes.length < maxNodes; index += step) {
513
- nodes.push(state.visibleNodes[index])
544
+ for (let index = 0; index < sourceNodes.length && nodes.length < maxNodes; index += step) {
545
+ nodes.push(sourceNodes[index])
514
546
  }
515
547
 
516
548
  if (state.selected && !nodes.find(node => node.id === state.selected.id)) {
@@ -607,7 +639,10 @@ const fitView = (options = { useFiltered: true }) => {
607
639
  const fitScale = Math.min(scaleX, scaleY)
608
640
  const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
609
641
  const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
610
- const scale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
642
+ const baselineScale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
643
+ const scale = nodes.length > massiveGraphNodeThreshold
644
+ ? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
645
+ : baselineScale
611
646
  const centerX = (bounds.minX + bounds.maxX) / 2
612
647
  const centerY = (bounds.minY + bounds.maxY) / 2
613
648
 
@@ -981,14 +1016,34 @@ const computeRenderVisibility = () => {
981
1016
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
982
1017
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
983
1018
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1019
+ if (state.transform.scale <= macroGalaxyZoomThreshold) {
1020
+ const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
1021
+ if (representative) {
1022
+ state.renderClusters = [
1023
+ {
1024
+ id: 'macro-galaxy',
1025
+ x: state.macroCenter.x,
1026
+ y: state.macroCenter.y,
1027
+ count: sourceNodes.length,
1028
+ representative
1029
+ }
1030
+ ]
1031
+ state.renderNodes = [representative]
1032
+ } else {
1033
+ state.renderClusters = []
1034
+ state.renderNodes = []
1035
+ }
1036
+ state.renderEdges = []
1037
+ return
1038
+ }
984
1039
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
985
1040
  const sampled = sourceNodes.length > sampleLimit
986
- ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget))
1041
+ ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), sourceNodes)
987
1042
  : sourceNodes.slice(0, Math.min(sourceNodes.length, renderNodeBudget))
988
1043
  const sampledIds = new Set(sampled.map((node) => node.id))
989
1044
  state.renderClusters = []
990
1045
  state.renderNodes = sampled
991
- state.renderEdges = state.transform.scale >= 0.12 ? collectVisibleEdgesForNodes(sampledIds) : []
1046
+ state.renderEdges = state.transform.scale >= 0.1 ? collectVisibleEdgesForNodes(sampledIds) : []
992
1047
  return
993
1048
  }
994
1049
 
@@ -1143,9 +1198,15 @@ const render = now => {
1143
1198
  } else {
1144
1199
  state.offscreenFrameCount = 0
1145
1200
  }
1201
+ const minimumEdgeScale =
1202
+ state.nodes.length > massiveGraphNodeThreshold
1203
+ ? 0.1
1204
+ : state.nodes.length > largeGraphNodeThreshold
1205
+ ? 0.16
1206
+ : 0
1146
1207
  const drawEdges =
1147
1208
  state.renderClusters.length === 0 &&
1148
- !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
1209
+ state.transform.scale >= minimumEdgeScale
1149
1210
  if (drawEdges) {
1150
1211
  state.renderEdges.forEach(edge => {
1151
1212
  const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
@@ -1161,19 +1222,22 @@ const render = now => {
1161
1222
  if (state.renderClusters.length > 0) {
1162
1223
  const safeScale = Math.max(state.transform.scale, 0.0001)
1163
1224
  state.renderClusters.forEach(cluster => {
1164
- const radiusPx = Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
1225
+ const isMacro = cluster.id === 'macro-galaxy'
1226
+ const radiusPx = isMacro
1227
+ ? 10
1228
+ : Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
1165
1229
  const radius = radiusPx / safeScale
1166
- const haloRadius = (radiusPx + 4) / safeScale
1230
+ const haloRadius = (radiusPx + (isMacro ? 8 : 4)) / safeScale
1167
1231
  ctx.beginPath()
1168
1232
  ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
1169
- ctx.fillStyle = graphTheme.nodeHalo
1233
+ ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
1170
1234
  ctx.fill()
1171
1235
  ctx.beginPath()
1172
1236
  ctx.arc(cluster.x, cluster.y, radius, 0, Math.PI * 2)
1173
- ctx.fillStyle = graphTheme.node
1237
+ ctx.fillStyle = isMacro ? '#f3f7fb' : graphTheme.node
1174
1238
  ctx.fill()
1175
1239
  ctx.lineWidth = 1.4 / safeScale
1176
- ctx.strokeStyle = graphTheme.nodeStroke
1240
+ ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
1177
1241
  ctx.stroke()
1178
1242
  // Keep cluster markers minimal and faster to draw on large graphs.
1179
1243
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.46",
3
+ "version": "0.1.0-beta.48",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",