@andespindola/brainlink 0.1.0-beta.84 → 0.1.0-beta.86
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 +3 -2
- package/dist/application/frontend/client-js.js +242 -82
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,10 +84,11 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
84
84
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
85
85
|
- Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
|
|
86
86
|
- WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
|
|
87
|
+
- Graph zoom-out renders hierarchical ecosystem subgraphs: macro clusters of up to 1000 notes expand near the user's focus into smaller graph meshes before individual notes are rendered.
|
|
87
88
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
88
89
|
- Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
|
|
89
90
|
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
|
|
90
|
-
- Zoomed-out graph LOD
|
|
91
|
+
- Zoomed-out graph LOD renders nested subgraphs and progressively expands only the focused cluster as zoom increases, including very large vaults.
|
|
91
92
|
- Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
|
|
92
93
|
- Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
|
|
93
94
|
- Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
|
|
@@ -601,7 +602,7 @@ The graph UI shows:
|
|
|
601
602
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
602
603
|
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
603
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
|
|
604
|
-
-
|
|
605
|
+
- graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes, zoom-in expands only focused clusters into 250-note and 60-note subgraphs with aggregated real links, then progressively raises the focused node budget so local areas keep nearby notes and links visible
|
|
605
606
|
|
|
606
607
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
607
608
|
|
|
@@ -21,14 +21,19 @@ 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 ecosystemGroupSize = 1000
|
|
25
|
+
const ecosystemGroupSizes = [1000, 250, 60]
|
|
26
|
+
const ecosystemClusterEdgeLimit = 520
|
|
27
|
+
const ecosystemClusterScaleThreshold = 0.32
|
|
28
|
+
const ecosystemSubgraphScaleThreshold = 0.18
|
|
29
|
+
const ecosystemMicroScaleThreshold = 0.08
|
|
30
|
+
const ecosystemFocusedParentLimit = 3
|
|
25
31
|
const zoomRecoveryGuardMs = 4200
|
|
26
32
|
const zoomCapTargetViewportShare = 0.72
|
|
27
33
|
const meshEdgeScaleThreshold = 0.09
|
|
28
34
|
const meshEdgeMinBudget = 140
|
|
29
35
|
const meshEdgeMaxBudget = 1400
|
|
30
36
|
const layeredCoreScaleThreshold = 0.55
|
|
31
|
-
const massiveOverviewClusterScaleThreshold = 0.035
|
|
32
37
|
const dragNeighborhoodMaxAffected = 180
|
|
33
38
|
const dragSettleRounds = 3
|
|
34
39
|
const wheelZoomExponent = 0.0018
|
|
@@ -44,6 +49,7 @@ const state = {
|
|
|
44
49
|
renderNodes: [],
|
|
45
50
|
renderEdges: [],
|
|
46
51
|
renderClusters: [],
|
|
52
|
+
renderClusterEdges: [],
|
|
47
53
|
nodeDegrees: new Map(),
|
|
48
54
|
selected: null,
|
|
49
55
|
hovered: null,
|
|
@@ -65,7 +71,8 @@ const state = {
|
|
|
65
71
|
lastViewportKey: '',
|
|
66
72
|
visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
|
|
67
73
|
visibleEdgeByNode: new Map(),
|
|
68
|
-
|
|
74
|
+
ecosystemClusters: [],
|
|
75
|
+
ecosystemClustersBySize: new Map(),
|
|
69
76
|
macroCenter: { x: 0, y: 0 },
|
|
70
77
|
macroRepresentative: null,
|
|
71
78
|
primaryHub: null,
|
|
@@ -562,7 +569,11 @@ const recomputeVisibility = () => {
|
|
|
562
569
|
state.visibleEdges = limitedEdges
|
|
563
570
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
564
571
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
565
|
-
|
|
572
|
+
const ecosystemGraph = nodes.length > 1
|
|
573
|
+
? buildEcosystemGraph(nodes)
|
|
574
|
+
: { clusters: [], clustersBySize: new Map() }
|
|
575
|
+
state.ecosystemClusters = ecosystemGraph.clusters
|
|
576
|
+
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
566
577
|
const primaryHub = rankedHubNodes()[0] ?? null
|
|
567
578
|
state.primaryHub = primaryHub
|
|
568
579
|
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
@@ -679,68 +690,219 @@ const createVisibleEdgeLookup = edges => {
|
|
|
679
690
|
return lookup
|
|
680
691
|
}
|
|
681
692
|
|
|
682
|
-
const
|
|
683
|
-
if (
|
|
684
|
-
return
|
|
693
|
+
const ecosystemKeyForNode = node => {
|
|
694
|
+
if (typeof node.segment === 'string' && node.segment.trim()) {
|
|
695
|
+
return node.segment.trim()
|
|
685
696
|
}
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
if (!bounds) {
|
|
689
|
-
return []
|
|
697
|
+
if (typeof node.group === 'string' && node.group.trim()) {
|
|
698
|
+
return node.group.trim()
|
|
690
699
|
}
|
|
700
|
+
const pathParts = String(node.path || '')
|
|
701
|
+
.split('/')
|
|
702
|
+
.filter(part => part.trim())
|
|
703
|
+
.slice(0, 2)
|
|
704
|
+
return pathParts.length > 0 ? pathParts.join('/') : 'root'
|
|
705
|
+
}
|
|
691
706
|
|
|
692
|
-
|
|
693
|
-
const
|
|
694
|
-
|
|
707
|
+
const compareNodesForEcosystem = (left, right) => {
|
|
708
|
+
const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
|
|
709
|
+
if (keyComparison !== 0) return keyComparison
|
|
710
|
+
const leftDegree = state.nodeDegrees.get(left.id) ?? 0
|
|
711
|
+
const rightDegree = state.nodeDegrees.get(right.id) ?? 0
|
|
712
|
+
if (leftDegree !== rightDegree) return rightDegree - leftDegree
|
|
713
|
+
return String(left.title || left.id).localeCompare(String(right.title || right.id))
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const selectEcosystemRepresentative = nodes => {
|
|
717
|
+
let representative = nodes[0] ?? null
|
|
718
|
+
let representativeScore = Number.NEGATIVE_INFINITY
|
|
695
719
|
|
|
696
720
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
697
721
|
const node = nodes[index]
|
|
698
|
-
const
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
722
|
+
const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
|
|
723
|
+
if (score > representativeScore) {
|
|
724
|
+
representative = node
|
|
725
|
+
representativeScore = score
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return representative
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const buildEcosystemCluster = (nodes, index) => {
|
|
733
|
+
const count = Math.max(nodes.length, 1)
|
|
734
|
+
const sum = nodes.reduce((accumulator, node) => ({
|
|
735
|
+
x: accumulator.x + node.x,
|
|
736
|
+
y: accumulator.y + node.y
|
|
737
|
+
}), { x: 0, y: 0 })
|
|
738
|
+
const representative = selectEcosystemRepresentative(nodes)
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
id: 'ecosystem-' + index,
|
|
742
|
+
x: sum.x / count,
|
|
743
|
+
y: sum.y / count,
|
|
744
|
+
count,
|
|
745
|
+
nodeIds: nodes.map(node => node.id),
|
|
746
|
+
representative,
|
|
747
|
+
label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
|
|
752
|
+
const clusters = []
|
|
753
|
+
const clusterByNodeId = new Map()
|
|
754
|
+
|
|
755
|
+
for (let offset = 0; offset < sortedNodes.length; offset += size) {
|
|
756
|
+
const clusterNodes = sortedNodes.slice(offset, offset + size)
|
|
757
|
+
const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
|
|
758
|
+
const cluster = {
|
|
759
|
+
...buildEcosystemCluster(clusterNodes, clusters.length),
|
|
760
|
+
id: 'ecosystem-' + size + '-' + clusters.length,
|
|
761
|
+
size,
|
|
762
|
+
parentId: parentCluster?.id ?? null
|
|
763
|
+
}
|
|
764
|
+
clusters.push(cluster)
|
|
765
|
+
for (let index = 0; index < clusterNodes.length; index += 1) {
|
|
766
|
+
clusterByNodeId.set(clusterNodes[index].id, cluster)
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return { clusters, clusterByNodeId }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const buildEcosystemGraph = (nodes) => {
|
|
774
|
+
if (nodes.length === 0) {
|
|
775
|
+
return { clusters: [], clustersBySize: new Map() }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
|
|
779
|
+
const clustersBySize = new Map()
|
|
780
|
+
let parentLookup = null
|
|
781
|
+
|
|
782
|
+
for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
|
|
783
|
+
const size = ecosystemGroupSizes[index]
|
|
784
|
+
const level = buildEcosystemLevel(sortedNodes, size, parentLookup)
|
|
785
|
+
clustersBySize.set(size, level.clusters)
|
|
786
|
+
parentLookup = level.clusterByNodeId
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
|
|
791
|
+
clustersBySize
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const isClusterInViewport = (cluster, viewport) =>
|
|
796
|
+
cluster.x >= viewport.minX &&
|
|
797
|
+
cluster.x <= viewport.maxX &&
|
|
798
|
+
cluster.y >= viewport.minY &&
|
|
799
|
+
cluster.y <= viewport.maxY
|
|
800
|
+
|
|
801
|
+
const filterEcosystemClustersByViewport = (clusters, viewport) => {
|
|
802
|
+
const visible = clusters.filter(cluster => isClusterInViewport(cluster, viewport))
|
|
803
|
+
return visible.length > 0 ? visible : [...clusters]
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const ecosystemFocusPoint = () => {
|
|
807
|
+
const now = performance.now()
|
|
808
|
+
if (now - state.lastZoomFocus.at <= 1800) {
|
|
809
|
+
return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
810
|
+
}
|
|
811
|
+
return viewportCenterWorldPoint()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
|
|
815
|
+
clusters
|
|
816
|
+
.map(cluster => ({
|
|
817
|
+
cluster,
|
|
818
|
+
distance: Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y)
|
|
819
|
+
}))
|
|
820
|
+
.sort((left, right) => left.distance - right.distance)
|
|
821
|
+
.slice(0, limit)
|
|
822
|
+
.map(item => item.cluster.id)
|
|
823
|
+
|
|
824
|
+
const ecosystemPlanForScale = scale => {
|
|
825
|
+
if (scale <= ecosystemMicroScaleThreshold) {
|
|
826
|
+
return { baseSize: 1000, childSize: null }
|
|
827
|
+
}
|
|
828
|
+
if (scale <= ecosystemSubgraphScaleThreshold) {
|
|
829
|
+
return { baseSize: 1000, childSize: 250 }
|
|
830
|
+
}
|
|
831
|
+
return { baseSize: 250, childSize: 60 }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const selectHierarchicalEcosystemClusters = viewport => {
|
|
835
|
+
const plan = ecosystemPlanForScale(state.transform.scale)
|
|
836
|
+
const baseClusters = state.ecosystemClustersBySize.get(plan.baseSize) ?? state.ecosystemClusters
|
|
837
|
+
const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
|
|
838
|
+
|
|
839
|
+
if (!plan.childSize) {
|
|
840
|
+
return visibleBaseClusters
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const focusPoint = ecosystemFocusPoint()
|
|
844
|
+
const expandedParentIds = new Set(nearestEcosystemParentIds(
|
|
845
|
+
visibleBaseClusters,
|
|
846
|
+
focusPoint,
|
|
847
|
+
ecosystemFocusedParentLimit
|
|
848
|
+
))
|
|
849
|
+
const childClusters = state.ecosystemClustersBySize.get(plan.childSize) ?? []
|
|
850
|
+
const visibleChildClusters = childClusters.filter(cluster =>
|
|
851
|
+
expandedParentIds.has(cluster.parentId) &&
|
|
852
|
+
isClusterInViewport(cluster, viewport)
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if (visibleChildClusters.length === 0) {
|
|
856
|
+
return visibleBaseClusters
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return [
|
|
860
|
+
...visibleBaseClusters.filter(cluster => !expandedParentIds.has(cluster.id)),
|
|
861
|
+
...visibleChildClusters
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const ecosystemEdgesForClusters = clusters => {
|
|
866
|
+
const clusterByNodeId = new Map()
|
|
867
|
+
for (let clusterIndex = 0; clusterIndex < clusters.length; clusterIndex += 1) {
|
|
868
|
+
const cluster = clusters[clusterIndex]
|
|
869
|
+
for (let nodeIndex = 0; nodeIndex < cluster.nodeIds.length; nodeIndex += 1) {
|
|
870
|
+
clusterByNodeId.set(cluster.nodeIds[nodeIndex], cluster)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const edgeByClusterPair = new Map()
|
|
875
|
+
for (let index = 0; index < state.visibleEdges.length; index += 1) {
|
|
876
|
+
const edge = state.visibleEdges[index]
|
|
877
|
+
const sourceCluster = clusterByNodeId.get(edge.source)
|
|
878
|
+
const targetCluster = clusterByNodeId.get(edge.target)
|
|
879
|
+
if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
|
|
880
|
+
continue
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const orderedIds = sourceCluster.id < targetCluster.id
|
|
884
|
+
? [sourceCluster.id, targetCluster.id]
|
|
885
|
+
: [targetCluster.id, sourceCluster.id]
|
|
886
|
+
const key = orderedIds.join(':')
|
|
887
|
+
const current = edgeByClusterPair.get(key)
|
|
703
888
|
if (current) {
|
|
704
|
-
current.
|
|
705
|
-
current.sumX += node.x
|
|
706
|
-
current.sumY += node.y
|
|
707
|
-
if (degree > current.degree) {
|
|
708
|
-
current.representative = node
|
|
709
|
-
current.degree = degree
|
|
710
|
-
}
|
|
889
|
+
current.weight += edgeWeight(edge)
|
|
711
890
|
continue
|
|
712
891
|
}
|
|
713
892
|
|
|
714
|
-
|
|
893
|
+
edgeByClusterPair.set(key, {
|
|
715
894
|
id: key,
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
representative: node,
|
|
720
|
-
degree
|
|
895
|
+
sourceCluster,
|
|
896
|
+
targetCluster,
|
|
897
|
+
weight: edgeWeight(edge)
|
|
721
898
|
})
|
|
722
899
|
}
|
|
723
900
|
|
|
724
|
-
return Array.from(
|
|
725
|
-
.sort((left, right) => right.
|
|
726
|
-
.slice(0,
|
|
727
|
-
.map((cluster) => ({
|
|
728
|
-
id: cluster.id,
|
|
729
|
-
x: cluster.sumX / Math.max(cluster.count, 1),
|
|
730
|
-
y: cluster.sumY / Math.max(cluster.count, 1),
|
|
731
|
-
count: cluster.count,
|
|
732
|
-
representative: cluster.representative
|
|
733
|
-
}))
|
|
901
|
+
return Array.from(edgeByClusterPair.values())
|
|
902
|
+
.sort((left, right) => right.weight - left.weight)
|
|
903
|
+
.slice(0, ecosystemClusterEdgeLimit)
|
|
734
904
|
}
|
|
735
905
|
|
|
736
|
-
const filterOverviewClustersByViewport = viewport =>
|
|
737
|
-
state.overviewClusters.filter((cluster) =>
|
|
738
|
-
cluster.x >= viewport.minX &&
|
|
739
|
-
cluster.x <= viewport.maxX &&
|
|
740
|
-
cluster.y >= viewport.minY &&
|
|
741
|
-
cluster.y <= viewport.maxY
|
|
742
|
-
)
|
|
743
|
-
|
|
744
906
|
const edgeBudgetForCurrentFrame = () => {
|
|
745
907
|
const zoom = state.transform.scale
|
|
746
908
|
if (zoom < 0.12) return 380
|
|
@@ -775,14 +937,6 @@ const nodeBudgetForScale = (scale) => {
|
|
|
775
937
|
return renderNodeBudget
|
|
776
938
|
}
|
|
777
939
|
|
|
778
|
-
const massiveLowZoomNodeBudgetForScale = (scale) => {
|
|
779
|
-
if (scale < 0.004) return 780
|
|
780
|
-
if (scale < 0.01) return 860
|
|
781
|
-
if (scale < 0.02) return 900
|
|
782
|
-
if (scale < 0.035) return 900
|
|
783
|
-
return renderNodeBudget
|
|
784
|
-
}
|
|
785
|
-
|
|
786
940
|
const layerFocusForScale = (scale) => {
|
|
787
941
|
const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
|
|
788
942
|
const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
|
|
@@ -2316,6 +2470,7 @@ const computeRenderVisibility = () => {
|
|
|
2316
2470
|
}
|
|
2317
2471
|
state.lastViewportKey = viewportKey
|
|
2318
2472
|
state.renderVisibilityDirty = false
|
|
2473
|
+
state.renderClusterEdges = []
|
|
2319
2474
|
|
|
2320
2475
|
const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
|
|
2321
2476
|
|
|
@@ -2339,12 +2494,24 @@ const computeRenderVisibility = () => {
|
|
|
2339
2494
|
state.renderNodes = []
|
|
2340
2495
|
}
|
|
2341
2496
|
state.renderEdges = []
|
|
2497
|
+
state.renderClusterEdges = []
|
|
2498
|
+
return
|
|
2499
|
+
}
|
|
2500
|
+
|
|
2501
|
+
if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
|
|
2502
|
+
const clusters = selectHierarchicalEcosystemClusters(viewport)
|
|
2503
|
+
.sort((left, right) => right.count - left.count)
|
|
2504
|
+
state.renderClusters = clusters
|
|
2505
|
+
state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
|
|
2506
|
+
state.renderNodes = []
|
|
2507
|
+
state.renderEdges = []
|
|
2342
2508
|
return
|
|
2343
2509
|
}
|
|
2344
2510
|
|
|
2345
2511
|
if (state.visibleNodes.length <= 2000) {
|
|
2346
2512
|
state.renderNodes = state.visibleNodes
|
|
2347
2513
|
state.renderClusters = []
|
|
2514
|
+
state.renderClusterEdges = []
|
|
2348
2515
|
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
2349
2516
|
state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
|
|
2350
2517
|
return
|
|
@@ -2352,30 +2519,6 @@ const computeRenderVisibility = () => {
|
|
|
2352
2519
|
|
|
2353
2520
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
2354
2521
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2355
|
-
if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
|
|
2356
|
-
const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
|
|
2357
|
-
const overviewClusters = filterOverviewClustersByViewport(viewport)
|
|
2358
|
-
.sort((left, right) => right.count - left.count)
|
|
2359
|
-
.slice(0, overviewLimit)
|
|
2360
|
-
if (overviewClusters.length > 0) {
|
|
2361
|
-
const overviewNodes = representativeNodesFromClusters(
|
|
2362
|
-
overviewClusters,
|
|
2363
|
-
overviewLimit
|
|
2364
|
-
)
|
|
2365
|
-
const anchoredNodes = includeHubPreviewNeighborhood(
|
|
2366
|
-
overviewNodes,
|
|
2367
|
-
Math.min(renderNodeBudget, overviewLimit)
|
|
2368
|
-
)
|
|
2369
|
-
const enriched = enrichSampleWithNeighbors(anchoredNodes)
|
|
2370
|
-
const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
|
|
2371
|
-
const previewIds = new Set(previewNodes.map((node) => node.id))
|
|
2372
|
-
const previewEdges = collectVisibleEdgesForNodes(previewIds)
|
|
2373
|
-
state.renderClusters = []
|
|
2374
|
-
state.renderNodes = previewNodes
|
|
2375
|
-
state.renderEdges = previewEdges
|
|
2376
|
-
return
|
|
2377
|
-
}
|
|
2378
|
-
}
|
|
2379
2522
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
2380
2523
|
const sampleLimit = nodeBudgetForScale(state.transform.scale)
|
|
2381
2524
|
const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
|
|
@@ -2421,6 +2564,7 @@ const computeRenderVisibility = () => {
|
|
|
2421
2564
|
}
|
|
2422
2565
|
|
|
2423
2566
|
state.renderClusters = []
|
|
2567
|
+
state.renderClusterEdges = []
|
|
2424
2568
|
state.renderNodes = sampledNodes
|
|
2425
2569
|
state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
|
|
2426
2570
|
return
|
|
@@ -2430,6 +2574,7 @@ const computeRenderVisibility = () => {
|
|
|
2430
2574
|
const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
|
|
2431
2575
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
2432
2576
|
state.renderClusters = []
|
|
2577
|
+
state.renderClusterEdges = []
|
|
2433
2578
|
state.renderNodes = sampled
|
|
2434
2579
|
state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
|
|
2435
2580
|
return
|
|
@@ -2439,11 +2584,13 @@ const computeRenderVisibility = () => {
|
|
|
2439
2584
|
const clusters = clusterViewportNodes(viewportNodes)
|
|
2440
2585
|
if (clusters.length > 0) {
|
|
2441
2586
|
state.renderClusters = []
|
|
2587
|
+
state.renderClusterEdges = []
|
|
2442
2588
|
state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
|
|
2443
2589
|
state.renderEdges = []
|
|
2444
2590
|
return
|
|
2445
2591
|
}
|
|
2446
2592
|
state.renderClusters = []
|
|
2593
|
+
state.renderClusterEdges = []
|
|
2447
2594
|
const stride = viewportNodeStride()
|
|
2448
2595
|
const picked = []
|
|
2449
2596
|
|
|
@@ -2467,6 +2614,7 @@ const computeRenderVisibility = () => {
|
|
|
2467
2614
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2468
2615
|
state.renderNodes = fallbackNodes
|
|
2469
2616
|
state.renderClusters = []
|
|
2617
|
+
state.renderClusterEdges = []
|
|
2470
2618
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2471
2619
|
return
|
|
2472
2620
|
}
|
|
@@ -2482,6 +2630,7 @@ const computeRenderVisibility = () => {
|
|
|
2482
2630
|
const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
|
|
2483
2631
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2484
2632
|
state.renderClusters = []
|
|
2633
|
+
state.renderClusterEdges = []
|
|
2485
2634
|
state.renderNodes = fallbackNodes
|
|
2486
2635
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2487
2636
|
}
|
|
@@ -2598,6 +2747,17 @@ const render = now => {
|
|
|
2598
2747
|
ctx.translate(state.transform.x, state.transform.y)
|
|
2599
2748
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2600
2749
|
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
2750
|
+
if (state.renderClusterEdges.length > 0) {
|
|
2751
|
+
ctx.beginPath()
|
|
2752
|
+
for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
|
|
2753
|
+
const edge = state.renderClusterEdges[index]
|
|
2754
|
+
ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
|
|
2755
|
+
ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
|
|
2756
|
+
}
|
|
2757
|
+
ctx.lineWidth = 1.2 / safeScale
|
|
2758
|
+
ctx.strokeStyle = 'rgba(153, 165, 181, 0.22)'
|
|
2759
|
+
ctx.stroke()
|
|
2760
|
+
}
|
|
2601
2761
|
state.renderClusters.forEach(cluster => {
|
|
2602
2762
|
const isMacro = cluster.id === 'macro-galaxy'
|
|
2603
2763
|
const radiusPx = isMacro
|
package/package.json
CHANGED