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

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>
@@ -570,17 +570,17 @@ const nodeBudgetForScale = (scale) => {
570
570
  return renderNodeBudget
571
571
  }
572
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 }
573
+ const layerFocusForScale = (scale) => {
574
+ const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
575
+ const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
576
+ const shellWidth = Math.max(0.16, 0.34 - normalized * 0.2)
577
+ const coreRadius = Math.max(0.06, 0.1 + normalized * 0.22)
578
+ const coreRatio = Math.max(0.2, Math.min(0.72, 0.24 + normalized * 0.48))
579
+
580
+ return { shellCenter, shellWidth, coreRadius, coreRatio }
581
581
  }
582
582
 
583
- const selectLayeredNodesForScale = (sourceNodes) => {
583
+ const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
584
584
  const hub = state.primaryHub
585
585
  if (!hub || sourceNodes.length <= 1200 || state.visibleNodes.length <= massiveGraphNodeThreshold) {
586
586
  return sourceNodes
@@ -599,39 +599,58 @@ const selectLayeredNodesForScale = (sourceNodes) => {
599
599
  return sourceNodes
600
600
  }
601
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
- }
602
+ const focus = layerFocusForScale(state.transform.scale)
603
+ const normalizedRows = distances.map((item) => ({
604
+ ...item,
605
+ normalized: item.distance / maxDistance
606
+ }))
607
+ const desired = Math.max(220, Math.min(sourceNodes.length, targetCount * 2))
608
+ const coreTarget = Math.max(36, Math.min(desired - 8, Math.floor(desired * focus.coreRatio)))
609
+ const shellTarget = Math.max(12, desired - coreTarget)
610
+ const shellHalf = focus.shellWidth / 2
612
611
 
613
- if (layered.length > 0) {
614
- return layered
615
- }
612
+ const coreNodes = normalizedRows
613
+ .filter((item) => item.normalized <= focus.coreRadius)
614
+ .sort((left, right) => {
615
+ const leftScore = state.nodeDegrees.get(left.node.id) ?? 0
616
+ const rightScore = state.nodeDegrees.get(right.node.id) ?? 0
617
+ if (leftScore !== rightScore) return rightScore - leftScore
618
+ return left.node.id.localeCompare(right.node.id)
619
+ })
620
+ .slice(0, coreTarget)
621
+ .map((item) => item.node)
616
622
 
617
- const midpoint = (window.inner + window.outer) / 2
618
- const fallback = [...distances]
623
+ const shellNodes = normalizedRows
619
624
  .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)
625
+ const leftDelta = Math.abs(left.normalized - focus.shellCenter)
626
+ const rightDelta = Math.abs(right.normalized - focus.shellCenter)
627
+ const leftInside = leftDelta <= shellHalf ? 0 : 1
628
+ const rightInside = rightDelta <= shellHalf ? 0 : 1
629
+ if (leftInside !== rightInside) return leftInside - rightInside
624
630
  if (leftDelta !== rightDelta) return leftDelta - rightDelta
631
+ const leftScore = state.nodeDegrees.get(left.node.id) ?? 0
632
+ const rightScore = state.nodeDegrees.get(right.node.id) ?? 0
633
+ if (leftScore !== rightScore) return rightScore - leftScore
625
634
  return left.node.id.localeCompare(right.node.id)
626
635
  })
627
- .slice(0, Math.min(900, sourceNodes.length))
636
+ .slice(0, shellTarget)
628
637
  .map((item) => item.node)
629
638
 
630
- if (state.transform.scale >= layeredCoreScaleThreshold && !fallback.some((node) => node.id === hub.id)) {
631
- fallback.push(hub)
639
+ const merged = []
640
+ const ids = new Set()
641
+ const pushUnique = (node) => {
642
+ if (!node || ids.has(node.id)) return
643
+ ids.add(node.id)
644
+ merged.push(node)
632
645
  }
633
646
 
634
- return fallback
647
+ if (state.transform.scale >= layeredCoreScaleThreshold) {
648
+ pushUnique(hub)
649
+ }
650
+ for (let index = 0; index < coreNodes.length; index += 1) pushUnique(coreNodes[index])
651
+ for (let index = 0; index < shellNodes.length; index += 1) pushUnique(shellNodes[index])
652
+
653
+ return merged.length > 0 ? merged : sourceNodes
635
654
  }
636
655
 
637
656
  const edgeIdentityKey = edge => {
@@ -1143,6 +1162,27 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
1143
1162
 
1144
1163
  const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
1145
1164
 
1165
+ const focusPrimaryHub = () => {
1166
+ const hub = state.primaryHub
1167
+ if (!hub) {
1168
+ fitView({ useFiltered: true, macro: false, preferHubCenter: true })
1169
+ return
1170
+ }
1171
+
1172
+ const rect = canvas.getBoundingClientRect()
1173
+ const width = Math.max(rect.width, 320)
1174
+ const height = Math.max(rect.height, 320)
1175
+ const targetScale = clampScale(Math.max(0.78, state.transform.scale))
1176
+
1177
+ state.transform = {
1178
+ x: clampTransformCoordinate(width / 2 - hub.x * targetScale),
1179
+ y: clampTransformCoordinate(height / 2 - hub.y * targetScale),
1180
+ scale: targetScale
1181
+ }
1182
+ state.offscreenFrameCount = 0
1183
+ markRenderDirty()
1184
+ }
1185
+
1146
1186
  const createLayout = graph => {
1147
1187
  const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
1148
1188
  const edgeRows = Array.isArray(graph.edges) ? graph.edges : []
@@ -1630,8 +1670,8 @@ const computeRenderVisibility = () => {
1630
1670
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
1631
1671
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1632
1672
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1633
- const layeredNodes = selectLayeredNodesForScale(sourceNodes)
1634
1673
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
1674
+ const layeredNodes = selectLayeredNodesForScale(sourceNodes, sampleLimit)
1635
1675
  const sampled = layeredNodes.length > sampleLimit
1636
1676
  ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), layeredNodes)
1637
1677
  : layeredNodes.slice(0, Math.min(layeredNodes.length, renderNodeBudget))
@@ -2020,8 +2060,8 @@ const wheelZoomFactor = event => {
2020
2060
  return 1
2021
2061
  }
2022
2062
 
2023
- const baseStep = Math.max(0.06, Math.min(0.45, absoluteDelta / 480))
2024
- const adjustedStep = baseStep * (isModifierZoom ? 1.4 : 1)
2063
+ const baseStep = Math.max(0.03, Math.min(0.2, absoluteDelta / 680))
2064
+ const adjustedStep = baseStep * (isModifierZoom ? 1.24 : 1)
2025
2065
 
2026
2066
  return event.deltaY < 0 ? 1 + adjustedStep : 1 / (1 + adjustedStep)
2027
2067
  }
@@ -2068,15 +2108,15 @@ const bindEvents = () => {
2068
2108
  })
2069
2109
  elements.zoomIn.addEventListener('click', () => {
2070
2110
  const rect = canvas.getBoundingClientRect()
2071
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.3)
2111
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14)
2072
2112
  })
2073
2113
  elements.zoomOut.addEventListener('click', () => {
2074
2114
  const rect = canvas.getBoundingClientRect()
2075
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.77)
2115
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88)
2076
2116
  })
2077
2117
  if (elements.fit) {
2078
2118
  elements.fit.addEventListener('click', () => {
2079
- fitView({ useFiltered: true })
2119
+ focusPrimaryHub()
2080
2120
  })
2081
2121
  }
2082
2122
  elements.reset.addEventListener('click', () => {
@@ -2096,7 +2136,7 @@ const bindEvents = () => {
2096
2136
  const rect = canvas.getBoundingClientRect()
2097
2137
  const cursorX = event.clientX - rect.left
2098
2138
  const cursorY = event.clientY - rect.top
2099
- zoomAtPoint(cursorX, cursorY, 1.25)
2139
+ zoomAtPoint(cursorX, cursorY, 1.12)
2100
2140
  })
2101
2141
  canvas.addEventListener('pointerdown', event => {
2102
2142
  const point = worldPoint(event)
@@ -2170,14 +2210,14 @@ const bindEvents = () => {
2170
2210
  if (event.key === '+' || event.key === '=') {
2171
2211
  event.preventDefault()
2172
2212
  const rect = canvas.getBoundingClientRect()
2173
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.25)
2213
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.12)
2174
2214
  return
2175
2215
  }
2176
2216
 
2177
2217
  if (event.key === '-' || event.key === '_') {
2178
2218
  event.preventDefault()
2179
2219
  const rect = canvas.getBoundingClientRect()
2180
- zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.8)
2220
+ zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.89)
2181
2221
  return
2182
2222
  }
2183
2223
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.62",
3
+ "version": "0.1.0-beta.64",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",