@andespindola/brainlink 0.1.0-beta.97 → 0.1.0-beta.99

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
@@ -602,7 +602,7 @@ The graph UI shows:
602
602
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
603
603
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
604
604
  - 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
605
- - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use one recursive model where each visible level targets up to 999 non-hub nodes, starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (again up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy and stress-50k follow the same structure at different depths
605
+ - graph LOD progression: graphs up to 1000 notes render directly; larger graphs use one recursive model where each visible level targets up to 999 non-hub nodes, starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (again up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy and stress-50k follow the same structure at different depths; for massive graphs the first expansion starts much deeper in zoom and low-size child levels use slower easing so the view stays as one compact graph longer
606
606
 
607
607
  The server indexes before starting by default. Use `--no-index` to skip that step:
608
608
 
@@ -782,17 +782,20 @@ const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
782
782
  if (levelSizes.length <= 1) {
783
783
  return []
784
784
  }
785
- const maxScale = nodeCount > massiveGraphNodeThreshold
785
+ const isMassive = nodeCount > massiveGraphNodeThreshold
786
+ const maxScale = isMassive
786
787
  ? massiveEcosystemClusterScaleThreshold
787
788
  : ecosystemClusterScaleThreshold
788
- const startScale = 0.04
789
+ const startScale = isMassive ? 0.82 : 0.18
789
790
  const transitionCount = levelSizes.length - 1
790
791
  const usableScale = Math.max(0.08, maxScale - startScale)
791
792
  const step = usableScale / transitionCount
793
+ const stride = isMassive ? 0.9 : 0.78
794
+ const overlap = isMassive ? 1.28 : 1.75
792
795
  const levels = []
793
796
  for (let index = 0; index < transitionCount; index += 1) {
794
- const start = startScale + step * index * 0.72
795
- const end = Math.min(maxScale, start + step * 1.85)
797
+ const start = startScale + step * index * stride
798
+ const end = Math.min(maxScale, start + step * overlap)
796
799
  levels.push({
797
800
  parentSize: levelSizes[index],
798
801
  childSize: levelSizes[index + 1],
@@ -936,6 +939,10 @@ const filterEcosystemClustersByViewport = (clusters, viewport) => {
936
939
  }
937
940
 
938
941
  const ecosystemFocusPoint = () => {
942
+ const cursorPoint = cursorWorldPoint()
943
+ if (cursorPoint) {
944
+ return cursorPoint
945
+ }
939
946
  const now = performance.now()
940
947
  if (now - state.lastZoomFocus.at <= 1800) {
941
948
  return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
@@ -947,7 +954,11 @@ const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
947
954
  clusters
948
955
  .map(cluster => ({
949
956
  cluster,
950
- distance: Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y)
957
+ distance: Math.max(
958
+ 0,
959
+ Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y) -
960
+ clusterRadiusPx(cluster) / Math.max(state.transform.scale, 0.0001)
961
+ )
951
962
  }))
952
963
  .sort((left, right) => left.distance - right.distance)
953
964
  .slice(0, limit)
@@ -962,7 +973,8 @@ const zoomProgress = (scale, start, end) =>
962
973
  smoothStep((scale - start) / Math.max(end - start, 0.0001))
963
974
 
964
975
  const semanticZoomSpread = (progress, childSize) => {
965
- const curve = Math.pow(progress, 4.2)
976
+ const spreadExponent = childSize <= Math.ceil(ecosystemLevelNodeCap / 12) ? 5.6 : 4.2
977
+ const curve = Math.pow(progress, spreadExponent)
966
978
  if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
967
979
  return 0.12 + curve * 0.88
968
980
  }
@@ -970,7 +982,8 @@ const semanticZoomSpread = (progress, childSize) => {
970
982
  }
971
983
 
972
984
  const opacityForProgress = (progress, childSize) => {
973
- const eased = Math.pow(progress, 2.1)
985
+ const opacityExponent = childSize <= Math.ceil(ecosystemLevelNodeCap / 12) ? 2.8 : 2.1
986
+ const eased = Math.pow(progress, opacityExponent)
974
987
  if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
975
988
  return 0.22 + eased * 0.78
976
989
  }
@@ -1293,6 +1306,29 @@ const viewportCenterWorldPoint = () => {
1293
1306
  }
1294
1307
  }
1295
1308
 
1309
+ const screenToWorldPoint = (screenX, screenY) => ({
1310
+ x: (screenX - state.transform.x) / state.transform.scale,
1311
+ y: (screenY - state.transform.y) / state.transform.scale
1312
+ })
1313
+
1314
+ const cursorWorldPoint = () => {
1315
+ if (!state.cursor.inCanvas) {
1316
+ return null
1317
+ }
1318
+ const rect = canvas.getBoundingClientRect()
1319
+ const screenX = state.cursor.x - rect.left
1320
+ const screenY = state.cursor.y - rect.top
1321
+ const width = Math.max(rect.width, 320)
1322
+ const height = Math.max(rect.height, 320)
1323
+ if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) {
1324
+ return null
1325
+ }
1326
+ if (screenX < 0 || screenX > width || screenY < 0 || screenY > height) {
1327
+ return null
1328
+ }
1329
+ return screenToWorldPoint(screenX, screenY)
1330
+ }
1331
+
1296
1332
  const visibilityScaleBucket = (scale) => {
1297
1333
  const safeScale = Math.max(zoomRange.min, scale)
1298
1334
  if (safeScale < 0.01) return Math.round(safeScale * 300_000)
@@ -1348,11 +1384,12 @@ const selectStableSampleNodes = (sourceNodes, limit) => {
1348
1384
  }
1349
1385
 
1350
1386
  const now = performance.now()
1387
+ const cursorPoint = cursorWorldPoint()
1351
1388
  const recentZoomFocus =
1352
1389
  now - state.lastZoomFocus.at <= 1500
1353
1390
  ? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
1354
1391
  : null
1355
- const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
1392
+ const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
1356
1393
  const previousIds = new Set(state.renderNodes.map((node) => node.id))
1357
1394
  const preferAnchorDistance = state.visibleNodes.length > massiveGraphNodeThreshold && state.transform.scale >= 0.28
1358
1395
 
@@ -1386,11 +1423,12 @@ const selectAccessBridgeNodes = (sourceNodes, limit) => {
1386
1423
  }
1387
1424
 
1388
1425
  const now = performance.now()
1426
+ const cursorPoint = cursorWorldPoint()
1389
1427
  const recentZoomFocus =
1390
1428
  now - state.lastZoomFocus.at <= 1200
1391
1429
  ? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
1392
1430
  : null
1393
- const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
1431
+ const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
1394
1432
  return [...sourceNodes]
1395
1433
  .sort((left, right) => {
1396
1434
  const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
@@ -2468,10 +2506,7 @@ const tick = delta => {
2468
2506
 
2469
2507
  const worldPoint = event => {
2470
2508
  const rect = canvas.getBoundingClientRect()
2471
- return {
2472
- x: (event.clientX - rect.left - state.transform.x) / state.transform.scale,
2473
- y: (event.clientY - rect.top - state.transform.y) / state.transform.scale
2474
- }
2509
+ return screenToWorldPoint(event.clientX - rect.left, event.clientY - rect.top)
2475
2510
  }
2476
2511
 
2477
2512
  const connectedNodeIdsFor = (nodeId) => {
@@ -3218,8 +3253,9 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
3218
3253
  if (nextScale === state.transform.scale) {
3219
3254
  return
3220
3255
  }
3221
- const worldX = (screenX - state.transform.x) / state.transform.scale
3222
- const worldY = (screenY - state.transform.y) / state.transform.scale
3256
+ const worldPointAtCursor = screenToWorldPoint(screenX, screenY)
3257
+ const worldX = worldPointAtCursor.x
3258
+ const worldY = worldPointAtCursor.y
3223
3259
  state.lastZoomFocus = {
3224
3260
  x: worldX,
3225
3261
  y: worldY,
@@ -3266,6 +3302,7 @@ const handleWheelZoom = event => {
3266
3302
  const rawCursorY = Number.isFinite(event.offsetY) ? event.offsetY : event.clientY - rect.top
3267
3303
  const cursorX = Math.max(0, Math.min(Math.max(rect.width, 320), rawCursorX))
3268
3304
  const cursorY = Math.max(0, Math.min(Math.max(rect.height, 320), rawCursorY))
3305
+ state.cursor = { x: event.clientX, y: event.clientY, inCanvas: true }
3269
3306
  const factor = wheelZoomFactor(event)
3270
3307
 
3271
3308
  if (!Number.isFinite(factor) || factor <= 0 || factor === 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.97",
3
+ "version": "0.1.0-beta.99",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",