@andespindola/brainlink 0.1.0-beta.107 → 0.1.0-beta.109

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.
package/README.md CHANGED
@@ -601,6 +601,9 @@ The graph UI shows:
601
601
  - floating graph totals (notes, links, tags) below the Brainlink title
602
602
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
603
603
  - adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
604
+ - hierarchical hot-path optimizations reduce per-frame allocations and repeated scans during layered cluster expansion and edge projection
605
+ - hierarchical edge projection now caches hub membership and node-to-cluster resolution per frame to keep large recursive subgraph rendering smooth during continuous zoom and pan
606
+ - hierarchical projection now uses stronger perspective yaw/pitch and depth-based render ordering so layered subgraphs read as a true 3D field instead of a flat expansion
604
607
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
605
608
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
606
609
  - graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
@@ -35,6 +35,10 @@ const ecosystemDepthNear = 80
35
35
  const ecosystemDepthFar = 2600
36
36
  const ecosystemDepthPerspective = 620
37
37
  const ecosystemDepthTiltY = 0.24
38
+ const ecosystemDepthYaw = 0.22
39
+ const ecosystemDepthPitch = 0.16
40
+ const ecosystemDepthRadialGain = 0.09
41
+ const ecosystemDepthOrbitalMaxOffset = 160
38
42
  const ecosystemDepthMinScale = 0.24
39
43
  const ecosystemDepthOpacityFloor = 0.2
40
44
  const zoomRecoveryGuardMs = 4200
@@ -91,6 +95,8 @@ const state = {
91
95
  ecosystemClustersBySize: new Map(),
92
96
  ecosystemNodeClusterBySize: new Map(),
93
97
  ecosystemLevelSizes: [],
98
+ ecosystemLevelIndexBySize: new Map(),
99
+ ecosystemHubNodeIds: new Set(),
94
100
  ecosystemExpansionLevels: [],
95
101
  ecosystemBaseSize: ecosystemLevelNodeCap,
96
102
  ecosystemHubCluster: null,
@@ -617,6 +623,11 @@ const recomputeVisibility = () => {
617
623
  state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
618
624
  state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
619
625
  state.ecosystemLevelSizes = ecosystemGraph.levelSizes
626
+ state.ecosystemLevelIndexBySize = ecosystemGraph.levelSizes.reduce((map, size, index) => {
627
+ map.set(size, index)
628
+ return map
629
+ }, new Map())
630
+ state.ecosystemHubNodeIds = new Set(ecosystemGraph.hubCluster?.nodeIds ?? [])
620
631
  state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
621
632
  state.ecosystemBaseSize = ecosystemGraph.baseSize
622
633
  state.ecosystemHubCluster = ecosystemGraph.hubCluster
@@ -1105,10 +1116,18 @@ const expandFocusedClusters = (parentClusters, focusPoint, childSize, progress,
1105
1116
  ecosystemFocusedParentLimit
1106
1117
  ))
1107
1118
  const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
1108
- const visibleChildClusters = childClusters
1109
- .filter(cluster => expandedParentIds.has(cluster.parentId))
1110
- .map(cluster => spreadChildClusterFromParent(cluster, childSize, progress, spread))
1111
- .filter(cluster => isClusterInViewport(cluster, viewport))
1119
+ const visibleChildClusters = []
1120
+ for (let index = 0; index < childClusters.length; index += 1) {
1121
+ const cluster = childClusters[index]
1122
+ if (!expandedParentIds.has(cluster.parentId)) {
1123
+ continue
1124
+ }
1125
+ const spreadCluster = spreadChildClusterFromParent(cluster, childSize, progress, spread)
1126
+ if (!isClusterInViewport(spreadCluster, viewport)) {
1127
+ continue
1128
+ }
1129
+ visibleChildClusters.push(spreadCluster)
1130
+ }
1112
1131
 
1113
1132
  return {
1114
1133
  expandedParentIds,
@@ -1137,11 +1156,21 @@ const selectHierarchicalEcosystemClusters = viewport => {
1137
1156
  const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
1138
1157
  const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
1139
1158
  const visibleClusters = [...visibleBaseClusters]
1159
+ const clustersBySize = new Map()
1160
+ for (let index = 0; index < visibleBaseClusters.length; index += 1) {
1161
+ const cluster = visibleBaseClusters[index]
1162
+ const levelClusters = clustersBySize.get(cluster.size)
1163
+ if (levelClusters) {
1164
+ levelClusters.push(cluster)
1165
+ } else {
1166
+ clustersBySize.set(cluster.size, [cluster])
1167
+ }
1168
+ }
1140
1169
  const focusPoint = ecosystemFocusPoint()
1141
1170
 
1142
1171
  for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
1143
1172
  const level = state.ecosystemExpansionLevels[index]
1144
- const parentClusters = visibleClusters.filter(cluster => cluster.size === level.parentSize)
1173
+ const parentClusters = clustersBySize.get(level.parentSize) ?? []
1145
1174
  if (parentClusters.length === 0) {
1146
1175
  continue
1147
1176
  }
@@ -1154,18 +1183,20 @@ const selectHierarchicalEcosystemClusters = viewport => {
1154
1183
  const spread = semanticZoomSpread(progress, level.childSize)
1155
1184
  const expansion = expandFocusedClusters(parentClusters, focusPoint, level.childSize, progress, spread, viewport)
1156
1185
  visibleClusters.push(...expansion.childClusters)
1186
+ if (expansion.childClusters.length > 0) {
1187
+ const levelClusters = clustersBySize.get(level.childSize)
1188
+ if (levelClusters) {
1189
+ levelClusters.push(...expansion.childClusters)
1190
+ } else {
1191
+ clustersBySize.set(level.childSize, [...expansion.childClusters])
1192
+ }
1193
+ }
1157
1194
  }
1158
1195
 
1159
1196
  return [...hubClusters, ...visibleClusters]
1160
1197
  }
1161
1198
 
1162
- const ecosystemLevelIndexBySize = () => {
1163
- const indexBySize = new Map()
1164
- for (let index = 0; index < state.ecosystemLevelSizes.length; index += 1) {
1165
- indexBySize.set(state.ecosystemLevelSizes[index], index)
1166
- }
1167
- return indexBySize
1168
- }
1199
+ const ecosystemLevelIndexBySize = () => state.ecosystemLevelIndexBySize
1169
1200
 
1170
1201
  const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
1171
1202
  if (cluster.isHub) {
@@ -1180,12 +1211,23 @@ const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
1180
1211
 
1181
1212
  const projectEcosystemPoint = (x, y, depth, anchor) => {
1182
1213
  const safeDepth = Math.max(0, depth)
1183
- const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + safeDepth)
1184
- const verticalTilt = safeDepth * ecosystemDepthTiltY
1214
+ const dx = x - anchor.x
1215
+ const dy = y - anchor.y
1216
+ const yawSin = Math.sin(ecosystemDepthYaw)
1217
+ const yawCos = Math.cos(ecosystemDepthYaw)
1218
+ const pitchSin = Math.sin(ecosystemDepthPitch)
1219
+ const pitchCos = Math.cos(ecosystemDepthPitch)
1220
+ const rotatedX = dx * yawCos + safeDepth * yawSin
1221
+ const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
1222
+ const rotatedY = dy * pitchCos - rotatedZ * pitchSin
1223
+ const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
1224
+ const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + projectedDepth)
1225
+ const verticalTilt = projectedDepth * ecosystemDepthTiltY
1185
1226
  return {
1186
- x: anchor.x + (x - anchor.x) * factor,
1187
- y: anchor.y + (y - anchor.y) * factor - verticalTilt,
1188
- factor
1227
+ x: anchor.x + rotatedX * factor,
1228
+ y: anchor.y + rotatedY * factor - verticalTilt,
1229
+ factor,
1230
+ projectedDepth
1189
1231
  }
1190
1232
  }
1191
1233
 
@@ -1196,7 +1238,13 @@ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1196
1238
 
1197
1239
  for (let index = 0; index < clusters.length; index += 1) {
1198
1240
  const cluster = clusters[index]
1199
- const depth = ecosystemDepthForCluster(cluster, levelIndexMap)
1241
+ const baseDepth = ecosystemDepthForCluster(cluster, levelIndexMap)
1242
+ const radialDistance = Math.hypot(cluster.x - anchor.x, cluster.y - anchor.y)
1243
+ const radialOffset = cluster.isHub ? 0 : Math.min(320, radialDistance * ecosystemDepthRadialGain)
1244
+ const orbitalOffset = cluster.isHub
1245
+ ? 0
1246
+ : Math.sin(Math.atan2(cluster.y - anchor.y, cluster.x - anchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
1247
+ const depth = Math.max(0, baseDepth + radialOffset + orbitalOffset)
1200
1248
  const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, anchor)
1201
1249
  const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
1202
1250
  const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
@@ -1209,27 +1257,27 @@ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1209
1257
  x: projected.x,
1210
1258
  y: projected.y,
1211
1259
  lodOpacity: baseOpacity * depthOpacity,
1212
- depth,
1260
+ depth: projected.projectedDepth,
1213
1261
  depthScale
1214
1262
  }
1215
1263
  projectedClusters.push(projectedCluster)
1216
1264
  clusterById.set(projectedCluster.id, projectedCluster)
1217
1265
  }
1218
1266
 
1219
- const projectedEdges = edges
1220
- .map((edge) => {
1221
- const sourceCluster = clusterById.get(edge.sourceCluster.id)
1222
- const targetCluster = clusterById.get(edge.targetCluster.id)
1223
- if (!sourceCluster || !targetCluster) {
1224
- return null
1225
- }
1226
- return {
1227
- ...edge,
1228
- sourceCluster,
1229
- targetCluster
1230
- }
1267
+ const projectedEdges = []
1268
+ for (let index = 0; index < edges.length; index += 1) {
1269
+ const edge = edges[index]
1270
+ const sourceCluster = clusterById.get(edge.sourceCluster.id)
1271
+ const targetCluster = clusterById.get(edge.targetCluster.id)
1272
+ if (!sourceCluster || !targetCluster) {
1273
+ continue
1274
+ }
1275
+ projectedEdges.push({
1276
+ ...edge,
1277
+ sourceCluster,
1278
+ targetCluster
1231
1279
  })
1232
- .filter(Boolean)
1280
+ }
1233
1281
 
1234
1282
  return {
1235
1283
  clusters: projectedClusters,
@@ -1289,28 +1337,37 @@ const ecosystemEdgesForClusters = clusters => {
1289
1337
  const clusterById = new Map(edgeClusters.map(cluster => [cluster.id, cluster]))
1290
1338
  const clusterIds = new Set(clusterById.keys())
1291
1339
  const levelsBySize = []
1340
+ const seenSizes = new Set()
1292
1341
  for (let index = 0; index < edgeClusters.length; index += 1) {
1293
1342
  const cluster = edgeClusters[index]
1294
1343
  if (!cluster.size || cluster.isHub) continue
1295
- if (!levelsBySize.some(level => level.size === cluster.size)) {
1296
- levelsBySize.push({
1297
- size: cluster.size,
1298
- lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
1299
- })
1300
- }
1344
+ if (seenSizes.has(cluster.size)) continue
1345
+ seenSizes.add(cluster.size)
1346
+ levelsBySize.push({
1347
+ size: cluster.size,
1348
+ lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
1349
+ })
1301
1350
  }
1302
1351
  levelsBySize.sort((left, right) => left.size - right.size)
1352
+ const resolvedNodeClusterById = new Map()
1303
1353
  const resolveClusterForNode = nodeId => {
1304
- if (state.ecosystemHubCluster?.nodeIds.includes(nodeId) && clusterIds.has(state.ecosystemHubCluster.id)) {
1354
+ if (resolvedNodeClusterById.has(nodeId)) {
1355
+ return resolvedNodeClusterById.get(nodeId)
1356
+ }
1357
+ if (state.ecosystemHubNodeIds.has(nodeId) && state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)) {
1358
+ resolvedNodeClusterById.set(nodeId, state.ecosystemHubCluster)
1305
1359
  return state.ecosystemHubCluster
1306
1360
  }
1307
1361
  for (let index = 0; index < levelsBySize.length; index += 1) {
1308
1362
  const lookup = levelsBySize[index].lookup
1309
1363
  const cluster = lookup.get(nodeId)
1310
1364
  if (cluster && clusterIds.has(cluster.id)) {
1311
- return clusterById.get(cluster.id) ?? cluster
1365
+ const resolvedCluster = clusterById.get(cluster.id) ?? cluster
1366
+ resolvedNodeClusterById.set(nodeId, resolvedCluster)
1367
+ return resolvedCluster
1312
1368
  }
1313
1369
  }
1370
+ resolvedNodeClusterById.set(nodeId, null)
1314
1371
  return null
1315
1372
  }
1316
1373
 
@@ -2945,6 +3002,9 @@ const clusterRadiusPx = cluster => {
2945
3002
  const clusterOpacity = cluster =>
2946
3003
  Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
2947
3004
 
3005
+ const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
3006
+ const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
3007
+
2948
3008
  const worldViewportBounds = () => {
2949
3009
  const width = Math.max(state.viewport.width, 320)
2950
3010
  const height = Math.max(state.viewport.height, 320)
@@ -3364,6 +3424,8 @@ const render = now => {
3364
3424
  ctx.save()
3365
3425
  ctx.translate(state.transform.x, state.transform.y)
3366
3426
  ctx.scale(state.transform.scale, state.transform.scale)
3427
+ const orderedClusters = [...state.renderClusters]
3428
+ .sort((left, right) => clusterDepth(right) - clusterDepth(left))
3367
3429
  const safeScale = Math.max(state.transform.scale, 0.0001)
3368
3430
  if (state.renderClusterEdges.length > 0) {
3369
3431
  for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
@@ -3372,15 +3434,17 @@ const render = now => {
3372
3434
  if (edgeOpacity <= 0.01) {
3373
3435
  continue
3374
3436
  }
3437
+ const depthScale = Math.min(clusterDepthScale(edge.sourceCluster), clusterDepthScale(edge.targetCluster))
3438
+ const widthScale = 0.6 + depthScale * 0.9
3375
3439
  ctx.beginPath()
3376
3440
  ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
3377
3441
  ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
3378
- ctx.lineWidth = 1.2 / safeScale
3442
+ ctx.lineWidth = (1.2 * widthScale) / safeScale
3379
3443
  ctx.strokeStyle = 'rgba(153, 165, 181, ' + (edge.inferred ? 0.14 : 0.22) * edgeOpacity + ')'
3380
3444
  ctx.stroke()
3381
3445
  }
3382
3446
  }
3383
- state.renderClusters.forEach(cluster => {
3447
+ orderedClusters.forEach(cluster => {
3384
3448
  const isMacro = cluster.id === 'macro-galaxy'
3385
3449
  const isEcosystem = String(cluster.id).startsWith('ecosystem-')
3386
3450
  const isHub = Boolean(cluster.isHub)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.107",
3
+ "version": "0.1.0-beta.109",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",