@andespindola/brainlink 0.1.0-beta.57 → 0.1.0-beta.59
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.
|
@@ -19,6 +19,9 @@ const hoverHitTestIntervalMs = 64
|
|
|
19
19
|
const overviewClusterMaxCount = 1400
|
|
20
20
|
const zoomRecoveryGuardMs = 1500
|
|
21
21
|
const zoomCapTargetViewportShare = 0.72
|
|
22
|
+
const meshEdgeScaleThreshold = 0.09
|
|
23
|
+
const meshEdgeMinBudget = 140
|
|
24
|
+
const meshEdgeMaxBudget = 1400
|
|
22
25
|
const state = {
|
|
23
26
|
graph: { nodes: [], edges: [] },
|
|
24
27
|
nodes: [],
|
|
@@ -595,6 +598,122 @@ const collectVisibleEdgesForNodes = nodeIds => {
|
|
|
595
598
|
return collected
|
|
596
599
|
}
|
|
597
600
|
|
|
601
|
+
const edgePairKey = (source, target) =>
|
|
602
|
+
source < target ? source + '|' + target : target + '|' + source
|
|
603
|
+
|
|
604
|
+
const meshNeighborBuckets = (nodes, cellSize) => {
|
|
605
|
+
const buckets = new Map()
|
|
606
|
+
|
|
607
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
608
|
+
const node = nodes[index]
|
|
609
|
+
const cellX = Math.floor(node.x / cellSize)
|
|
610
|
+
const cellY = Math.floor(node.y / cellSize)
|
|
611
|
+
const key = cellX + ':' + cellY
|
|
612
|
+
const bucket = buckets.get(key)
|
|
613
|
+
if (bucket) {
|
|
614
|
+
bucket.push(node)
|
|
615
|
+
} else {
|
|
616
|
+
buckets.set(key, [node])
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
return buckets
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const meshCandidatesForNode = (node, buckets, cellSize) => {
|
|
624
|
+
const cellX = Math.floor(node.x / cellSize)
|
|
625
|
+
const cellY = Math.floor(node.y / cellSize)
|
|
626
|
+
const candidates = []
|
|
627
|
+
|
|
628
|
+
for (let offsetX = -1; offsetX <= 1; offsetX += 1) {
|
|
629
|
+
for (let offsetY = -1; offsetY <= 1; offsetY += 1) {
|
|
630
|
+
const bucket = buckets.get((cellX + offsetX) + ':' + (cellY + offsetY))
|
|
631
|
+
if (!bucket) continue
|
|
632
|
+
for (let index = 0; index < bucket.length; index += 1) {
|
|
633
|
+
const candidate = bucket[index]
|
|
634
|
+
if (candidate.id !== node.id) {
|
|
635
|
+
candidates.push(candidate)
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return candidates
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const buildMeshEdgesForNodes = (nodes, existingEdges) => {
|
|
645
|
+
if (nodes.length < 2 || state.transform.scale < meshEdgeScaleThreshold) {
|
|
646
|
+
return []
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
const existingKeys = new Set()
|
|
650
|
+
for (let index = 0; index < existingEdges.length; index += 1) {
|
|
651
|
+
const edge = existingEdges[index]
|
|
652
|
+
if (edge.target) {
|
|
653
|
+
existingKeys.add(edgePairKey(edge.source, edge.target))
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const desiredBudget = Math.min(
|
|
658
|
+
meshEdgeMaxBudget,
|
|
659
|
+
Math.max(meshEdgeMinBudget, Math.floor(edgeBudgetForCurrentFrame() * 0.62))
|
|
660
|
+
)
|
|
661
|
+
const perNodeNeighborCount =
|
|
662
|
+
state.transform.scale >= 1.05 ? 4
|
|
663
|
+
: state.transform.scale >= 0.62 ? 3
|
|
664
|
+
: 2
|
|
665
|
+
const cellSize = Math.max(120, 280 / Math.max(state.transform.scale, 0.0001))
|
|
666
|
+
const maxDistance = 980
|
|
667
|
+
const maxDistanceSquared = maxDistance * maxDistance
|
|
668
|
+
const buckets = meshNeighborBuckets(nodes, cellSize)
|
|
669
|
+
const meshEdges = []
|
|
670
|
+
const meshKeys = new Set()
|
|
671
|
+
|
|
672
|
+
for (let index = 0; index < nodes.length && meshEdges.length < desiredBudget; index += 1) {
|
|
673
|
+
const node = nodes[index]
|
|
674
|
+
const candidates = meshCandidatesForNode(node, buckets, cellSize)
|
|
675
|
+
.map((candidate) => ({
|
|
676
|
+
node: candidate,
|
|
677
|
+
distanceSquared: (candidate.x - node.x) ** 2 + (candidate.y - node.y) ** 2
|
|
678
|
+
}))
|
|
679
|
+
.filter((candidate) => candidate.distanceSquared <= maxDistanceSquared)
|
|
680
|
+
.sort((left, right) => left.distanceSquared - right.distanceSquared)
|
|
681
|
+
|
|
682
|
+
let linked = 0
|
|
683
|
+
for (let candidateIndex = 0; candidateIndex < candidates.length && linked < perNodeNeighborCount && meshEdges.length < desiredBudget; candidateIndex += 1) {
|
|
684
|
+
const candidate = candidates[candidateIndex].node
|
|
685
|
+
const key = edgePairKey(node.id, candidate.id)
|
|
686
|
+
if (existingKeys.has(key) || meshKeys.has(key)) {
|
|
687
|
+
continue
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
meshKeys.add(key)
|
|
691
|
+
meshEdges.push({
|
|
692
|
+
source: node.id,
|
|
693
|
+
target: candidate.id,
|
|
694
|
+
targetTitle: candidate.title,
|
|
695
|
+
weight: 1,
|
|
696
|
+
priority: 'normal',
|
|
697
|
+
sourceNode: node,
|
|
698
|
+
targetNode: candidate,
|
|
699
|
+
inferred: true
|
|
700
|
+
})
|
|
701
|
+
linked += 1
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
return meshEdges
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const withMeshEdges = (nodes, edges) => {
|
|
709
|
+
if (nodes.length === 0 || state.visibleNodes.length <= largeGraphNodeThreshold || state.transform.scale < meshEdgeScaleThreshold) {
|
|
710
|
+
return edges
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
const meshEdges = buildMeshEdgesForNodes(nodes, edges)
|
|
714
|
+
return meshEdges.length > 0 ? edges.concat(meshEdges) : edges
|
|
715
|
+
}
|
|
716
|
+
|
|
598
717
|
const fallbackViewportNodes = () => {
|
|
599
718
|
const nodes = []
|
|
600
719
|
const maxNodes = Math.min(renderNodeBudget, 220)
|
|
@@ -710,9 +829,9 @@ const ensureHubNodesInRenderedSet = (nodes) => {
|
|
|
710
829
|
}
|
|
711
830
|
|
|
712
831
|
const zoomCapByNodeCount = (nodeCount) => {
|
|
713
|
-
if (nodeCount > 50000) return
|
|
714
|
-
if (nodeCount > 20000) return
|
|
715
|
-
if (nodeCount > 6000) return 1
|
|
832
|
+
if (nodeCount > 50000) return 2.6
|
|
833
|
+
if (nodeCount > 20000) return 2.35
|
|
834
|
+
if (nodeCount > 6000) return 2.1
|
|
716
835
|
if (nodeCount > 2000) return 2.2
|
|
717
836
|
return zoomRange.max
|
|
718
837
|
}
|
|
@@ -731,7 +850,9 @@ const zoomCapByHubDistance = (distance) => {
|
|
|
731
850
|
|
|
732
851
|
const currentZoomMax = () => {
|
|
733
852
|
const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
|
|
734
|
-
const
|
|
853
|
+
const hubDistanceCap = zoomCapByHubDistance(state.hubNeighborDistance)
|
|
854
|
+
const minimumUsefulCap = nodeCount > massiveGraphNodeThreshold ? 1.9 : nodeCount > largeGraphNodeThreshold ? 1.35 : 0.8
|
|
855
|
+
const capped = Math.min(zoomCapByNodeCount(nodeCount), Math.max(minimumUsefulCap, hubDistanceCap))
|
|
735
856
|
return Math.max(zoomRange.min * 2, capped)
|
|
736
857
|
}
|
|
737
858
|
|
|
@@ -1066,7 +1187,12 @@ const hitNode = point => {
|
|
|
1066
1187
|
if (state.renderClusters.length > 0) {
|
|
1067
1188
|
return null
|
|
1068
1189
|
}
|
|
1069
|
-
|
|
1190
|
+
const hitScaleFloor = state.nodes.length > massiveGraphNodeThreshold
|
|
1191
|
+
? 0.2
|
|
1192
|
+
: state.nodes.length > largeGraphNodeThreshold
|
|
1193
|
+
? 0.34
|
|
1194
|
+
: 0
|
|
1195
|
+
if (state.transform.scale < hitScaleFloor) {
|
|
1070
1196
|
return null
|
|
1071
1197
|
}
|
|
1072
1198
|
|
|
@@ -1225,7 +1351,7 @@ const computeRenderVisibility = () => {
|
|
|
1225
1351
|
state.renderNodes = state.visibleNodes
|
|
1226
1352
|
state.renderClusters = []
|
|
1227
1353
|
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
1228
|
-
state.renderEdges = collectVisibleEdgesForNodes(ids)
|
|
1354
|
+
state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
|
|
1229
1355
|
return
|
|
1230
1356
|
}
|
|
1231
1357
|
|
|
@@ -1249,7 +1375,7 @@ const computeRenderVisibility = () => {
|
|
|
1249
1375
|
|
|
1250
1376
|
state.renderClusters = []
|
|
1251
1377
|
state.renderNodes = sampledNodes
|
|
1252
|
-
state.renderEdges = sampledEdges
|
|
1378
|
+
state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
|
|
1253
1379
|
return
|
|
1254
1380
|
}
|
|
1255
1381
|
|
|
@@ -1258,7 +1384,7 @@ const computeRenderVisibility = () => {
|
|
|
1258
1384
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
1259
1385
|
state.renderClusters = []
|
|
1260
1386
|
state.renderNodes = sampled
|
|
1261
|
-
state.renderEdges = collectVisibleEdgesForNodes(sampledIds)
|
|
1387
|
+
state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
|
|
1262
1388
|
return
|
|
1263
1389
|
}
|
|
1264
1390
|
|
|
@@ -1294,7 +1420,7 @@ const computeRenderVisibility = () => {
|
|
|
1294
1420
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
1295
1421
|
state.renderNodes = fallbackNodes
|
|
1296
1422
|
state.renderClusters = []
|
|
1297
|
-
state.renderEdges = collectVisibleEdgesForNodes(fallbackIds)
|
|
1423
|
+
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
1298
1424
|
return
|
|
1299
1425
|
}
|
|
1300
1426
|
|
|
@@ -1303,14 +1429,14 @@ const computeRenderVisibility = () => {
|
|
|
1303
1429
|
const edges = collectVisibleEdgesForNodes(nodeIds)
|
|
1304
1430
|
|
|
1305
1431
|
state.renderNodes = normalizedNodes
|
|
1306
|
-
state.renderEdges = edges
|
|
1432
|
+
state.renderEdges = withMeshEdges(normalizedNodes, edges)
|
|
1307
1433
|
|
|
1308
1434
|
if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
|
|
1309
1435
|
const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
|
|
1310
1436
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
1311
1437
|
state.renderClusters = []
|
|
1312
1438
|
state.renderNodes = fallbackNodes
|
|
1313
|
-
state.renderEdges = collectVisibleEdgesForNodes(fallbackIds)
|
|
1439
|
+
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
1314
1440
|
}
|
|
1315
1441
|
}
|
|
1316
1442
|
|
|
@@ -1420,11 +1546,18 @@ const render = now => {
|
|
|
1420
1546
|
if (drawEdges) {
|
|
1421
1547
|
state.renderEdges.forEach(edge => {
|
|
1422
1548
|
const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
1549
|
+
const inferredEdge = Boolean(edge.inferred)
|
|
1423
1550
|
ctx.beginPath()
|
|
1424
1551
|
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
1425
1552
|
ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
|
|
1426
|
-
ctx.strokeStyle = selectedEdge
|
|
1427
|
-
|
|
1553
|
+
ctx.strokeStyle = selectedEdge
|
|
1554
|
+
? graphTheme.edgeActive
|
|
1555
|
+
: inferredEdge
|
|
1556
|
+
? 'rgba(203, 213, 225, 0.1)'
|
|
1557
|
+
: graphTheme.edge
|
|
1558
|
+
ctx.lineWidth = inferredEdge
|
|
1559
|
+
? 0.82
|
|
1560
|
+
: (selectedEdge ? 1.8 : 1) + Math.min(edgeWeight(edge) - 1, 8) * 0.22
|
|
1428
1561
|
ctx.stroke()
|
|
1429
1562
|
})
|
|
1430
1563
|
}
|
|
@@ -1478,6 +1611,7 @@ const render = now => {
|
|
|
1478
1611
|
const shouldDrawLabels =
|
|
1479
1612
|
isSelected ||
|
|
1480
1613
|
isHovered ||
|
|
1614
|
+
(state.nodes.length > largeGraphNodeThreshold && state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) ||
|
|
1481
1615
|
(state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
|
|
1482
1616
|
if (shouldDrawLabels) {
|
|
1483
1617
|
ctx.fillStyle = graphTheme.label
|
|
@@ -1721,7 +1855,7 @@ const bindEvents = () => {
|
|
|
1721
1855
|
const point = worldPoint(event)
|
|
1722
1856
|
const now = performance.now()
|
|
1723
1857
|
const canHoverHitTest =
|
|
1724
|
-
!(state.nodes.length > massiveGraphNodeThreshold && state.transform.scale < 0.
|
|
1858
|
+
!(state.nodes.length > massiveGraphNodeThreshold && state.transform.scale < 0.06)
|
|
1725
1859
|
const shouldHitTest = canHoverHitTest &&
|
|
1726
1860
|
(state.pointer.down || now - state.lastHoverHitAt >= hoverHitTestIntervalMs)
|
|
1727
1861
|
if (shouldHitTest) {
|
package/package.json
CHANGED