@andespindola/brainlink 0.1.0-beta.61 → 0.1.0-beta.63

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.
@@ -38,7 +38,7 @@ export const createClientHtml = () => `<!doctype html>
38
38
  <div class="toolbar" aria-label="Graph controls">
39
39
  <button id="zoomIn" type="button" title="Zoom in">+</button>
40
40
  <button id="zoomOut" type="button" title="Zoom out">-</button>
41
- <button id="fit" type="button" title="Fit visible nodes">◎</button>
41
+ <button id="fit" type="button" title="Focus central hub">◎</button>
42
42
  <button id="reset" type="button" title="Reset view">⌂</button>
43
43
  </div>
44
44
  </div>
@@ -23,6 +23,8 @@ const meshEdgeScaleThreshold = 0.09
23
23
  const meshEdgeMinBudget = 140
24
24
  const meshEdgeMaxBudget = 1400
25
25
  const layeredCoreScaleThreshold = 0.55
26
+ const dragNeighborhoodMaxAffected = 180
27
+ const dragSettleRounds = 3
26
28
  const state = {
27
29
  graph: { nodes: [], edges: [] },
28
30
  nodes: [],
@@ -569,13 +571,12 @@ const nodeBudgetForScale = (scale) => {
569
571
  }
570
572
 
571
573
  const layerWindowForScale = (scale) => {
572
- if (scale < 0.08) return { inner: 0.78, outer: 1 }
573
- if (scale < 0.14) return { inner: 0.62, outer: 0.9 }
574
- if (scale < 0.24) return { inner: 0.46, outer: 0.74 }
575
- if (scale < 0.36) return { inner: 0.3, outer: 0.58 }
576
- if (scale < layeredCoreScaleThreshold) return { inner: 0.16, outer: 0.42 }
577
- if (scale < 0.9) return { inner: 0.06, outer: 0.26 }
578
- return { inner: 0, outer: 0.14 }
574
+ const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
575
+ const outer = Math.max(0.14, 1 - normalized * 0.86)
576
+ const band = Math.max(0.14, 0.26 - normalized * 0.12)
577
+ const inner = Math.max(0, outer - band)
578
+
579
+ return { inner, outer }
579
580
  }
580
581
 
581
582
  const selectLayeredNodesForScale = (sourceNodes) => {
@@ -1141,6 +1142,27 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
1141
1142
 
1142
1143
  const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
1143
1144
 
1145
+ const focusPrimaryHub = () => {
1146
+ const hub = state.primaryHub
1147
+ if (!hub) {
1148
+ fitView({ useFiltered: true, macro: false, preferHubCenter: true })
1149
+ return
1150
+ }
1151
+
1152
+ const rect = canvas.getBoundingClientRect()
1153
+ const width = Math.max(rect.width, 320)
1154
+ const height = Math.max(rect.height, 320)
1155
+ const targetScale = clampScale(Math.max(0.78, state.transform.scale))
1156
+
1157
+ state.transform = {
1158
+ x: clampTransformCoordinate(width / 2 - hub.x * targetScale),
1159
+ y: clampTransformCoordinate(height / 2 - hub.y * targetScale),
1160
+ scale: targetScale
1161
+ }
1162
+ state.offscreenFrameCount = 0
1163
+ markRenderDirty()
1164
+ }
1165
+
1144
1166
  const createLayout = graph => {
1145
1167
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
1146
1168
  const edgeRows = Array.isArray(graph.edges) ? graph.edges : []
@@ -1354,6 +1376,104 @@ const worldPoint = event => {
1354
1376
  }
1355
1377
  }
1356
1378
 
1379
+ const connectedNodeIdsFor = (nodeId) => {
1380
+ const edges = state.visibleEdgeByNode.get(nodeId) ?? []
1381
+ const ids = new Set()
1382
+
1383
+ for (let index = 0; index < edges.length; index += 1) {
1384
+ const edge = edges[index]
1385
+ if (!edge.target) continue
1386
+ if (edge.source === nodeId) {
1387
+ ids.add(edge.target)
1388
+ } else if (edge.target === nodeId) {
1389
+ ids.add(edge.source)
1390
+ }
1391
+ }
1392
+
1393
+ return ids
1394
+ }
1395
+
1396
+ const applyDragNeighborhoodAdjustment = (dragNode, deltaX, deltaY) => {
1397
+ if (!dragNode) return
1398
+ if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) return
1399
+ if (Math.abs(deltaX) + Math.abs(deltaY) <= 0.001) return
1400
+
1401
+ const scale = Math.max(state.transform.scale, 0.0001)
1402
+ const influenceRadius = Math.max(220, Math.min(920, 440 / scale))
1403
+ const influenceRadiusSquared = influenceRadius * influenceRadius
1404
+ const connectedIds = connectedNodeIdsFor(dragNode.id)
1405
+ const candidates = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
1406
+ let adjusted = 0
1407
+
1408
+ for (let index = 0; index < candidates.length && adjusted < dragNeighborhoodMaxAffected; index += 1) {
1409
+ const node = candidates[index]
1410
+ if (node.id === dragNode.id) continue
1411
+
1412
+ const isConnected = connectedIds.has(node.id)
1413
+ const dx = node.x - dragNode.x
1414
+ const dy = node.y - dragNode.y
1415
+ const distanceSquared = dx * dx + dy * dy
1416
+ const withinRadius = distanceSquared <= influenceRadiusSquared
1417
+ if (!isConnected && !withinRadius) continue
1418
+
1419
+ const distance = Math.max(Math.sqrt(distanceSquared), 0.0001)
1420
+ const proximity = withinRadius ? 1 - (distance / influenceRadius) : 0
1421
+ const coupledStrength = isConnected ? 0.28 : 0.12
1422
+ const influence = Math.min(0.46, coupledStrength + proximity * 0.34)
1423
+ node.x += deltaX * influence
1424
+ node.y += deltaY * influence
1425
+ node.vx = (Number.isFinite(node.vx) ? node.vx : 0) + deltaX * influence * 0.06
1426
+ node.vy = (Number.isFinite(node.vy) ? node.vy : 0) + deltaY * influence * 0.06
1427
+ adjusted += 1
1428
+ }
1429
+ }
1430
+
1431
+ const settleNeighborhoodAroundNode = (dragNode) => {
1432
+ if (!dragNode) return
1433
+
1434
+ const scale = Math.max(state.transform.scale, 0.0001)
1435
+ const settleRadius = Math.max(240, Math.min(980, 520 / scale))
1436
+ const settleRadiusSquared = settleRadius * settleRadius
1437
+ const connectedIds = connectedNodeIdsFor(dragNode.id)
1438
+ const candidates = (state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes)
1439
+ .filter((node) => {
1440
+ if (node.id === dragNode.id) return true
1441
+ const dx = node.x - dragNode.x
1442
+ const dy = node.y - dragNode.y
1443
+ const distanceSquared = dx * dx + dy * dy
1444
+ return connectedIds.has(node.id) || distanceSquared <= settleRadiusSquared
1445
+ })
1446
+ .slice(0, dragNeighborhoodMaxAffected)
1447
+
1448
+ if (candidates.length <= 1) return
1449
+
1450
+ for (let round = 0; round < dragSettleRounds; round += 1) {
1451
+ for (let leftIndex = 0; leftIndex < candidates.length; leftIndex += 1) {
1452
+ const left = candidates[leftIndex]
1453
+ for (let rightIndex = leftIndex + 1; rightIndex < candidates.length; rightIndex += 1) {
1454
+ const right = candidates[rightIndex]
1455
+ const dx = right.x - left.x
1456
+ const dy = right.y - left.y
1457
+ const distance = Math.max(Math.hypot(dx, dy), 0.001)
1458
+ const minDistance = baseNodeRadius(left) + baseNodeRadius(right) + 10
1459
+ if (distance >= minDistance) continue
1460
+
1461
+ const push = (minDistance - distance) * 0.36
1462
+ const ux = dx / distance
1463
+ const uy = dy / distance
1464
+ if (left.id !== dragNode.id) {
1465
+ left.x -= ux * push
1466
+ left.y -= uy * push
1467
+ }
1468
+ if (right.id !== dragNode.id) {
1469
+ right.x += ux * push
1470
+ right.y += uy * push
1471
+ }
1472
+ }
1473
+ }
1474
+ }
1475
+ }
1476
+
1357
1477
  const hitNode = point => {
1358
1478
  computeRenderVisibility()
1359
1479
  if (state.renderClusters.length > 0) {
@@ -1920,8 +2040,8 @@ const wheelZoomFactor = event => {
1920
2040
  return 1
1921
2041
  }
1922
2042
 
1923
- const baseStep = Math.max(0.06, Math.min(0.45, absoluteDelta / 480))
1924
- const adjustedStep = baseStep * (isModifierZoom ? 1.4 : 1)
2043
+ const baseStep = Math.max(0.03, Math.min(0.2, absoluteDelta / 680))
2044
+ const adjustedStep = baseStep * (isModifierZoom ? 1.24 : 1)
1925
2045
 
1926
2046
  return event.deltaY < 0 ? 1 + adjustedStep : 1 / (1 + adjustedStep)
1927
2047
  }
@@ -1968,15 +2088,15 @@ const bindEvents = () => {
1968
2088
  })
1969
2089
  elements.zoomIn.addEventListener('click', () => {
1970
2090
  const rect = canvas.getBoundingClientRect()
1971
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.3)
2091
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14)
1972
2092
  })
1973
2093
  elements.zoomOut.addEventListener('click', () => {
1974
2094
  const rect = canvas.getBoundingClientRect()
1975
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.77)
2095
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88)
1976
2096
  })
1977
2097
  if (elements.fit) {
1978
2098
  elements.fit.addEventListener('click', () => {
1979
- fitView({ useFiltered: true })
2099
+ focusPrimaryHub()
1980
2100
  })
1981
2101
  }
1982
2102
  elements.reset.addEventListener('click', () => {
@@ -1996,7 +2116,7 @@ const bindEvents = () => {
1996
2116
  const rect = canvas.getBoundingClientRect()
1997
2117
  const cursorX = event.clientX - rect.left
1998
2118
  const cursorY = event.clientY - rect.top
1999
- zoomAtPoint(cursorX, cursorY, 1.25)
2119
+ zoomAtPoint(cursorX, cursorY, 1.12)
2000
2120
  })
2001
2121
  canvas.addEventListener('pointerdown', event => {
2002
2122
  const point = worldPoint(event)
@@ -2030,8 +2150,12 @@ const bindEvents = () => {
2030
2150
  state.pointer.y = event.clientY
2031
2151
  state.pointer.moved = state.pointer.moved || Math.abs(dx) + Math.abs(dy) > 3
2032
2152
  if (state.pointer.dragNode) {
2033
- state.pointer.dragNode.x = point.x
2034
- state.pointer.dragNode.y = point.y
2153
+ const dragNode = state.pointer.dragNode
2154
+ const previousX = dragNode.x
2155
+ const previousY = dragNode.y
2156
+ dragNode.x = point.x
2157
+ dragNode.y = point.y
2158
+ applyDragNeighborhoodAdjustment(dragNode, dragNode.x - previousX, dragNode.y - previousY)
2035
2159
  markRenderDirty()
2036
2160
  return
2037
2161
  }
@@ -2043,8 +2167,13 @@ const bindEvents = () => {
2043
2167
  markRenderDirty()
2044
2168
  })
2045
2169
  canvas.addEventListener('pointerup', event => {
2046
- if (state.pointer.dragNode && !state.pointer.moved) selectNode(state.pointer.dragNode, { openContent: true })
2047
- if (!state.pointer.dragNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
2170
+ const draggedNode = state.pointer.dragNode
2171
+ if (draggedNode && state.pointer.moved) {
2172
+ settleNeighborhoodAroundNode(draggedNode)
2173
+ markRenderDirty()
2174
+ }
2175
+ if (draggedNode && !state.pointer.moved) selectNode(draggedNode, { openContent: true })
2176
+ if (!draggedNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
2048
2177
  state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
2049
2178
  canvas.releasePointerCapture(event.pointerId)
2050
2179
  })
@@ -2061,14 +2190,14 @@ const bindEvents = () => {
2061
2190
  if (event.key === '+' || event.key === '=') {
2062
2191
  event.preventDefault()
2063
2192
  const rect = canvas.getBoundingClientRect()
2064
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.25)
2193
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.12)
2065
2194
  return
2066
2195
  }
2067
2196
 
2068
2197
  if (event.key === '-' || event.key === '_') {
2069
2198
  event.preventDefault()
2070
2199
  const rect = canvas.getBoundingClientRect()
2071
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.8)
2200
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.89)
2072
2201
  return
2073
2202
  }
2074
2203
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.61",
3
+ "version": "0.1.0-beta.63",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",