@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 +1 -0
- package/dist/application/frontend/client-js.js +171 -30
- package/package.json +1 -1
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
|
-
|
|
1822
|
+
const width = selectedEdge ? 1.22 : 0.84
|
|
1823
|
+
return width * edgeDepthScale(edge)
|
|
1795
1824
|
}
|
|
1796
1825
|
|
|
1797
|
-
|
|
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
|
|
1804
|
-
ctx.lineTo(edge.targetNode
|
|
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
|
|
1819
|
-
ctx.lineTo(edge.targetNode
|
|
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(
|
|
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(
|
|
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),
|
|
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
|
-
|
|
1909
|
-
|
|
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.
|
|
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
|
-
|
|
1920
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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