@andespindola/brainlink 0.1.0-beta.109 → 0.1.0-beta.110

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
@@ -604,6 +604,7 @@ The graph UI shows:
604
604
  - hierarchical hot-path optimizations reduce per-frame allocations and repeated scans during layered cluster expansion and edge projection
605
605
  - hierarchical edge projection now caches hub membership and node-to-cluster resolution per frame to keep large recursive subgraph rendering smooth during continuous zoom and pan
606
606
  - hierarchical projection now uses stronger perspective yaw/pitch and depth-based render ordering so layered subgraphs read as a true 3D field instead of a flat expansion
607
+ - node rendering also applies depth projection cues on large visible sets (position, scale, opacity and edge depth weighting) so the graph keeps 3D perception when leaving cluster-only LOD layers
607
608
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
608
609
  - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
609
610
  - 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
@@ -41,6 +41,16 @@ const ecosystemDepthRadialGain = 0.09
41
41
  const ecosystemDepthOrbitalMaxOffset = 160
42
42
  const ecosystemDepthMinScale = 0.24
43
43
  const ecosystemDepthOpacityFloor = 0.2
44
+ const graphDepthNear = 40
45
+ const graphDepthFar = 560
46
+ const graphDepthPerspective = 760
47
+ const graphDepthYaw = 0.2
48
+ const graphDepthPitch = 0.12
49
+ const graphDepthRadialGain = 0.08
50
+ const graphDepthMinScale = 0.58
51
+ const graphDepthOpacityFloor = 0.42
52
+ const graphDepthEdgeOpacityFloor = 0.24
53
+ const graphDepthProjectionNodeThreshold = 120
44
54
  const zoomRecoveryGuardMs = 4200
45
55
  const zoomCapTargetViewportShare = 0.72
46
56
  const meshEdgeScaleThreshold = 0.09
@@ -67,6 +77,7 @@ const state = {
67
77
  renderEdges: [],
68
78
  renderClusters: [],
69
79
  renderClusterEdges: [],
80
+ renderNodeDepthProjectionById: new Map(),
70
81
  nodeDegrees: new Map(),
71
82
  selected: null,
72
83
  hovered: null,
@@ -1778,12 +1789,29 @@ const edgeOpacityForScale = (edge, scale) => {
1778
1789
  return 0.46
1779
1790
  }
1780
1791
 
1792
+ const edgeDepthOpacity = edge => {
1793
+ if (!shouldProjectRenderNodesInDepth()) {
1794
+ return 1
1795
+ }
1796
+ return Math.max(
1797
+ graphDepthEdgeOpacityFloor,
1798
+ Math.min(nodeRenderOpacity(edge.sourceNode), nodeRenderOpacity(edge.targetNode))
1799
+ )
1800
+ }
1801
+
1802
+ const edgeDepthScale = edge => {
1803
+ if (!shouldProjectRenderNodesInDepth()) {
1804
+ return 1
1805
+ }
1806
+ return Math.max(0.62, Math.min(1.18, Math.min(nodeRenderScale(edge.sourceNode), nodeRenderScale(edge.targetNode))))
1807
+ }
1808
+
1781
1809
  const edgeStrokeFor = (edge, selectedEdge) => {
1782
1810
  if (selectedEdge) {
1783
1811
  return graphTheme.edgeActive
1784
1812
  }
1785
1813
 
1786
- const opacity = edgeOpacityForScale(edge, state.transform.scale)
1814
+ const opacity = edgeOpacityForScale(edge, state.transform.scale) * edgeDepthOpacity(edge)
1787
1815
  return edge.inferred
1788
1816
  ? 'rgba(203, 213, 225, ' + opacity + ')'
1789
1817
  : 'rgba(153, 165, 181, ' + opacity + ')'
@@ -1791,17 +1819,19 @@ const edgeStrokeFor = (edge, selectedEdge) => {
1791
1819
 
1792
1820
  const edgeWidthFor = (edge, selectedEdge) => {
1793
1821
  if (edge.inferred) {
1794
- return selectedEdge ? 1.22 : 0.84
1822
+ const width = selectedEdge ? 1.22 : 0.84
1823
+ return width * edgeDepthScale(edge)
1795
1824
  }
1796
1825
 
1797
- return (selectedEdge ? 1.9 : 1.05) + Math.min(edgeWeight(edge) - 1, 8) * 0.24
1826
+ const width = (selectedEdge ? 1.9 : 1.05) + Math.min(edgeWeight(edge) - 1, 8) * 0.24
1827
+ return width * edgeDepthScale(edge)
1798
1828
  }
1799
1829
 
1800
1830
  const drawGraphEdge = (edge) => {
1801
1831
  const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
1802
1832
  ctx.beginPath()
1803
- ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
1804
- ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
1833
+ ctx.moveTo(nodeRenderX(edge.sourceNode), nodeRenderY(edge.sourceNode))
1834
+ ctx.lineTo(nodeRenderX(edge.targetNode), nodeRenderY(edge.targetNode))
1805
1835
  ctx.strokeStyle = edgeStrokeFor(edge, selectedEdge)
1806
1836
  ctx.lineWidth = edgeWidthFor(edge, selectedEdge)
1807
1837
  ctx.stroke()
@@ -1815,8 +1845,8 @@ const drawEdgeBatch = (edges, options) => {
1815
1845
  ctx.beginPath()
1816
1846
  for (let index = 0; index < edges.length; index += 1) {
1817
1847
  const edge = edges[index]
1818
- ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
1819
- ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
1848
+ ctx.moveTo(nodeRenderX(edge.sourceNode), nodeRenderY(edge.sourceNode))
1849
+ ctx.lineTo(nodeRenderX(edge.targetNode), nodeRenderY(edge.targetNode))
1820
1850
  }
1821
1851
  ctx.strokeStyle = options.strokeStyle
1822
1852
  ctx.lineWidth = options.lineWidth
@@ -1871,15 +1901,19 @@ const shouldDrawNodeLabels = (node, isSelected, isHovered) =>
1871
1901
  (state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
1872
1902
 
1873
1903
  const drawSingleNode = (node, options = { drawLabel: true }) => {
1874
- const radius = nodeRadius(node)
1904
+ const radius = nodeRadius(node) * nodeRenderScale(node)
1905
+ const x = nodeRenderX(node)
1906
+ const y = nodeRenderY(node)
1907
+ const opacity = nodeRenderOpacity(node)
1875
1908
  const isSelected = state.selected?.id === node.id
1876
1909
  const isHovered = state.hovered?.id === node.id
1910
+ ctx.globalAlpha = opacity
1877
1911
  ctx.beginPath()
1878
- ctx.arc(node.x, node.y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
1912
+ ctx.arc(x, y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
1879
1913
  ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
1880
1914
  ctx.fill()
1881
1915
  ctx.beginPath()
1882
- ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
1916
+ ctx.arc(x, y, radius, 0, Math.PI * 2)
1883
1917
  ctx.fillStyle = isSelected ? graphTheme.nodeSelected : isHovered ? graphTheme.nodeHover : graphTheme.node
1884
1918
  ctx.fill()
1885
1919
  ctx.lineWidth = isSelected ? 2.6 : 1.5
@@ -1887,12 +1921,14 @@ const drawSingleNode = (node, options = { drawLabel: true }) => {
1887
1921
  ctx.stroke()
1888
1922
 
1889
1923
  if (options.drawLabel && shouldDrawNodeLabels(node, isSelected, isHovered)) {
1924
+ ctx.globalAlpha = Math.max(0.58, opacity)
1890
1925
  ctx.fillStyle = graphTheme.label
1891
1926
  ctx.font = '12px Inter, system-ui, sans-serif'
1892
1927
  ctx.textAlign = 'center'
1893
1928
  ctx.textBaseline = 'top'
1894
- ctx.fillText(node.title.slice(0, 34), node.x, node.y + radius + 8)
1929
+ ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
1895
1930
  }
1931
+ ctx.globalAlpha = 1
1896
1932
  }
1897
1933
 
1898
1934
  const drawNodeBatch = (nodes) => {
@@ -1902,28 +1938,36 @@ const drawNodeBatch = (nodes) => {
1902
1938
 
1903
1939
  const drawHalos = state.renderNodes.length <= 1200 || state.transform.scale >= 0.45
1904
1940
  if (drawHalos) {
1905
- ctx.beginPath()
1906
1941
  for (let index = 0; index < nodes.length; index += 1) {
1907
1942
  const node = nodes[index]
1908
- ctx.moveTo(node.x + nodeRadius(node) + 3, node.y)
1909
- ctx.arc(node.x, node.y, nodeRadius(node) + 3, 0, Math.PI * 2)
1943
+ const radius = nodeRadius(node) * nodeRenderScale(node)
1944
+ const x = nodeRenderX(node)
1945
+ const y = nodeRenderY(node)
1946
+ ctx.globalAlpha = Math.max(0.2, nodeRenderOpacity(node) * 0.5)
1947
+ ctx.beginPath()
1948
+ ctx.arc(x, y, radius + 3, 0, Math.PI * 2)
1949
+ ctx.fillStyle = graphTheme.nodeHalo
1950
+ ctx.fill()
1910
1951
  }
1911
- ctx.fillStyle = graphTheme.nodeHalo
1912
- ctx.fill()
1952
+ ctx.globalAlpha = 1
1913
1953
  }
1914
1954
 
1915
- ctx.beginPath()
1916
1955
  for (let index = 0; index < nodes.length; index += 1) {
1917
1956
  const node = nodes[index]
1918
- const radius = nodeRadius(node)
1919
- ctx.moveTo(node.x + radius, node.y)
1920
- ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
1957
+ const radius = nodeRadius(node) * nodeRenderScale(node)
1958
+ const x = nodeRenderX(node)
1959
+ const y = nodeRenderY(node)
1960
+ const opacity = nodeRenderOpacity(node)
1961
+ ctx.globalAlpha = opacity
1962
+ ctx.beginPath()
1963
+ ctx.arc(x, y, radius, 0, Math.PI * 2)
1964
+ ctx.fillStyle = graphTheme.node
1965
+ ctx.fill()
1966
+ ctx.lineWidth = 1.25
1967
+ ctx.strokeStyle = graphTheme.nodeStroke
1968
+ ctx.stroke()
1921
1969
  }
1922
- ctx.fillStyle = graphTheme.node
1923
- ctx.fill()
1924
- ctx.lineWidth = 1.25
1925
- ctx.strokeStyle = graphTheme.nodeStroke
1926
- ctx.stroke()
1970
+ ctx.globalAlpha = 1
1927
1971
  }
1928
1972
 
1929
1973
  const drawGraphNodes = () => {
@@ -1942,6 +1986,10 @@ const drawGraphNodes = () => {
1942
1986
  }
1943
1987
  }
1944
1988
 
1989
+ if (shouldProjectRenderNodesInDepth()) {
1990
+ regularNodes.sort((left, right) => nodeRenderDepth(right) - nodeRenderDepth(left))
1991
+ }
1992
+
1945
1993
  drawNodeBatch(regularNodes)
1946
1994
 
1947
1995
  if (state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) {
@@ -1951,8 +1999,13 @@ const drawGraphNodes = () => {
1951
1999
  ctx.textBaseline = 'top'
1952
2000
  for (let index = 0; index < regularNodes.length; index += 1) {
1953
2001
  const node = regularNodes[index]
1954
- ctx.fillText(node.title.slice(0, 34), node.x, node.y + nodeRadius(node) + 8)
2002
+ const x = nodeRenderX(node)
2003
+ const y = nodeRenderY(node)
2004
+ const radius = nodeRadius(node) * nodeRenderScale(node)
2005
+ ctx.globalAlpha = Math.max(0.6, nodeRenderOpacity(node))
2006
+ ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
1955
2007
  }
2008
+ ctx.globalAlpha = 1
1956
2009
  }
1957
2010
 
1958
2011
  priorityNodes.forEach(node => drawSingleNode(node))
@@ -2003,12 +2056,17 @@ const drawGraphLabels = nodes => {
2003
2056
  ctx.textBaseline = 'top'
2004
2057
  for (let index = 0; index < nodes.length; index += 1) {
2005
2058
  const node = nodes[index]
2006
- ctx.fillText(node.title.slice(0, 34), node.x, node.y + nodeRadius(node) + 8)
2059
+ const x = nodeRenderX(node)
2060
+ const y = nodeRenderY(node)
2061
+ const radius = nodeRadius(node) * nodeRenderScale(node)
2062
+ ctx.globalAlpha = Math.max(0.6, nodeRenderOpacity(node))
2063
+ ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
2007
2064
  }
2065
+ ctx.globalAlpha = 1
2008
2066
  }
2009
2067
 
2010
2068
  const drawAcceleratedGraph = (width, height, drawEdges) => {
2011
- if (!webGlRenderer || state.renderClusters.length > 0) {
2069
+ if (!webGlRenderer || state.renderClusters.length > 0 || shouldProjectRenderNodesInDepth()) {
2012
2070
  return false
2013
2071
  }
2014
2072
 
@@ -2968,8 +3026,10 @@ const hitNode = point => {
2968
3026
  const nodes = state.renderNodes
2969
3027
  for (let index = nodes.length - 1; index >= 0; index -= 1) {
2970
3028
  const node = nodes[index]
2971
- const radius = nodeRadius(node)
2972
- if (Math.hypot(point.x - node.x, point.y - node.y) <= radius + 5) return node
3029
+ const radius = nodeRadius(node) * nodeRenderScale(node)
3030
+ const x = nodeRenderX(node)
3031
+ const y = nodeRenderY(node)
3032
+ if (Math.hypot(point.x - x, point.y - y) <= radius + 5) return node
2973
3033
  }
2974
3034
  return null
2975
3035
  }
@@ -3005,6 +3065,86 @@ const clusterOpacity = cluster =>
3005
3065
  const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
3006
3066
  const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
3007
3067
 
3068
+ const shouldProjectRenderNodesInDepth = () =>
3069
+ state.renderClusters.length === 0 &&
3070
+ state.renderNodes.length >= graphDepthProjectionNodeThreshold
3071
+
3072
+ const nodeProjectionAnchor = () => {
3073
+ const hub = state.primaryHub
3074
+ if (hub) {
3075
+ return { x: hub.x, y: hub.y }
3076
+ }
3077
+ if (state.macroRepresentative) {
3078
+ return { x: state.macroRepresentative.x, y: state.macroRepresentative.y }
3079
+ }
3080
+ return ecosystemFocusPoint()
3081
+ }
3082
+
3083
+ const projectGraphNodePoint = (x, y, depth, anchor) => {
3084
+ const safeDepth = Math.max(0, depth)
3085
+ const dx = x - anchor.x
3086
+ const dy = y - anchor.y
3087
+ const yawSin = Math.sin(graphDepthYaw)
3088
+ const yawCos = Math.cos(graphDepthYaw)
3089
+ const pitchSin = Math.sin(graphDepthPitch)
3090
+ const pitchCos = Math.cos(graphDepthPitch)
3091
+ const rotatedX = dx * yawCos + safeDepth * yawSin
3092
+ const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
3093
+ const rotatedY = dy * pitchCos - rotatedZ * pitchSin
3094
+ const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
3095
+ const factor = graphDepthPerspective / (graphDepthPerspective + projectedDepth)
3096
+ return {
3097
+ x: anchor.x + rotatedX * factor,
3098
+ y: anchor.y + rotatedY * factor,
3099
+ depth: projectedDepth,
3100
+ scale: graphDepthMinScale + (1 - graphDepthMinScale) * factor
3101
+ }
3102
+ }
3103
+
3104
+ const refreshRenderNodeDepthProjection = () => {
3105
+ if (!shouldProjectRenderNodesInDepth()) {
3106
+ state.renderNodeDepthProjectionById = new Map()
3107
+ return
3108
+ }
3109
+
3110
+ const anchor = nodeProjectionAnchor()
3111
+ let maxDistance = 1
3112
+ for (let index = 0; index < state.renderNodes.length; index += 1) {
3113
+ const node = state.renderNodes[index]
3114
+ const distance = Math.hypot(node.x - anchor.x, node.y - anchor.y)
3115
+ if (distance > maxDistance) {
3116
+ maxDistance = distance
3117
+ }
3118
+ }
3119
+
3120
+ const projectionById = new Map()
3121
+ for (let index = 0; index < state.renderNodes.length; index += 1) {
3122
+ const node = state.renderNodes[index]
3123
+ const radialDistance = Math.hypot(node.x - anchor.x, node.y - anchor.y)
3124
+ const radialRatio = radialDistance / maxDistance
3125
+ const degree = state.nodeDegrees.get(node.id) ?? 0
3126
+ const degreeLift = Math.min(60, degree * 2.2)
3127
+ const radialDepth = radialRatio * graphDepthFar * graphDepthRadialGain
3128
+ const depth = Math.max(0, graphDepthNear + radialDepth - degreeLift)
3129
+ const projected = projectGraphNodePoint(node.x, node.y, depth, anchor)
3130
+ const opacity = Math.max(graphDepthOpacityFloor, Math.min(1, projected.scale * 1.08))
3131
+ projectionById.set(node.id, {
3132
+ x: projected.x,
3133
+ y: projected.y,
3134
+ depth: projected.depth,
3135
+ scale: projected.scale,
3136
+ opacity
3137
+ })
3138
+ }
3139
+ state.renderNodeDepthProjectionById = projectionById
3140
+ }
3141
+
3142
+ const projectedNode = node => state.renderNodeDepthProjectionById.get(node.id) ?? null
3143
+ const nodeRenderX = node => projectedNode(node)?.x ?? node.x
3144
+ const nodeRenderY = node => projectedNode(node)?.y ?? node.y
3145
+ const nodeRenderDepth = node => projectedNode(node)?.depth ?? graphDepthNear
3146
+ const nodeRenderScale = node => projectedNode(node)?.scale ?? 1
3147
+ const nodeRenderOpacity = node => projectedNode(node)?.opacity ?? 1
3008
3148
  const worldViewportBounds = () => {
3009
3149
  const width = Math.max(state.viewport.width, 320)
3010
3150
  const height = Math.max(state.viewport.height, 320)
@@ -3389,6 +3529,7 @@ const render = now => {
3389
3529
 
3390
3530
  computeRenderVisibility()
3391
3531
  tick(delta, now)
3532
+ refreshRenderNodeDepthProjection()
3392
3533
  const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
3393
3534
  const manualZoomGuardActive = now - state.lastManualZoomAt < zoomRecoveryGuardMs
3394
3535
  const allowViewportAutoRecovery = state.nodes.length <= massiveGraphNodeThreshold
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.109",
3
+ "version": "0.1.0-beta.110",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",