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

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.
@@ -570,16 +570,17 @@ const nodeBudgetForScale = (scale) => {
570
570
  return renderNodeBudget
571
571
  }
572
572
 
573
- const layerWindowForScale = (scale) => {
573
+ const layerFocusForScale = (scale) => {
574
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)
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))
578
579
 
579
- return { inner, outer }
580
+ return { shellCenter, shellWidth, coreRadius, coreRatio }
580
581
  }
581
582
 
582
- const selectLayeredNodesForScale = (sourceNodes) => {
583
+ const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
583
584
  const hub = state.primaryHub
584
585
  if (!hub || sourceNodes.length <= 1200 || state.visibleNodes.length <= massiveGraphNodeThreshold) {
585
586
  return sourceNodes
@@ -598,39 +599,58 @@ const selectLayeredNodesForScale = (sourceNodes) => {
598
599
  return sourceNodes
599
600
  }
600
601
 
601
- const window = layerWindowForScale(state.transform.scale)
602
- const inner = window.inner * maxDistance
603
- const outer = window.outer * maxDistance
604
- const layered = distances
605
- .filter((item) => item.distance >= inner && item.distance <= outer)
606
- .map((item) => item.node)
607
-
608
- if (state.transform.scale >= layeredCoreScaleThreshold && !layered.some((node) => node.id === hub.id)) {
609
- layered.push(hub)
610
- }
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
611
611
 
612
- if (layered.length > 0) {
613
- return layered
614
- }
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)
615
622
 
616
- const midpoint = (window.inner + window.outer) / 2
617
- const fallback = [...distances]
623
+ const shellNodes = normalizedRows
618
624
  .sort((left, right) => {
619
- const leftNorm = left.distance / maxDistance
620
- const rightNorm = right.distance / maxDistance
621
- const leftDelta = Math.abs(leftNorm - midpoint)
622
- 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
623
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
624
634
  return left.node.id.localeCompare(right.node.id)
625
635
  })
626
- .slice(0, Math.min(900, sourceNodes.length))
636
+ .slice(0, shellTarget)
627
637
  .map((item) => item.node)
628
638
 
629
- if (state.transform.scale >= layeredCoreScaleThreshold && !fallback.some((node) => node.id === hub.id)) {
630
- 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)
631
645
  }
632
646
 
633
- 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
634
654
  }
635
655
 
636
656
  const edgeIdentityKey = edge => {
@@ -1650,8 +1670,8 @@ const computeRenderVisibility = () => {
1650
1670
  if (state.visibleNodes.length > massiveGraphNodeThreshold) {
1651
1671
  const viewportNodes = viewportNodesFromSpatialIndex(viewport)
1652
1672
  const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
1653
- const layeredNodes = selectLayeredNodesForScale(sourceNodes)
1654
1673
  const sampleLimit = nodeBudgetForScale(state.transform.scale)
1674
+ const layeredNodes = selectLayeredNodesForScale(sourceNodes, sampleLimit)
1655
1675
  const sampled = layeredNodes.length > sampleLimit
1656
1676
  ? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), layeredNodes)
1657
1677
  : layeredNodes.slice(0, Math.min(layeredNodes.length, renderNodeBudget))
@@ -2113,6 +2133,13 @@ const bindEvents = () => {
2113
2133
  })
2114
2134
  canvas.addEventListener('wheel', handleWheelZoom, { passive: false })
2115
2135
  canvas.addEventListener('dblclick', event => {
2136
+ const point = worldPoint(event)
2137
+ const node = hitNode(point)
2138
+ if (node) {
2139
+ selectNode(node, { openContent: true })
2140
+ return
2141
+ }
2142
+
2116
2143
  const rect = canvas.getBoundingClientRect()
2117
2144
  const cursorX = event.clientX - rect.left
2118
2145
  const cursorY = event.clientY - rect.top
@@ -2172,8 +2199,8 @@ const bindEvents = () => {
2172
2199
  settleNeighborhoodAroundNode(draggedNode)
2173
2200
  markRenderDirty()
2174
2201
  }
2175
- if (draggedNode && !state.pointer.moved) selectNode(draggedNode, { openContent: true })
2176
- if (!draggedNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
2202
+ if (draggedNode && !state.pointer.moved) selectNode(draggedNode, { openContent: false })
2203
+ if (!draggedNode && !state.pointer.moved) selectNode(state.hovered, { openContent: false })
2177
2204
  state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
2178
2205
  canvas.releasePointerCapture(event.pointerId)
2179
2206
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.63",
3
+ "version": "0.1.0-beta.65",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",