@andespindola/brainlink 0.1.0-beta.60 → 0.1.0-beta.62

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.
@@ -22,6 +22,9 @@ const zoomCapTargetViewportShare = 0.72
22
22
  const meshEdgeScaleThreshold = 0.09
23
23
  const meshEdgeMinBudget = 140
24
24
  const meshEdgeMaxBudget = 1400
25
+ const layeredCoreScaleThreshold = 0.55
26
+ const dragNeighborhoodMaxAffected = 180
27
+ const dragSettleRounds = 3
25
28
  const state = {
26
29
  graph: { nodes: [], edges: [] },
27
30
  nodes: [],
@@ -567,6 +570,70 @@ const nodeBudgetForScale = (scale) => {
567
570
  return renderNodeBudget
568
571
  }
569
572
 
573
+ const layerWindowForScale = (scale) => {
574
+ if (scale < 0.08) return { inner: 0.78, outer: 1 }
575
+ if (scale < 0.14) return { inner: 0.62, outer: 0.9 }
576
+ if (scale < 0.24) return { inner: 0.46, outer: 0.74 }
577
+ if (scale < 0.36) return { inner: 0.3, outer: 0.58 }
578
+ if (scale < layeredCoreScaleThreshold) return { inner: 0.16, outer: 0.42 }
579
+ if (scale < 0.9) return { inner: 0.06, outer: 0.26 }
580
+ return { inner: 0, outer: 0.14 }
581
+ }
582
+
583
+ const selectLayeredNodesForScale = (sourceNodes) => {
584
+ const hub = state.primaryHub
585
+ if (!hub || sourceNodes.length <= 1200 || state.visibleNodes.length <= massiveGraphNodeThreshold) {
586
+ return sourceNodes
587
+ }
588
+
589
+ let maxDistance = 0
590
+ const distances = sourceNodes.map((node) => {
591
+ const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
592
+ if (distance > maxDistance) {
593
+ maxDistance = distance
594
+ }
595
+ return { node, distance }
596
+ })
597
+
598
+ if (maxDistance <= 0.001) {
599
+ return sourceNodes
600
+ }
601
+
602
+ const window = layerWindowForScale(state.transform.scale)
603
+ const inner = window.inner * maxDistance
604
+ const outer = window.outer * maxDistance
605
+ const layered = distances
606
+ .filter((item) => item.distance >= inner && item.distance <= outer)
607
+ .map((item) => item.node)
608
+
609
+ if (state.transform.scale >= layeredCoreScaleThreshold && !layered.some((node) => node.id === hub.id)) {
610
+ layered.push(hub)
611
+ }
612
+
613
+ if (layered.length > 0) {
614
+ return layered
615
+ }
616
+
617
+ const midpoint = (window.inner + window.outer) / 2
618
+ const fallback = [...distances]
619
+ .sort((left, right) => {
620
+ const leftNorm = left.distance / maxDistance
621
+ const rightNorm = right.distance / maxDistance
622
+ const leftDelta = Math.abs(leftNorm - midpoint)
623
+ const rightDelta = Math.abs(rightNorm - midpoint)
624
+ if (leftDelta !== rightDelta) return leftDelta - rightDelta
625
+ return left.node.id.localeCompare(right.node.id)
626
+ })
627
+ .slice(0, Math.min(900, sourceNodes.length))
628
+ .map((item) => item.node)
629
+
630
+ if (state.transform.scale >= layeredCoreScaleThreshold && !fallback.some((node) => node.id === hub.id)) {
631
+ fallback.push(hub)
632
+ }
633
+
634
+ return fallback
635
+ }
636
+
570
637
  const edgeIdentityKey = edge => {
571
638
  if (!edge.target) return ''
572
639
  const pair = edge.source < edge.target
@@ -1289,6 +1356,104 @@ const worldPoint = event => {
1289
1356
  }
1290
1357
  }
1291
1358
 
1359
+ const connectedNodeIdsFor = (nodeId) => {
1360
+ const edges = state.visibleEdgeByNode.get(nodeId) ?? []
1361
+ const ids = new Set()
1362
+
1363
+ for (let index = 0; index < edges.length; index += 1) {
1364
+ const edge = edges[index]
1365
+ if (!edge.target) continue
1366
+ if (edge.source === nodeId) {
1367
+ ids.add(edge.target)
1368
+ } else if (edge.target === nodeId) {
1369
+ ids.add(edge.source)
1370
+ }
1371
+ }
1372
+
1373
+ return ids
1374
+ }
1375
+
1376
+ const applyDragNeighborhoodAdjustment = (dragNode, deltaX, deltaY) => {
1377
+ if (!dragNode) return
1378
+ if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) return
1379
+ if (Math.abs(deltaX) + Math.abs(deltaY) <= 0.001) return
1380
+
1381
+ const scale = Math.max(state.transform.scale, 0.0001)
1382
+ const influenceRadius = Math.max(220, Math.min(920, 440 / scale))
1383
+ const influenceRadiusSquared = influenceRadius * influenceRadius
1384
+ const connectedIds = connectedNodeIdsFor(dragNode.id)
1385
+ const candidates = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
1386
+ let adjusted = 0
1387
+
1388
+ for (let index = 0; index < candidates.length && adjusted < dragNeighborhoodMaxAffected; index += 1) {
1389
+ const node = candidates[index]
1390
+ if (node.id === dragNode.id) continue
1391
+
1392
+ const isConnected = connectedIds.has(node.id)
1393
+ const dx = node.x - dragNode.x
1394
+ const dy = node.y - dragNode.y
1395
+ const distanceSquared = dx * dx + dy * dy
1396
+ const withinRadius = distanceSquared <= influenceRadiusSquared
1397
+ if (!isConnected && !withinRadius) continue
1398
+
1399
+ const distance = Math.max(Math.sqrt(distanceSquared), 0.0001)
1400
+ const proximity = withinRadius ? 1 - (distance / influenceRadius) : 0
1401
+ const coupledStrength = isConnected ? 0.28 : 0.12
1402
+ const influence = Math.min(0.46, coupledStrength + proximity * 0.34)
1403
+ node.x += deltaX * influence
1404
+ node.y += deltaY * influence
1405
+ node.vx = (Number.isFinite(node.vx) ? node.vx : 0) + deltaX * influence * 0.06
1406
+ node.vy = (Number.isFinite(node.vy) ? node.vy : 0) + deltaY * influence * 0.06
1407
+ adjusted += 1
1408
+ }
1409
+ }
1410
+
1411
+ const settleNeighborhoodAroundNode = (dragNode) => {
1412
+ if (!dragNode) return
1413
+
1414
+ const scale = Math.max(state.transform.scale, 0.0001)
1415
+ const settleRadius = Math.max(240, Math.min(980, 520 / scale))
1416
+ const settleRadiusSquared = settleRadius * settleRadius
1417
+ const connectedIds = connectedNodeIdsFor(dragNode.id)
1418
+ const candidates = (state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes)
1419
+ .filter((node) => {
1420
+ if (node.id === dragNode.id) return true
1421
+ const dx = node.x - dragNode.x
1422
+ const dy = node.y - dragNode.y
1423
+ const distanceSquared = dx * dx + dy * dy
1424
+ return connectedIds.has(node.id) || distanceSquared <= settleRadiusSquared
1425
+ })
1426
+ .slice(0, dragNeighborhoodMaxAffected)
1427
+
1428
+ if (candidates.length <= 1) return
1429
+
1430
+ for (let round = 0; round < dragSettleRounds; round += 1) {
1431
+ for (let leftIndex = 0; leftIndex < candidates.length; leftIndex += 1) {
1432
+ const left = candidates[leftIndex]
1433
+ for (let rightIndex = leftIndex + 1; rightIndex < candidates.length; rightIndex += 1) {
1434
+ const right = candidates[rightIndex]
1435
+ const dx = right.x - left.x
1436
+ const dy = right.y - left.y
1437
+ const distance = Math.max(Math.hypot(dx, dy), 0.001)
1438
+ const minDistance = baseNodeRadius(left) + baseNodeRadius(right) + 10
1439
+ if (distance >= minDistance) continue
1440
+
1441
+ const push = (minDistance - distance) * 0.36
1442
+ const ux = dx / distance
1443
+ const uy = dy / distance
1444
+ if (left.id !== dragNode.id) {
1445
+ left.x -= ux * push
1446
+ left.y -= uy * push
1447
+ }
1448
+ if (right.id !== dragNode.id) {
1449
+ right.x += ux * push
1450
+ right.y += uy * push
1451
+ }
1452
+ }
1453
+ }
1454
+ }
1455
+ }
1456
+
1292
1457
  const hitNode = point => {
1293
1458
  computeRenderVisibility()
1294
1459
  if (state.renderClusters.length > 0) {
@@ -1465,10 +1630,11 @@ const computeRenderVisibility = () => {
1465
1630
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
1466
1631
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1467
1632
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1633
+ const layeredNodes = selectLayeredNodesForScale(sourceNodes)
1468
1634
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
1469
- const sampled = sourceNodes.length > sampleLimit
1470
- ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), sourceNodes)
1471
- : sourceNodes.slice(0, Math.min(sourceNodes.length, renderNodeBudget))
1635
+ const sampled = layeredNodes.length > sampleLimit
1636
+ ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), layeredNodes)
1637
+ : layeredNodes.slice(0, Math.min(layeredNodes.length, renderNodeBudget))
1472
1638
  const sampledIds = new Set(sampled.map((node) => node.id))
1473
1639
  let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
1474
1640
  let sampledNodes = ensureHubNodesInRenderedSet(sampled)
@@ -1964,8 +2130,12 @@ const bindEvents = () => {
1964
2130
  state.pointer.y = event.clientY
1965
2131
  state.pointer.moved = state.pointer.moved || Math.abs(dx) + Math.abs(dy) > 3
1966
2132
  if (state.pointer.dragNode) {
1967
- state.pointer.dragNode.x = point.x
1968
- state.pointer.dragNode.y = point.y
2133
+ const dragNode = state.pointer.dragNode
2134
+ const previousX = dragNode.x
2135
+ const previousY = dragNode.y
2136
+ dragNode.x = point.x
2137
+ dragNode.y = point.y
2138
+ applyDragNeighborhoodAdjustment(dragNode, dragNode.x - previousX, dragNode.y - previousY)
1969
2139
  markRenderDirty()
1970
2140
  return
1971
2141
  }
@@ -1977,8 +2147,13 @@ const bindEvents = () => {
1977
2147
  markRenderDirty()
1978
2148
  })
1979
2149
  canvas.addEventListener('pointerup', event => {
1980
- if (state.pointer.dragNode && !state.pointer.moved) selectNode(state.pointer.dragNode, { openContent: true })
1981
- if (!state.pointer.dragNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
2150
+ const draggedNode = state.pointer.dragNode
2151
+ if (draggedNode && state.pointer.moved) {
2152
+ settleNeighborhoodAroundNode(draggedNode)
2153
+ markRenderDirty()
2154
+ }
2155
+ if (draggedNode && !state.pointer.moved) selectNode(draggedNode, { openContent: true })
2156
+ if (!draggedNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
1982
2157
  state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
1983
2158
  canvas.releasePointerCapture(event.pointerId)
1984
2159
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.60",
3
+ "version": "0.1.0-beta.62",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",