@andespindola/brainlink 0.1.0-beta.116 → 0.1.0-beta.118
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/dist/application/frontend/client-js.js +139 -176
- package/package.json +1 -1
|
@@ -6,7 +6,10 @@ const massiveGraphNodeThreshold = 20000
|
|
|
6
6
|
const largeGraphEdgeRenderLimit = 120000
|
|
7
7
|
const renderNodeBudget = 900
|
|
8
8
|
const zoomedMassiveRenderNodeBudget = 2200
|
|
9
|
-
const
|
|
9
|
+
const massiveOverviewRenderNodeBudget = 1800
|
|
10
|
+
const massiveOverviewScaleThreshold = 0.065
|
|
11
|
+
const massiveSegmentedScaleThreshold = 0.45
|
|
12
|
+
const massiveSegmentRepresentativeBudget = 760
|
|
10
13
|
const massiveAutoFitMacroScale = 0.018
|
|
11
14
|
const minNodePixelRadius = 2.3
|
|
12
15
|
const viewportPaddingPx = 280
|
|
@@ -17,7 +20,6 @@ const zoomRecoveryGuardMs = 4200
|
|
|
17
20
|
const meshEdgeScaleThreshold = 0.09
|
|
18
21
|
const meshEdgeMinBudget = 140
|
|
19
22
|
const meshEdgeMaxBudget = 1400
|
|
20
|
-
const layeredCoreScaleThreshold = 0.55
|
|
21
23
|
const dragNeighborhoodMaxAffected = 180
|
|
22
24
|
const dragSettleRounds = 3
|
|
23
25
|
const wheelZoomExponent = 0.0009
|
|
@@ -42,7 +44,6 @@ const state = {
|
|
|
42
44
|
visibleEdges: [],
|
|
43
45
|
renderNodes: [],
|
|
44
46
|
renderEdges: [],
|
|
45
|
-
renderNodeDepthProjectionById: new Map(),
|
|
46
47
|
nodeDegrees: new Map(),
|
|
47
48
|
selected: null,
|
|
48
49
|
hovered: null,
|
|
@@ -73,7 +74,6 @@ const state = {
|
|
|
73
74
|
lastHoverHitAt: 0,
|
|
74
75
|
lastManualZoomAt: 0,
|
|
75
76
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
76
|
-
depthProjectionActive: false,
|
|
77
77
|
zoomTransition: {
|
|
78
78
|
active: false,
|
|
79
79
|
source: 'generic',
|
|
@@ -635,12 +635,6 @@ const createVisibleEdgeLookup = edges => {
|
|
|
635
635
|
return lookup
|
|
636
636
|
}
|
|
637
637
|
|
|
638
|
-
const isClusterInViewport = (cluster, viewport) =>
|
|
639
|
-
cluster.x >= viewport.minX &&
|
|
640
|
-
cluster.x <= viewport.maxX &&
|
|
641
|
-
cluster.y >= viewport.minY &&
|
|
642
|
-
cluster.y <= viewport.maxY
|
|
643
|
-
|
|
644
638
|
const edgeBudgetForCurrentFrame = () => {
|
|
645
639
|
const zoom = state.transform.scale
|
|
646
640
|
if (zoom < 0.12) return 380
|
|
@@ -652,112 +646,24 @@ const edgeBudgetForCurrentFrame = () => {
|
|
|
652
646
|
return 7600
|
|
653
647
|
}
|
|
654
648
|
|
|
655
|
-
const clusterBudgetForScale = (scale) => {
|
|
656
|
-
if (scale < 0.008) return 90
|
|
657
|
-
if (scale < 0.014) return 150
|
|
658
|
-
if (scale < 0.022) return 240
|
|
659
|
-
if (scale < 0.035) return 360
|
|
660
|
-
return 520
|
|
661
|
-
}
|
|
662
|
-
|
|
663
649
|
const nodeBudgetForScale = (scale) => {
|
|
664
|
-
if (scale < 0.035) return 220
|
|
665
|
-
if (scale < 0.06) return 360
|
|
666
|
-
if (scale < 0.09) return 520
|
|
667
|
-
if (scale < 0.14) return 720
|
|
668
650
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
651
|
+
if (scale < massiveOverviewScaleThreshold) return massiveOverviewRenderNodeBudget
|
|
652
|
+
if (scale < 0.09) return 1600
|
|
653
|
+
if (scale < 0.14) return 1800
|
|
669
654
|
if (scale < 0.28) return renderNodeBudget
|
|
670
655
|
if (scale < 0.45) return 1100
|
|
671
656
|
if (scale < 0.7) return 1400
|
|
672
657
|
if (scale < 1.05) return 1800
|
|
673
658
|
return zoomedMassiveRenderNodeBudget
|
|
674
659
|
}
|
|
660
|
+
if (scale < 0.035) return 220
|
|
661
|
+
if (scale < 0.06) return 360
|
|
662
|
+
if (scale < 0.09) return 520
|
|
663
|
+
if (scale < 0.14) return 720
|
|
675
664
|
return renderNodeBudget
|
|
676
665
|
}
|
|
677
666
|
|
|
678
|
-
const layerFocusForScale = (scale) => {
|
|
679
|
-
const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
|
|
680
|
-
const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
|
|
681
|
-
const shellWidth = Math.max(0.24, 0.46 - normalized * 0.16)
|
|
682
|
-
const coreRadius = Math.max(0.06, 0.1 + normalized * 0.22)
|
|
683
|
-
const coreRatio = Math.max(0.2, Math.min(0.72, 0.24 + normalized * 0.48))
|
|
684
|
-
|
|
685
|
-
return { shellCenter, shellWidth, coreRadius, coreRatio }
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const selectLayeredNodesForScale = (sourceNodes, targetCount) => {
|
|
689
|
-
const hub = state.primaryHub
|
|
690
|
-
if (!hub || sourceNodes.length <= 1200 || state.visibleNodes.length <= massiveGraphNodeThreshold) {
|
|
691
|
-
return sourceNodes
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
let maxDistance = 0
|
|
695
|
-
const distances = sourceNodes.map((node) => {
|
|
696
|
-
const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
|
|
697
|
-
if (distance > maxDistance) {
|
|
698
|
-
maxDistance = distance
|
|
699
|
-
}
|
|
700
|
-
return { node, distance }
|
|
701
|
-
})
|
|
702
|
-
|
|
703
|
-
if (maxDistance <= 0.001) {
|
|
704
|
-
return sourceNodes
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
const focus = layerFocusForScale(state.transform.scale)
|
|
708
|
-
const normalizedRows = distances.map((item) => ({
|
|
709
|
-
...item,
|
|
710
|
-
normalized: item.distance / maxDistance
|
|
711
|
-
}))
|
|
712
|
-
const desired = Math.max(260, Math.min(sourceNodes.length, targetCount * 2))
|
|
713
|
-
const coreTarget = Math.max(36, Math.min(desired - 8, Math.floor(desired * focus.coreRatio)))
|
|
714
|
-
const shellTarget = Math.max(12, desired - coreTarget)
|
|
715
|
-
const shellHalf = focus.shellWidth / 2
|
|
716
|
-
|
|
717
|
-
const coreNodes = normalizedRows
|
|
718
|
-
.filter((item) => item.normalized <= focus.coreRadius)
|
|
719
|
-
.sort((left, right) => {
|
|
720
|
-
const leftScore = state.nodeDegrees.get(left.node.id) ?? 0
|
|
721
|
-
const rightScore = state.nodeDegrees.get(right.node.id) ?? 0
|
|
722
|
-
if (leftScore !== rightScore) return rightScore - leftScore
|
|
723
|
-
return left.node.id.localeCompare(right.node.id)
|
|
724
|
-
})
|
|
725
|
-
.slice(0, coreTarget)
|
|
726
|
-
.map((item) => item.node)
|
|
727
|
-
|
|
728
|
-
const shellNodes = normalizedRows
|
|
729
|
-
.sort((left, right) => {
|
|
730
|
-
const leftDelta = Math.abs(left.normalized - focus.shellCenter)
|
|
731
|
-
const rightDelta = Math.abs(right.normalized - focus.shellCenter)
|
|
732
|
-
const leftInside = leftDelta <= shellHalf ? 0 : 1
|
|
733
|
-
const rightInside = rightDelta <= shellHalf ? 0 : 1
|
|
734
|
-
if (leftInside !== rightInside) return leftInside - rightInside
|
|
735
|
-
if (leftDelta !== rightDelta) return leftDelta - rightDelta
|
|
736
|
-
const leftScore = state.nodeDegrees.get(left.node.id) ?? 0
|
|
737
|
-
const rightScore = state.nodeDegrees.get(right.node.id) ?? 0
|
|
738
|
-
if (leftScore !== rightScore) return rightScore - leftScore
|
|
739
|
-
return left.node.id.localeCompare(right.node.id)
|
|
740
|
-
})
|
|
741
|
-
.slice(0, shellTarget)
|
|
742
|
-
.map((item) => item.node)
|
|
743
|
-
|
|
744
|
-
const merged = []
|
|
745
|
-
const ids = new Set()
|
|
746
|
-
const pushUnique = (node) => {
|
|
747
|
-
if (!node || ids.has(node.id)) return
|
|
748
|
-
ids.add(node.id)
|
|
749
|
-
merged.push(node)
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
if (state.transform.scale >= layeredCoreScaleThreshold) {
|
|
753
|
-
pushUnique(hub)
|
|
754
|
-
}
|
|
755
|
-
for (let index = 0; index < coreNodes.length; index += 1) pushUnique(coreNodes[index])
|
|
756
|
-
for (let index = 0; index < shellNodes.length; index += 1) pushUnique(shellNodes[index])
|
|
757
|
-
|
|
758
|
-
return merged.length > 0 ? merged : sourceNodes
|
|
759
|
-
}
|
|
760
|
-
|
|
761
667
|
const viewportCenterWorldPoint = () => {
|
|
762
668
|
const viewport = worldViewportBounds()
|
|
763
669
|
return {
|
|
@@ -857,31 +763,6 @@ const selectStableSampleNodes = (sourceNodes, limit) => {
|
|
|
857
763
|
.slice(0, limit)
|
|
858
764
|
}
|
|
859
765
|
|
|
860
|
-
const selectAccessBridgeNodes = (sourceNodes, limit) => {
|
|
861
|
-
if (limit <= 0 || sourceNodes.length === 0) {
|
|
862
|
-
return []
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const now = performance.now()
|
|
866
|
-
const cursorPoint = cursorWorldPoint()
|
|
867
|
-
const recentZoomFocus =
|
|
868
|
-
now - state.lastZoomFocus.at <= 1200
|
|
869
|
-
? { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
870
|
-
: null
|
|
871
|
-
const anchor = cursorPoint ?? recentZoomFocus ?? viewportCenterWorldPoint()
|
|
872
|
-
return [...sourceNodes]
|
|
873
|
-
.sort((left, right) => {
|
|
874
|
-
const leftDistance = Math.hypot(left.x - anchor.x, left.y - anchor.y)
|
|
875
|
-
const rightDistance = Math.hypot(right.x - anchor.x, right.y - anchor.y)
|
|
876
|
-
if (leftDistance !== rightDistance) return leftDistance - rightDistance
|
|
877
|
-
const leftDegree = state.nodeDegrees.get(left.id) ?? 0
|
|
878
|
-
const rightDegree = state.nodeDegrees.get(right.id) ?? 0
|
|
879
|
-
if (leftDegree !== rightDegree) return rightDegree - leftDegree
|
|
880
|
-
return left.id.localeCompare(right.id)
|
|
881
|
-
})
|
|
882
|
-
.slice(0, limit)
|
|
883
|
-
}
|
|
884
|
-
|
|
885
766
|
const edgeIdentityKey = edge => {
|
|
886
767
|
if (!edge.target) return ''
|
|
887
768
|
const pair = edge.source < edge.target
|
|
@@ -1002,8 +883,8 @@ const edgeWidthFor = (edge, selectedEdge) => {
|
|
|
1002
883
|
const drawGraphEdge = (edge) => {
|
|
1003
884
|
const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
1004
885
|
ctx.beginPath()
|
|
1005
|
-
ctx.moveTo(
|
|
1006
|
-
ctx.lineTo(
|
|
886
|
+
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
887
|
+
ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
|
|
1007
888
|
ctx.strokeStyle = edgeStrokeFor(edge, selectedEdge)
|
|
1008
889
|
ctx.lineWidth = edgeWidthFor(edge, selectedEdge)
|
|
1009
890
|
ctx.stroke()
|
|
@@ -1017,8 +898,8 @@ const drawEdgeBatch = (edges, options) => {
|
|
|
1017
898
|
ctx.beginPath()
|
|
1018
899
|
for (let index = 0; index < edges.length; index += 1) {
|
|
1019
900
|
const edge = edges[index]
|
|
1020
|
-
ctx.moveTo(
|
|
1021
|
-
ctx.lineTo(
|
|
901
|
+
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
902
|
+
ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
|
|
1022
903
|
}
|
|
1023
904
|
ctx.strokeStyle = options.strokeStyle
|
|
1024
905
|
ctx.lineWidth = options.lineWidth
|
|
@@ -1073,13 +954,11 @@ const shouldDrawNodeLabels = (node, isSelected, isHovered) =>
|
|
|
1073
954
|
(state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
|
|
1074
955
|
|
|
1075
956
|
const drawSingleNode = (node, options = { drawLabel: true }) => {
|
|
1076
|
-
const radius = nodeRadius(node)
|
|
1077
|
-
const x =
|
|
1078
|
-
const y =
|
|
1079
|
-
const opacity = nodeRenderOpacity(node)
|
|
957
|
+
const radius = nodeRadius(node)
|
|
958
|
+
const x = node.x
|
|
959
|
+
const y = node.y
|
|
1080
960
|
const isSelected = state.selected?.id === node.id
|
|
1081
961
|
const isHovered = state.hovered?.id === node.id
|
|
1082
|
-
ctx.globalAlpha = opacity
|
|
1083
962
|
ctx.beginPath()
|
|
1084
963
|
ctx.arc(x, y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
|
|
1085
964
|
ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
|
|
@@ -1093,7 +972,7 @@ const drawSingleNode = (node, options = { drawLabel: true }) => {
|
|
|
1093
972
|
ctx.stroke()
|
|
1094
973
|
|
|
1095
974
|
if (options.drawLabel && shouldDrawNodeLabels(node, isSelected, isHovered)) {
|
|
1096
|
-
ctx.globalAlpha =
|
|
975
|
+
ctx.globalAlpha = 1
|
|
1097
976
|
ctx.fillStyle = graphTheme.label
|
|
1098
977
|
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
1099
978
|
ctx.textAlign = 'center'
|
|
@@ -1112,10 +991,10 @@ const drawNodeBatch = (nodes) => {
|
|
|
1112
991
|
if (drawHalos) {
|
|
1113
992
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
1114
993
|
const node = nodes[index]
|
|
1115
|
-
const radius = nodeRadius(node)
|
|
1116
|
-
const x =
|
|
1117
|
-
const y =
|
|
1118
|
-
ctx.globalAlpha =
|
|
994
|
+
const radius = nodeRadius(node)
|
|
995
|
+
const x = node.x
|
|
996
|
+
const y = node.y
|
|
997
|
+
ctx.globalAlpha = 0.5
|
|
1119
998
|
ctx.beginPath()
|
|
1120
999
|
ctx.arc(x, y, radius + 3, 0, Math.PI * 2)
|
|
1121
1000
|
ctx.fillStyle = graphTheme.nodeHalo
|
|
@@ -1126,11 +1005,10 @@ const drawNodeBatch = (nodes) => {
|
|
|
1126
1005
|
|
|
1127
1006
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
1128
1007
|
const node = nodes[index]
|
|
1129
|
-
const radius = nodeRadius(node)
|
|
1130
|
-
const x =
|
|
1131
|
-
const y =
|
|
1132
|
-
|
|
1133
|
-
ctx.globalAlpha = opacity
|
|
1008
|
+
const radius = nodeRadius(node)
|
|
1009
|
+
const x = node.x
|
|
1010
|
+
const y = node.y
|
|
1011
|
+
ctx.globalAlpha = 1
|
|
1134
1012
|
ctx.beginPath()
|
|
1135
1013
|
ctx.arc(x, y, radius, 0, Math.PI * 2)
|
|
1136
1014
|
ctx.fillStyle = graphTheme.node
|
|
@@ -1167,10 +1045,10 @@ const drawGraphNodes = () => {
|
|
|
1167
1045
|
ctx.textBaseline = 'top'
|
|
1168
1046
|
for (let index = 0; index < regularNodes.length; index += 1) {
|
|
1169
1047
|
const node = regularNodes[index]
|
|
1170
|
-
const x =
|
|
1171
|
-
const y =
|
|
1172
|
-
const radius = nodeRadius(node)
|
|
1173
|
-
ctx.globalAlpha =
|
|
1048
|
+
const x = node.x
|
|
1049
|
+
const y = node.y
|
|
1050
|
+
const radius = nodeRadius(node)
|
|
1051
|
+
ctx.globalAlpha = 1
|
|
1174
1052
|
ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
|
|
1175
1053
|
}
|
|
1176
1054
|
ctx.globalAlpha = 1
|
|
@@ -1224,10 +1102,10 @@ const drawGraphLabels = nodes => {
|
|
|
1224
1102
|
ctx.textBaseline = 'top'
|
|
1225
1103
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
1226
1104
|
const node = nodes[index]
|
|
1227
|
-
const x =
|
|
1228
|
-
const y =
|
|
1229
|
-
const radius = nodeRadius(node)
|
|
1230
|
-
ctx.globalAlpha =
|
|
1105
|
+
const x = node.x
|
|
1106
|
+
const y = node.y
|
|
1107
|
+
const radius = nodeRadius(node)
|
|
1108
|
+
ctx.globalAlpha = 1
|
|
1231
1109
|
ctx.fillText(node.title.slice(0, 34), x, y + radius + 8)
|
|
1232
1110
|
}
|
|
1233
1111
|
ctx.globalAlpha = 1
|
|
@@ -1435,6 +1313,82 @@ const sampleVisibleNodes = (limit = renderNodeBudget, sourceNodes = state.visibl
|
|
|
1435
1313
|
return nodes
|
|
1436
1314
|
}
|
|
1437
1315
|
|
|
1316
|
+
const sampleMassiveOverviewNodes = (limit) => {
|
|
1317
|
+
const sampled = sampleVisibleNodes(limit, state.visibleNodes)
|
|
1318
|
+
return ensureHubNodesInRenderedSet(sampled)
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
const representativeNodeFromBucket = bucket => {
|
|
1322
|
+
if (!bucket || bucket.length === 0) {
|
|
1323
|
+
return null
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
let representative = bucket[0]
|
|
1327
|
+
let representativeDegree = state.nodeDegrees.get(representative.id) ?? 0
|
|
1328
|
+
|
|
1329
|
+
for (let index = 1; index < bucket.length; index += 1) {
|
|
1330
|
+
const candidate = bucket[index]
|
|
1331
|
+
const candidateDegree = state.nodeDegrees.get(candidate.id) ?? 0
|
|
1332
|
+
if (candidateDegree <= representativeDegree) {
|
|
1333
|
+
continue
|
|
1334
|
+
}
|
|
1335
|
+
representative = candidate
|
|
1336
|
+
representativeDegree = candidateDegree
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
return representative
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
const sampleMassiveSegmentRepresentatives = (limit) => {
|
|
1343
|
+
const spatial = state.visibleNodeSpatial
|
|
1344
|
+
if (!spatial || spatial.buckets.size === 0 || limit <= 0) {
|
|
1345
|
+
return []
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const keys = [...spatial.buckets.keys()].sort()
|
|
1349
|
+
const maxNodes = Math.min(limit, keys.length)
|
|
1350
|
+
const step = Math.max(1, Math.ceil(keys.length / maxNodes))
|
|
1351
|
+
const representatives = []
|
|
1352
|
+
|
|
1353
|
+
for (let index = 0; index < keys.length && representatives.length < maxNodes; index += step) {
|
|
1354
|
+
const representative = representativeNodeFromBucket(spatial.buckets.get(keys[index]))
|
|
1355
|
+
if (representative) {
|
|
1356
|
+
representatives.push(representative)
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
return representatives
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const massiveSegmentRepresentativeLimit = (scale, limit) => {
|
|
1364
|
+
if (scale >= massiveSegmentedScaleThreshold) {
|
|
1365
|
+
return 0
|
|
1366
|
+
}
|
|
1367
|
+
if (scale < 0.09) {
|
|
1368
|
+
return Math.min(massiveSegmentRepresentativeBudget, Math.floor(limit * 0.5))
|
|
1369
|
+
}
|
|
1370
|
+
if (scale < 0.18) {
|
|
1371
|
+
return Math.min(620, Math.floor(limit * 0.42))
|
|
1372
|
+
}
|
|
1373
|
+
if (scale < 0.28) {
|
|
1374
|
+
return Math.min(460, Math.floor(limit * 0.34))
|
|
1375
|
+
}
|
|
1376
|
+
return Math.min(260, Math.floor(limit * 0.22))
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const sampleMassiveSegmentedNodes = (limit, viewport) => {
|
|
1380
|
+
const representativeLimit = massiveSegmentRepresentativeLimit(state.transform.scale, limit)
|
|
1381
|
+
const representatives = sampleMassiveSegmentRepresentatives(representativeLimit)
|
|
1382
|
+
const localLimit = Math.max(1, limit - representatives.length)
|
|
1383
|
+
const localMargin = Math.max(520, Math.min(5200, 780 / Math.max(state.transform.scale, 0.0001)))
|
|
1384
|
+
const localViewport = expandViewportBounds(viewport, localMargin)
|
|
1385
|
+
const localViewportNodes = viewportNodesFromSpatialIndex(localViewport)
|
|
1386
|
+
const localSource = localViewportNodes.length > 0 ? localViewportNodes : state.visibleNodes
|
|
1387
|
+
const localNodes = selectStableSampleNodes(localSource, localLimit)
|
|
1388
|
+
|
|
1389
|
+
return mergeUniqueNodes(representatives, localNodes, limit)
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1438
1392
|
const enrichSampleWithNeighbors = (nodes) => {
|
|
1439
1393
|
if (nodes.length === 0) {
|
|
1440
1394
|
return {
|
|
@@ -1724,7 +1678,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
|
1724
1678
|
return { min: 0.0085, max: 0.36 }
|
|
1725
1679
|
}
|
|
1726
1680
|
|
|
1727
|
-
const fitView = (options = { useFiltered: true,
|
|
1681
|
+
const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
|
|
1728
1682
|
const rect = canvas.getBoundingClientRect()
|
|
1729
1683
|
const width = Math.max(rect.width, 320)
|
|
1730
1684
|
const height = Math.max(rect.height, 320)
|
|
@@ -1777,12 +1731,12 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
1777
1731
|
markRenderDirty()
|
|
1778
1732
|
}
|
|
1779
1733
|
|
|
1780
|
-
const resetView = () => fitView({ useFiltered: false,
|
|
1734
|
+
const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
|
|
1781
1735
|
|
|
1782
1736
|
const focusPrimaryHub = () => {
|
|
1783
1737
|
const hub = state.primaryHub
|
|
1784
1738
|
if (!hub) {
|
|
1785
|
-
fitView({ useFiltered: true,
|
|
1739
|
+
fitView({ useFiltered: true, preferHubCenter: true })
|
|
1786
1740
|
return
|
|
1787
1741
|
}
|
|
1788
1742
|
|
|
@@ -2161,9 +2115,9 @@ const hitNode = point => {
|
|
|
2161
2115
|
const nodes = state.renderNodes
|
|
2162
2116
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
2163
2117
|
const node = nodes[index]
|
|
2164
|
-
const radius = nodeRadius(node)
|
|
2165
|
-
const x =
|
|
2166
|
-
const y =
|
|
2118
|
+
const radius = nodeRadius(node)
|
|
2119
|
+
const x = node.x
|
|
2120
|
+
const y = node.y
|
|
2167
2121
|
if (Math.hypot(point.x - x, point.y - y) <= radius + 5) return node
|
|
2168
2122
|
}
|
|
2169
2123
|
return null
|
|
@@ -2176,15 +2130,6 @@ const baseNodeRadius = node => {
|
|
|
2176
2130
|
|
|
2177
2131
|
const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
|
|
2178
2132
|
|
|
2179
|
-
const refreshRenderNodeDepthProjection = () => {
|
|
2180
|
-
state.renderNodeDepthProjectionById = new Map()
|
|
2181
|
-
}
|
|
2182
|
-
|
|
2183
|
-
const projectedNode = node => state.renderNodeDepthProjectionById.get(node.id) ?? null
|
|
2184
|
-
const nodeRenderX = node => projectedNode(node)?.x ?? node.x
|
|
2185
|
-
const nodeRenderY = node => projectedNode(node)?.y ?? node.y
|
|
2186
|
-
const nodeRenderScale = node => projectedNode(node)?.scale ?? 1
|
|
2187
|
-
const nodeRenderOpacity = node => projectedNode(node)?.opacity ?? 1
|
|
2188
2133
|
const worldViewportBounds = () => {
|
|
2189
2134
|
const width = Math.max(state.viewport.width, 320)
|
|
2190
2135
|
const height = Math.max(state.viewport.height, 320)
|
|
@@ -2264,9 +2209,26 @@ const computeRenderVisibility = () => {
|
|
|
2264
2209
|
}
|
|
2265
2210
|
|
|
2266
2211
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
2267
|
-
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2268
|
-
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
2269
2212
|
const sampleLimit = nodeBudgetForScale(state.transform.scale)
|
|
2213
|
+
if (state.transform.scale < massiveOverviewScaleThreshold) {
|
|
2214
|
+
const overviewNodes = sampleMassiveOverviewNodes(sampleLimit)
|
|
2215
|
+
const overviewIds = new Set(overviewNodes.map((node) => node.id))
|
|
2216
|
+
state.renderNodes = overviewNodes
|
|
2217
|
+
state.renderEdges = withMeshEdges(overviewNodes, collectVisibleEdgesForNodes(overviewIds))
|
|
2218
|
+
return
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2222
|
+
const segmentedNodes =
|
|
2223
|
+
state.transform.scale < massiveSegmentedScaleThreshold
|
|
2224
|
+
? sampleMassiveSegmentedNodes(sampleLimit, viewport)
|
|
2225
|
+
: []
|
|
2226
|
+
const sourceNodes =
|
|
2227
|
+
segmentedNodes.length > 0
|
|
2228
|
+
? segmentedNodes
|
|
2229
|
+
: viewportNodes.length > 0
|
|
2230
|
+
? viewportNodes
|
|
2231
|
+
: sampleMassiveOverviewNodes(sampleLimit)
|
|
2270
2232
|
const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
|
|
2271
2233
|
const carryViewport = expandViewportBounds(viewport, carryMargin)
|
|
2272
2234
|
const carryOverLimit = Math.max(180, Math.min(sampleLimit, Math.floor(sampleLimit * 0.5)))
|
|
@@ -2441,10 +2403,11 @@ const render = now => {
|
|
|
2441
2403
|
|
|
2442
2404
|
computeRenderVisibility()
|
|
2443
2405
|
tick(delta, now)
|
|
2444
|
-
refreshRenderNodeDepthProjection()
|
|
2445
2406
|
const hasVisibleNodeOnScreen = state.renderNodes.some((node) => isNodeVisibleOnScreen(node, width, height))
|
|
2446
2407
|
const manualZoomGuardActive = now - state.lastManualZoomAt < zoomRecoveryGuardMs
|
|
2447
|
-
const allowViewportAutoRecovery =
|
|
2408
|
+
const allowViewportAutoRecovery =
|
|
2409
|
+
state.nodes.length <= massiveGraphNodeThreshold ||
|
|
2410
|
+
state.transform.scale >= massiveOverviewScaleThreshold
|
|
2448
2411
|
if (allowViewportAutoRecovery && !hasVisibleNodeOnScreen && state.renderNodes.length > 0 && !manualZoomGuardActive) {
|
|
2449
2412
|
state.offscreenFrameCount += 1
|
|
2450
2413
|
if (state.offscreenFrameCount >= 22 && !state.recoveringViewport) {
|
package/package.json
CHANGED