@andespindola/brainlink 0.1.0-beta.96 → 0.1.0-beta.98
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 -1
- package/dist/application/frontend/client-js.js +134 -54
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -602,7 +602,7 @@ The graph UI shows:
|
|
|
602
602
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
603
603
|
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
604
604
|
- 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
|
|
605
|
-
- graph LOD progression: graphs up to 1000 notes render directly; larger graphs use
|
|
605
|
+
- graph LOD progression: graphs up to 1000 notes render directly; larger graphs use one recursive model where each visible level targets up to 999 non-hub nodes, starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (again up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy and stress-50k follow the same structure at different depths
|
|
606
606
|
|
|
607
607
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
608
608
|
|
|
@@ -21,9 +21,8 @@ const viewportPaddingPx = 280
|
|
|
21
21
|
const worldCoordinateLimit = 5_000_000
|
|
22
22
|
const transformCoordinateLimit = 20_000_000
|
|
23
23
|
const hoverHitTestIntervalMs = 64
|
|
24
|
-
const
|
|
24
|
+
const ecosystemLevelNodeCap = 999
|
|
25
25
|
const ecosystemActivationNodeThreshold = 1000
|
|
26
|
-
const ecosystemGroupSizes = [1000, 500, 250, 125, 60, 30, 15, 8]
|
|
27
26
|
const ecosystemClusterEdgeLimit = 520
|
|
28
27
|
const ecosystemHubEdgeLimit = 120
|
|
29
28
|
const ecosystemSiblingEdgeLimit = 180
|
|
@@ -32,15 +31,6 @@ const massiveEcosystemClusterScaleThreshold = 4.2
|
|
|
32
31
|
const ecosystemSubgraphScaleThreshold = 0.18
|
|
33
32
|
const ecosystemMicroScaleThreshold = 0.08
|
|
34
33
|
const ecosystemFocusedParentLimit = 2
|
|
35
|
-
const ecosystemExpansionLevels = [
|
|
36
|
-
{ parentSize: 1000, childSize: 500, start: 0.04, end: 0.62 },
|
|
37
|
-
{ parentSize: 500, childSize: 250, start: 0.16, end: 0.72 },
|
|
38
|
-
{ parentSize: 250, childSize: 125, start: 0.3, end: 0.78 },
|
|
39
|
-
{ parentSize: 125, childSize: 60, start: 0.46, end: 0.86 },
|
|
40
|
-
{ parentSize: 60, childSize: 30, start: 0.64, end: 1.06 },
|
|
41
|
-
{ parentSize: 30, childSize: 15, start: 0.84, end: 1.38 },
|
|
42
|
-
{ parentSize: 15, childSize: 8, start: 1.08, end: 1.82 }
|
|
43
|
-
]
|
|
44
34
|
const zoomRecoveryGuardMs = 4200
|
|
45
35
|
const zoomCapTargetViewportShare = 0.72
|
|
46
36
|
const meshEdgeScaleThreshold = 0.09
|
|
@@ -87,6 +77,9 @@ const state = {
|
|
|
87
77
|
ecosystemClusters: [],
|
|
88
78
|
ecosystemClustersBySize: new Map(),
|
|
89
79
|
ecosystemNodeClusterBySize: new Map(),
|
|
80
|
+
ecosystemLevelSizes: [],
|
|
81
|
+
ecosystemExpansionLevels: [],
|
|
82
|
+
ecosystemBaseSize: ecosystemLevelNodeCap,
|
|
90
83
|
ecosystemHubCluster: null,
|
|
91
84
|
macroCenter: { x: 0, y: 0 },
|
|
92
85
|
macroRepresentative: null,
|
|
@@ -597,10 +590,21 @@ const recomputeVisibility = () => {
|
|
|
597
590
|
: { x: 0, y: 0 }
|
|
598
591
|
const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
|
|
599
592
|
? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
|
|
600
|
-
: {
|
|
593
|
+
: {
|
|
594
|
+
clusters: [],
|
|
595
|
+
clustersBySize: new Map(),
|
|
596
|
+
nodeClusterBySize: new Map(),
|
|
597
|
+
levelSizes: [],
|
|
598
|
+
expansionLevels: [],
|
|
599
|
+
baseSize: ecosystemLevelNodeCap,
|
|
600
|
+
hubCluster: null
|
|
601
|
+
}
|
|
601
602
|
state.ecosystemClusters = ecosystemGraph.clusters
|
|
602
603
|
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
603
604
|
state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
|
|
605
|
+
state.ecosystemLevelSizes = ecosystemGraph.levelSizes
|
|
606
|
+
state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
|
|
607
|
+
state.ecosystemBaseSize = ecosystemGraph.baseSize
|
|
604
608
|
state.ecosystemHubCluster = ecosystemGraph.hubCluster
|
|
605
609
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
606
610
|
markRenderDirty()
|
|
@@ -747,14 +751,56 @@ const selectEcosystemRepresentative = nodes => {
|
|
|
747
751
|
}
|
|
748
752
|
|
|
749
753
|
const ecosystemLayoutSpacingForSize = size => {
|
|
750
|
-
if (size >=
|
|
751
|
-
if (size >=
|
|
752
|
-
if (size >=
|
|
753
|
-
if (size >=
|
|
754
|
-
if (size >=
|
|
755
|
-
if (size >=
|
|
756
|
-
|
|
757
|
-
|
|
754
|
+
if (size >= ecosystemLevelNodeCap) return 260
|
|
755
|
+
if (size >= 320) return 110
|
|
756
|
+
if (size >= 120) return 64
|
|
757
|
+
if (size >= 48) return 34
|
|
758
|
+
if (size >= 18) return 18
|
|
759
|
+
if (size >= 8) return 11
|
|
760
|
+
return 7
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
const buildEcosystemLevelSizes = nodeCount => {
|
|
764
|
+
if (nodeCount <= 0) return []
|
|
765
|
+
const sizes = []
|
|
766
|
+
let currentSize = Math.max(1, Math.ceil(nodeCount / ecosystemLevelNodeCap))
|
|
767
|
+
while (currentSize >= 1) {
|
|
768
|
+
sizes.push(currentSize)
|
|
769
|
+
if (currentSize === 1) {
|
|
770
|
+
break
|
|
771
|
+
}
|
|
772
|
+
const nextSize = Math.max(1, Math.ceil(currentSize / ecosystemLevelNodeCap))
|
|
773
|
+
if (nextSize === currentSize) {
|
|
774
|
+
break
|
|
775
|
+
}
|
|
776
|
+
currentSize = nextSize
|
|
777
|
+
}
|
|
778
|
+
return sizes
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
|
|
782
|
+
if (levelSizes.length <= 1) {
|
|
783
|
+
return []
|
|
784
|
+
}
|
|
785
|
+
const maxScale = nodeCount > massiveGraphNodeThreshold
|
|
786
|
+
? massiveEcosystemClusterScaleThreshold
|
|
787
|
+
: ecosystemClusterScaleThreshold
|
|
788
|
+
const startScale = 0.04
|
|
789
|
+
const transitionCount = levelSizes.length - 1
|
|
790
|
+
const usableScale = Math.max(0.08, maxScale - startScale)
|
|
791
|
+
const step = usableScale / transitionCount
|
|
792
|
+
const levels = []
|
|
793
|
+
for (let index = 0; index < transitionCount; index += 1) {
|
|
794
|
+
const start = startScale + step * index * 0.72
|
|
795
|
+
const end = Math.min(maxScale, start + step * 1.85)
|
|
796
|
+
levels.push({
|
|
797
|
+
parentSize: levelSizes[index],
|
|
798
|
+
childSize: levelSizes[index + 1],
|
|
799
|
+
start,
|
|
800
|
+
end
|
|
801
|
+
})
|
|
802
|
+
}
|
|
803
|
+
return levels
|
|
758
804
|
}
|
|
759
805
|
|
|
760
806
|
const ecosystemCompactPoint = (index, total, center, spacing) => {
|
|
@@ -837,19 +883,30 @@ const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
|
|
|
837
883
|
|
|
838
884
|
const buildEcosystemGraph = (nodes, center, hub) => {
|
|
839
885
|
if (nodes.length === 0) {
|
|
840
|
-
return {
|
|
886
|
+
return {
|
|
887
|
+
clusters: [],
|
|
888
|
+
clustersBySize: new Map(),
|
|
889
|
+
nodeClusterBySize: new Map(),
|
|
890
|
+
levelSizes: [],
|
|
891
|
+
expansionLevels: [],
|
|
892
|
+
baseSize: ecosystemLevelNodeCap,
|
|
893
|
+
hubCluster: null
|
|
894
|
+
}
|
|
841
895
|
}
|
|
842
896
|
|
|
843
897
|
const hubCluster = buildEcosystemHubCluster(hub, center)
|
|
844
898
|
const sortedNodes = nodes
|
|
845
899
|
.filter(node => node.id !== hub?.id)
|
|
846
900
|
.sort(compareNodesForEcosystem)
|
|
901
|
+
const levelSizes = buildEcosystemLevelSizes(sortedNodes.length)
|
|
902
|
+
const expansionLevels = buildEcosystemExpansionLevels(levelSizes, nodes.length)
|
|
903
|
+
const baseSize = levelSizes[0] ?? ecosystemLevelNodeCap
|
|
847
904
|
const clustersBySize = new Map()
|
|
848
905
|
const nodeClusterBySize = new Map()
|
|
849
906
|
let parentLookup = null
|
|
850
907
|
|
|
851
|
-
for (let index = 0; index <
|
|
852
|
-
const size =
|
|
908
|
+
for (let index = 0; index < levelSizes.length; index += 1) {
|
|
909
|
+
const size = levelSizes[index]
|
|
853
910
|
const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
|
|
854
911
|
clustersBySize.set(size, level.clusters)
|
|
855
912
|
nodeClusterBySize.set(size, level.clusterByNodeId)
|
|
@@ -857,9 +914,12 @@ const buildEcosystemGraph = (nodes, center, hub) => {
|
|
|
857
914
|
}
|
|
858
915
|
|
|
859
916
|
return {
|
|
860
|
-
clusters: clustersBySize.get(
|
|
917
|
+
clusters: clustersBySize.get(baseSize) ?? [],
|
|
861
918
|
clustersBySize,
|
|
862
919
|
nodeClusterBySize,
|
|
920
|
+
levelSizes,
|
|
921
|
+
expansionLevels,
|
|
922
|
+
baseSize,
|
|
863
923
|
hubCluster
|
|
864
924
|
}
|
|
865
925
|
}
|
|
@@ -876,6 +936,10 @@ const filterEcosystemClustersByViewport = (clusters, viewport) => {
|
|
|
876
936
|
}
|
|
877
937
|
|
|
878
938
|
const ecosystemFocusPoint = () => {
|
|
939
|
+
const cursorPoint = cursorWorldPoint()
|
|
940
|
+
if (cursorPoint) {
|
|
941
|
+
return cursorPoint
|
|
942
|
+
}
|
|
879
943
|
const now = performance.now()
|
|
880
944
|
if (now - state.lastZoomFocus.at <= 1800) {
|
|
881
945
|
return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
@@ -887,7 +951,11 @@ const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
|
|
|
887
951
|
clusters
|
|
888
952
|
.map(cluster => ({
|
|
889
953
|
cluster,
|
|
890
|
-
distance: Math.
|
|
954
|
+
distance: Math.max(
|
|
955
|
+
0,
|
|
956
|
+
Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y) -
|
|
957
|
+
clusterRadiusPx(cluster) / Math.max(state.transform.scale, 0.0001)
|
|
958
|
+
)
|
|
891
959
|
}))
|
|
892
960
|
.sort((left, right) => left.distance - right.distance)
|
|
893
961
|
.slice(0, limit)
|
|
@@ -903,7 +971,7 @@ const zoomProgress = (scale, start, end) =>
|
|
|
903
971
|
|
|
904
972
|
const semanticZoomSpread = (progress, childSize) => {
|
|
905
973
|
const curve = Math.pow(progress, 4.2)
|
|
906
|
-
if (childSize >=
|
|
974
|
+
if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
|
|
907
975
|
return 0.12 + curve * 0.88
|
|
908
976
|
}
|
|
909
977
|
return curve
|
|
@@ -911,7 +979,7 @@ const semanticZoomSpread = (progress, childSize) => {
|
|
|
911
979
|
|
|
912
980
|
const opacityForProgress = (progress, childSize) => {
|
|
913
981
|
const eased = Math.pow(progress, 2.1)
|
|
914
|
-
if (childSize >=
|
|
982
|
+
if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
|
|
915
983
|
return 0.22 + eased * 0.78
|
|
916
984
|
}
|
|
917
985
|
return eased
|
|
@@ -953,13 +1021,13 @@ const spreadChildClusterFromParent = (cluster, childSize, progress, spread) => {
|
|
|
953
1021
|
}
|
|
954
1022
|
|
|
955
1023
|
const selectHierarchicalEcosystemClusters = viewport => {
|
|
956
|
-
const baseClusters = state.ecosystemClustersBySize.get(
|
|
1024
|
+
const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
|
|
957
1025
|
const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
|
|
958
1026
|
const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
|
|
959
1027
|
const visibleClusters = [...visibleBaseClusters]
|
|
960
1028
|
|
|
961
|
-
for (let index = 0; index < ecosystemExpansionLevels.length; index += 1) {
|
|
962
|
-
const level = ecosystemExpansionLevels[index]
|
|
1029
|
+
for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
|
|
1030
|
+
const level = state.ecosystemExpansionLevels[index]
|
|
963
1031
|
const parentClusters = visibleClusters.filter(cluster => cluster.size === level.parentSize)
|
|
964
1032
|
if (parentClusters.length === 0) {
|
|
965
1033
|
continue
|
|
@@ -1233,6 +1301,29 @@ const viewportCenterWorldPoint = () => {
|
|
|
1233
1301
|
}
|
|
1234
1302
|
}
|
|
1235
1303
|
|
|
1304
|
+
const screenToWorldPoint = (screenX, screenY) => ({
|
|
1305
|
+
x: (screenX - state.transform.x) / state.transform.scale,
|
|
1306
|
+
y: (screenY - state.transform.y) / state.transform.scale
|
|
1307
|
+
})
|
|
1308
|
+
|
|
1309
|
+
const cursorWorldPoint = () => {
|
|
1310
|
+
if (!state.cursor.inCanvas) {
|
|
1311
|
+
return null
|
|
1312
|
+
}
|
|
1313
|
+
const rect = canvas.getBoundingClientRect()
|
|
1314
|
+
const screenX = state.cursor.x - rect.left
|
|
1315
|
+
const screenY = state.cursor.y - rect.top
|
|
1316
|
+
const width = Math.max(rect.width, 320)
|
|
1317
|
+
const height = Math.max(rect.height, 320)
|
|
1318
|
+
if (!Number.isFinite(screenX) || !Number.isFinite(screenY)) {
|
|
1319
|
+
return null
|
|
1320
|
+
}
|
|
1321
|
+
if (screenX < 0 || screenX > width || screenY < 0 || screenY > height) {
|
|
1322
|
+
return null
|
|
1323
|
+
}
|
|
1324
|
+
return screenToWorldPoint(screenX, screenY)
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1236
1327
|
const visibilityScaleBucket = (scale) => {
|
|
1237
1328
|
const safeScale = Math.max(zoomRange.min, scale)
|
|
1238
1329
|
if (safeScale < 0.01) return Math.round(safeScale * 300_000)
|
|
@@ -1288,11 +1379,12 @@ const selectStableSampleNodes = (sourceNodes, limit) => {
|
|
|
1288
1379
|
}
|
|
1289
1380
|
|
|
1290
1381
|
const now = performance.now()
|
|
1382
|
+
const cursorPoint = cursorWorldPoint()
|
|
1291
1383
|
const recentZoomFocus =
|
|
1292
1384
|
now - state.lastZoomFocus.at <= 1500
|
|
1293
1385
|
? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
1294
1386
|
: null
|
|
1295
|
-
const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
|
|
1387
|
+
const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
|
|
1296
1388
|
const previousIds = new Set(state.renderNodes.map((node) => node.id))
|
|
1297
1389
|
const preferAnchorDistance = state.visibleNodes.length > massiveGraphNodeThreshold && state.transform.scale >= 0.28
|
|
1298
1390
|
|
|
@@ -1326,11 +1418,12 @@ const selectAccessBridgeNodes = (sourceNodes, limit) => {
|
|
|
1326
1418
|
}
|
|
1327
1419
|
|
|
1328
1420
|
const now = performance.now()
|
|
1421
|
+
const cursorPoint = cursorWorldPoint()
|
|
1329
1422
|
const recentZoomFocus =
|
|
1330
1423
|
now - state.lastZoomFocus.at <= 1200
|
|
1331
1424
|
? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
1332
1425
|
: null
|
|
1333
|
-
const anchor = recentZoomFocus ?? viewportCenterWorldPoint()
|
|
1426
|
+
const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
|
|
1334
1427
|
return [...sourceNodes]
|
|
1335
1428
|
.sort((left, right) => {
|
|
1336
1429
|
const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
|
|
@@ -2408,10 +2501,7 @@ const tick = delta => {
|
|
|
2408
2501
|
|
|
2409
2502
|
const worldPoint = event => {
|
|
2410
2503
|
const rect = canvas.getBoundingClientRect()
|
|
2411
|
-
return
|
|
2412
|
-
x: (event.clientX - rect.left - state.transform.x) / state.transform.scale,
|
|
2413
|
-
y: (event.clientY - rect.top - state.transform.y) / state.transform.scale
|
|
2414
|
-
}
|
|
2504
|
+
return screenToWorldPoint(event.clientX - rect.left, event.clientY - rect.top)
|
|
2415
2505
|
}
|
|
2416
2506
|
|
|
2417
2507
|
const connectedNodeIdsFor = (nodeId) => {
|
|
@@ -2550,22 +2640,10 @@ const clusterRadiusPx = cluster => {
|
|
|
2550
2640
|
return 3.8
|
|
2551
2641
|
}
|
|
2552
2642
|
if (String(cluster.id).startsWith('ecosystem-')) {
|
|
2553
|
-
const
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
: cluster.size >= 250
|
|
2558
|
-
? 0.82
|
|
2559
|
-
: cluster.size >= 125
|
|
2560
|
-
? 0.74
|
|
2561
|
-
: cluster.size >= 60
|
|
2562
|
-
? 0.66
|
|
2563
|
-
: cluster.size >= 30
|
|
2564
|
-
? 0.62
|
|
2565
|
-
: cluster.size >= 15
|
|
2566
|
-
? 0.74
|
|
2567
|
-
: 0.9
|
|
2568
|
-
return Math.max(0.62, Math.min(2.2, base + Math.log10(cluster.count + 1) * 0.14))
|
|
2643
|
+
const size = Math.max(1, Math.min(ecosystemLevelNodeCap, cluster.size || cluster.count || 1))
|
|
2644
|
+
const sizeBias = 0.56 + Math.log10(size + 1) * 0.28
|
|
2645
|
+
const densityBias = Math.log10((cluster.count || 1) + 1) * 0.12
|
|
2646
|
+
return Math.max(0.62, Math.min(2.4, sizeBias + densityBias))
|
|
2569
2647
|
}
|
|
2570
2648
|
return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
|
|
2571
2649
|
}
|
|
@@ -3170,8 +3248,9 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
|
3170
3248
|
if (nextScale === state.transform.scale) {
|
|
3171
3249
|
return
|
|
3172
3250
|
}
|
|
3173
|
-
const
|
|
3174
|
-
const
|
|
3251
|
+
const worldPointAtCursor = screenToWorldPoint(screenX, screenY)
|
|
3252
|
+
const worldX = worldPointAtCursor.x
|
|
3253
|
+
const worldY = worldPointAtCursor.y
|
|
3175
3254
|
state.lastZoomFocus = {
|
|
3176
3255
|
x: worldX,
|
|
3177
3256
|
y: worldY,
|
|
@@ -3218,6 +3297,7 @@ const handleWheelZoom = event => {
|
|
|
3218
3297
|
const rawCursorY = Number.isFinite(event.offsetY) ? event.offsetY : event.clientY - rect.top
|
|
3219
3298
|
const cursorX = Math.max(0, Math.min(Math.max(rect.width, 320), rawCursorX))
|
|
3220
3299
|
const cursorY = Math.max(0, Math.min(Math.max(rect.height, 320), rawCursorY))
|
|
3300
|
+
state.cursor = { x: event.clientX, y: event.clientY, inCanvas: true }
|
|
3221
3301
|
const factor = wheelZoomFactor(event)
|
|
3222
3302
|
|
|
3223
3303
|
if (!Number.isFinite(factor) || factor <= 0 || factor === 1) {
|
package/package.json
CHANGED