@andespindola/brainlink 0.1.0-beta.83 → 0.1.0-beta.85
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 -1
- package/dist/application/frontend/client-js.js +166 -100
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,6 +84,7 @@ 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 connected ecosystem clusters of up to 1000 notes before expanding into individual notes, keeping vaults visually oriented with a smaller render scope.
|
|
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.
|
|
@@ -600,7 +601,8 @@ The graph UI shows:
|
|
|
600
601
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
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
|
|
605
|
+
- graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes with aggregated real links, then progressively raises the focused node budget as zoom increases so dense local areas keep nearby notes and links visible
|
|
604
606
|
|
|
605
607
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
606
608
|
|
|
@@ -21,14 +21,15 @@ 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 ecosystemClusterEdgeLimit = 520
|
|
26
|
+
const ecosystemClusterScaleThreshold = 0.08
|
|
25
27
|
const zoomRecoveryGuardMs = 4200
|
|
26
28
|
const zoomCapTargetViewportShare = 0.72
|
|
27
29
|
const meshEdgeScaleThreshold = 0.09
|
|
28
30
|
const meshEdgeMinBudget = 140
|
|
29
31
|
const meshEdgeMaxBudget = 1400
|
|
30
32
|
const layeredCoreScaleThreshold = 0.55
|
|
31
|
-
const massiveOverviewClusterScaleThreshold = 0.035
|
|
32
33
|
const dragNeighborhoodMaxAffected = 180
|
|
33
34
|
const dragSettleRounds = 3
|
|
34
35
|
const wheelZoomExponent = 0.0018
|
|
@@ -44,6 +45,7 @@ const state = {
|
|
|
44
45
|
renderNodes: [],
|
|
45
46
|
renderEdges: [],
|
|
46
47
|
renderClusters: [],
|
|
48
|
+
renderClusterEdges: [],
|
|
47
49
|
nodeDegrees: new Map(),
|
|
48
50
|
selected: null,
|
|
49
51
|
hovered: null,
|
|
@@ -65,7 +67,8 @@ const state = {
|
|
|
65
67
|
lastViewportKey: '',
|
|
66
68
|
visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
|
|
67
69
|
visibleEdgeByNode: new Map(),
|
|
68
|
-
|
|
70
|
+
ecosystemClusters: [],
|
|
71
|
+
ecosystemEdges: [],
|
|
69
72
|
macroCenter: { x: 0, y: 0 },
|
|
70
73
|
macroRepresentative: null,
|
|
71
74
|
primaryHub: null,
|
|
@@ -537,6 +540,17 @@ const nearestHubNeighborDistance = (hub, nodes) => {
|
|
|
537
540
|
return minimum
|
|
538
541
|
}
|
|
539
542
|
|
|
543
|
+
const isDominantHub = (hub, nodeCount = state.visibleNodes.length) => {
|
|
544
|
+
if (!hub || nodeCount <= 0) {
|
|
545
|
+
return false
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const degree = state.nodeDegrees.get(hub.id) ?? 0
|
|
549
|
+
const minimumDegree = Math.max(18, Math.sqrt(nodeCount) * 1.8)
|
|
550
|
+
const degreeRatio = degree / Math.max(nodeCount, 1)
|
|
551
|
+
return degree >= minimumDegree || degreeRatio >= 0.035
|
|
552
|
+
}
|
|
553
|
+
|
|
540
554
|
const recomputeVisibility = () => {
|
|
541
555
|
const nodes = filteredNodes()
|
|
542
556
|
const ids = new Set(nodes.map(node => node.id))
|
|
@@ -551,15 +565,20 @@ const recomputeVisibility = () => {
|
|
|
551
565
|
state.visibleEdges = limitedEdges
|
|
552
566
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
553
567
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
554
|
-
|
|
568
|
+
const ecosystemGraph = nodes.length > 1
|
|
569
|
+
? buildEcosystemGraph(nodes, limitedEdges)
|
|
570
|
+
: { clusters: [], edges: [] }
|
|
571
|
+
state.ecosystemClusters = ecosystemGraph.clusters
|
|
572
|
+
state.ecosystemEdges = ecosystemGraph.edges
|
|
555
573
|
const primaryHub = rankedHubNodes()[0] ?? null
|
|
556
574
|
state.primaryHub = primaryHub
|
|
557
575
|
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
558
576
|
const bounds = graphBounds(nodes)
|
|
577
|
+
const macroHub = isDominantHub(primaryHub, nodes.length) ? primaryHub : null
|
|
559
578
|
state.macroCenter = bounds
|
|
560
579
|
? {
|
|
561
|
-
x:
|
|
562
|
-
y:
|
|
580
|
+
x: macroHub ? macroHub.x : (bounds.minX + bounds.maxX) / 2,
|
|
581
|
+
y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
|
|
563
582
|
}
|
|
564
583
|
: { x: 0, y: 0 }
|
|
565
584
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
@@ -667,68 +686,131 @@ const createVisibleEdgeLookup = edges => {
|
|
|
667
686
|
return lookup
|
|
668
687
|
}
|
|
669
688
|
|
|
670
|
-
const
|
|
671
|
-
if (
|
|
672
|
-
return
|
|
689
|
+
const ecosystemKeyForNode = node => {
|
|
690
|
+
if (typeof node.segment === 'string' && node.segment.trim()) {
|
|
691
|
+
return node.segment.trim()
|
|
673
692
|
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
if (!bounds) {
|
|
677
|
-
return []
|
|
693
|
+
if (typeof node.group === 'string' && node.group.trim()) {
|
|
694
|
+
return node.group.trim()
|
|
678
695
|
}
|
|
696
|
+
const pathParts = String(node.path || '')
|
|
697
|
+
.split('/')
|
|
698
|
+
.filter(part => part.trim())
|
|
699
|
+
.slice(0, 2)
|
|
700
|
+
return pathParts.length > 0 ? pathParts.join('/') : 'root'
|
|
701
|
+
}
|
|
679
702
|
|
|
680
|
-
|
|
681
|
-
const
|
|
682
|
-
|
|
703
|
+
const compareNodesForEcosystem = (left, right) => {
|
|
704
|
+
const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
|
|
705
|
+
if (keyComparison !== 0) return keyComparison
|
|
706
|
+
const leftDegree = state.nodeDegrees.get(left.id) ?? 0
|
|
707
|
+
const rightDegree = state.nodeDegrees.get(right.id) ?? 0
|
|
708
|
+
if (leftDegree !== rightDegree) return rightDegree - leftDegree
|
|
709
|
+
return String(left.title || left.id).localeCompare(String(right.title || right.id))
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const selectEcosystemRepresentative = nodes => {
|
|
713
|
+
let representative = nodes[0] ?? null
|
|
714
|
+
let representativeScore = Number.NEGATIVE_INFINITY
|
|
683
715
|
|
|
684
716
|
for (let index = 0; index < nodes.length; index += 1) {
|
|
685
717
|
const node = nodes[index]
|
|
686
|
-
const
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
718
|
+
const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
|
|
719
|
+
if (score > representativeScore) {
|
|
720
|
+
representative = node
|
|
721
|
+
representativeScore = score
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return representative
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const buildEcosystemCluster = (nodes, index) => {
|
|
729
|
+
const count = Math.max(nodes.length, 1)
|
|
730
|
+
const sum = nodes.reduce((accumulator, node) => ({
|
|
731
|
+
x: accumulator.x + node.x,
|
|
732
|
+
y: accumulator.y + node.y
|
|
733
|
+
}), { x: 0, y: 0 })
|
|
734
|
+
const representative = selectEcosystemRepresentative(nodes)
|
|
735
|
+
|
|
736
|
+
return {
|
|
737
|
+
id: 'ecosystem-' + index,
|
|
738
|
+
x: sum.x / count,
|
|
739
|
+
y: sum.y / count,
|
|
740
|
+
count,
|
|
741
|
+
representative,
|
|
742
|
+
label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const buildEcosystemGraph = (nodes, edges) => {
|
|
747
|
+
if (nodes.length === 0) {
|
|
748
|
+
return { clusters: [], edges: [] }
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
|
|
752
|
+
const clusters = []
|
|
753
|
+
const clusterByNodeId = new Map()
|
|
754
|
+
|
|
755
|
+
for (let offset = 0; offset < sortedNodes.length; offset += ecosystemGroupSize) {
|
|
756
|
+
const clusterNodes = sortedNodes.slice(offset, offset + ecosystemGroupSize)
|
|
757
|
+
const cluster = buildEcosystemCluster(clusterNodes, clusters.length)
|
|
758
|
+
clusters.push(cluster)
|
|
759
|
+
for (let index = 0; index < clusterNodes.length; index += 1) {
|
|
760
|
+
clusterByNodeId.set(clusterNodes[index].id, cluster)
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const edgeByClusterPair = new Map()
|
|
765
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
766
|
+
const edge = edges[index]
|
|
767
|
+
const sourceCluster = clusterByNodeId.get(edge.source)
|
|
768
|
+
const targetCluster = clusterByNodeId.get(edge.target)
|
|
769
|
+
if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
|
|
770
|
+
continue
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const orderedIds = sourceCluster.id < targetCluster.id
|
|
774
|
+
? [sourceCluster.id, targetCluster.id]
|
|
775
|
+
: [targetCluster.id, sourceCluster.id]
|
|
776
|
+
const key = orderedIds.join(':')
|
|
777
|
+
const current = edgeByClusterPair.get(key)
|
|
691
778
|
if (current) {
|
|
692
|
-
current.
|
|
693
|
-
current.sumX += node.x
|
|
694
|
-
current.sumY += node.y
|
|
695
|
-
if (degree > current.degree) {
|
|
696
|
-
current.representative = node
|
|
697
|
-
current.degree = degree
|
|
698
|
-
}
|
|
779
|
+
current.weight += edgeWeight(edge)
|
|
699
780
|
continue
|
|
700
781
|
}
|
|
701
782
|
|
|
702
|
-
|
|
783
|
+
edgeByClusterPair.set(key, {
|
|
703
784
|
id: key,
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
representative: node,
|
|
708
|
-
degree
|
|
785
|
+
sourceCluster,
|
|
786
|
+
targetCluster,
|
|
787
|
+
weight: edgeWeight(edge)
|
|
709
788
|
})
|
|
710
789
|
}
|
|
711
790
|
|
|
712
|
-
|
|
713
|
-
.sort((left, right) => right.
|
|
714
|
-
.slice(0,
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
x: cluster.sumX / Math.max(cluster.count, 1),
|
|
718
|
-
y: cluster.sumY / Math.max(cluster.count, 1),
|
|
719
|
-
count: cluster.count,
|
|
720
|
-
representative: cluster.representative
|
|
721
|
-
}))
|
|
791
|
+
const aggregatedEdges = Array.from(edgeByClusterPair.values())
|
|
792
|
+
.sort((left, right) => right.weight - left.weight)
|
|
793
|
+
.slice(0, ecosystemClusterEdgeLimit)
|
|
794
|
+
|
|
795
|
+
return { clusters, edges: aggregatedEdges }
|
|
722
796
|
}
|
|
723
797
|
|
|
724
|
-
const
|
|
725
|
-
state.
|
|
798
|
+
const filterEcosystemClustersByViewport = viewport =>
|
|
799
|
+
state.ecosystemClusters.filter((cluster) =>
|
|
726
800
|
cluster.x >= viewport.minX &&
|
|
727
801
|
cluster.x <= viewport.maxX &&
|
|
728
802
|
cluster.y >= viewport.minY &&
|
|
729
803
|
cluster.y <= viewport.maxY
|
|
730
804
|
)
|
|
731
805
|
|
|
806
|
+
const ecosystemEdgesForClusters = clusters => {
|
|
807
|
+
const clusterIds = new Set(clusters.map(cluster => cluster.id))
|
|
808
|
+
return state.ecosystemEdges.filter(edge =>
|
|
809
|
+
clusterIds.has(edge.sourceCluster.id) &&
|
|
810
|
+
clusterIds.has(edge.targetCluster.id)
|
|
811
|
+
)
|
|
812
|
+
}
|
|
813
|
+
|
|
732
814
|
const edgeBudgetForCurrentFrame = () => {
|
|
733
815
|
const zoom = state.transform.scale
|
|
734
816
|
if (zoom < 0.12) return 380
|
|
@@ -763,14 +845,6 @@ const nodeBudgetForScale = (scale) => {
|
|
|
763
845
|
return renderNodeBudget
|
|
764
846
|
}
|
|
765
847
|
|
|
766
|
-
const massiveLowZoomNodeBudgetForScale = (scale) => {
|
|
767
|
-
if (scale < 0.004) return 780
|
|
768
|
-
if (scale < 0.01) return 860
|
|
769
|
-
if (scale < 0.02) return 900
|
|
770
|
-
if (scale < 0.035) return 900
|
|
771
|
-
return renderNodeBudget
|
|
772
|
-
}
|
|
773
|
-
|
|
774
848
|
const layerFocusForScale = (scale) => {
|
|
775
849
|
const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
|
|
776
850
|
const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
|
|
@@ -1675,7 +1749,9 @@ const zoomCapByHubDistance = (distance) => {
|
|
|
1675
1749
|
|
|
1676
1750
|
const currentZoomMax = () => {
|
|
1677
1751
|
const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
|
|
1678
|
-
const hubDistanceCap =
|
|
1752
|
+
const hubDistanceCap = isDominantHub(state.primaryHub, nodeCount)
|
|
1753
|
+
? zoomCapByHubDistance(state.hubNeighborDistance)
|
|
1754
|
+
: zoomRange.max
|
|
1679
1755
|
const minimumUsefulCap = nodeCount > massiveGraphNodeThreshold ? 1.9 : nodeCount > largeGraphNodeThreshold ? 1.35 : 0.8
|
|
1680
1756
|
const capped = Math.min(zoomCapByNodeCount(nodeCount), Math.max(minimumUsefulCap, hubDistanceCap))
|
|
1681
1757
|
return Math.max(zoomRange.min * 2, capped)
|
|
@@ -1776,7 +1852,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
1776
1852
|
? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
|
|
1777
1853
|
: baselineScale
|
|
1778
1854
|
const hubCenter =
|
|
1779
|
-
options.preferHubCenter && state.primaryHub && nodes.some((node) => node.id === state.primaryHub.id)
|
|
1855
|
+
options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
|
|
1780
1856
|
? state.primaryHub
|
|
1781
1857
|
: null
|
|
1782
1858
|
const centerX = hubCenter ? hubCenter.x : (bounds.minX + bounds.maxX) / 2
|
|
@@ -2302,6 +2378,7 @@ const computeRenderVisibility = () => {
|
|
|
2302
2378
|
}
|
|
2303
2379
|
state.lastViewportKey = viewportKey
|
|
2304
2380
|
state.renderVisibilityDirty = false
|
|
2381
|
+
state.renderClusterEdges = []
|
|
2305
2382
|
|
|
2306
2383
|
const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
|
|
2307
2384
|
|
|
@@ -2325,12 +2402,27 @@ const computeRenderVisibility = () => {
|
|
|
2325
2402
|
state.renderNodes = []
|
|
2326
2403
|
}
|
|
2327
2404
|
state.renderEdges = []
|
|
2405
|
+
state.renderClusterEdges = []
|
|
2406
|
+
return
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
|
|
2410
|
+
const viewportClusters = filterEcosystemClustersByViewport(viewport)
|
|
2411
|
+
.sort((left, right) => right.count - left.count)
|
|
2412
|
+
const clusters = viewportClusters.length > 0
|
|
2413
|
+
? viewportClusters
|
|
2414
|
+
: state.ecosystemClusters
|
|
2415
|
+
state.renderClusters = clusters
|
|
2416
|
+
state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
|
|
2417
|
+
state.renderNodes = []
|
|
2418
|
+
state.renderEdges = []
|
|
2328
2419
|
return
|
|
2329
2420
|
}
|
|
2330
2421
|
|
|
2331
2422
|
if (state.visibleNodes.length <= 2000) {
|
|
2332
2423
|
state.renderNodes = state.visibleNodes
|
|
2333
2424
|
state.renderClusters = []
|
|
2425
|
+
state.renderClusterEdges = []
|
|
2334
2426
|
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
2335
2427
|
state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
|
|
2336
2428
|
return
|
|
@@ -2338,30 +2430,6 @@ const computeRenderVisibility = () => {
|
|
|
2338
2430
|
|
|
2339
2431
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
2340
2432
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2341
|
-
if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
|
|
2342
|
-
const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
|
|
2343
|
-
const overviewClusters = filterOverviewClustersByViewport(viewport)
|
|
2344
|
-
.sort((left, right) => right.count - left.count)
|
|
2345
|
-
.slice(0, overviewLimit)
|
|
2346
|
-
if (overviewClusters.length > 0) {
|
|
2347
|
-
const overviewNodes = representativeNodesFromClusters(
|
|
2348
|
-
overviewClusters,
|
|
2349
|
-
overviewLimit
|
|
2350
|
-
)
|
|
2351
|
-
const anchoredNodes = includeHubPreviewNeighborhood(
|
|
2352
|
-
overviewNodes,
|
|
2353
|
-
Math.min(renderNodeBudget, overviewLimit)
|
|
2354
|
-
)
|
|
2355
|
-
const enriched = enrichSampleWithNeighbors(anchoredNodes)
|
|
2356
|
-
const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
|
|
2357
|
-
const previewIds = new Set(previewNodes.map((node) => node.id))
|
|
2358
|
-
const previewEdges = collectVisibleEdgesForNodes(previewIds)
|
|
2359
|
-
state.renderClusters = []
|
|
2360
|
-
state.renderNodes = previewNodes
|
|
2361
|
-
state.renderEdges = previewEdges
|
|
2362
|
-
return
|
|
2363
|
-
}
|
|
2364
|
-
}
|
|
2365
2433
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
2366
2434
|
const sampleLimit = nodeBudgetForScale(state.transform.scale)
|
|
2367
2435
|
const carryMargin = Math.max(240, Math.min(1200, 340 / Math.max(state.transform.scale, 0.0001)))
|
|
@@ -2407,6 +2475,7 @@ const computeRenderVisibility = () => {
|
|
|
2407
2475
|
}
|
|
2408
2476
|
|
|
2409
2477
|
state.renderClusters = []
|
|
2478
|
+
state.renderClusterEdges = []
|
|
2410
2479
|
state.renderNodes = sampledNodes
|
|
2411
2480
|
state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
|
|
2412
2481
|
return
|
|
@@ -2416,6 +2485,7 @@ const computeRenderVisibility = () => {
|
|
|
2416
2485
|
const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
|
|
2417
2486
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
2418
2487
|
state.renderClusters = []
|
|
2488
|
+
state.renderClusterEdges = []
|
|
2419
2489
|
state.renderNodes = sampled
|
|
2420
2490
|
state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
|
|
2421
2491
|
return
|
|
@@ -2425,11 +2495,13 @@ const computeRenderVisibility = () => {
|
|
|
2425
2495
|
const clusters = clusterViewportNodes(viewportNodes)
|
|
2426
2496
|
if (clusters.length > 0) {
|
|
2427
2497
|
state.renderClusters = []
|
|
2498
|
+
state.renderClusterEdges = []
|
|
2428
2499
|
state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
|
|
2429
2500
|
state.renderEdges = []
|
|
2430
2501
|
return
|
|
2431
2502
|
}
|
|
2432
2503
|
state.renderClusters = []
|
|
2504
|
+
state.renderClusterEdges = []
|
|
2433
2505
|
const stride = viewportNodeStride()
|
|
2434
2506
|
const picked = []
|
|
2435
2507
|
|
|
@@ -2453,6 +2525,7 @@ const computeRenderVisibility = () => {
|
|
|
2453
2525
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2454
2526
|
state.renderNodes = fallbackNodes
|
|
2455
2527
|
state.renderClusters = []
|
|
2528
|
+
state.renderClusterEdges = []
|
|
2456
2529
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2457
2530
|
return
|
|
2458
2531
|
}
|
|
@@ -2468,6 +2541,7 @@ const computeRenderVisibility = () => {
|
|
|
2468
2541
|
const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
|
|
2469
2542
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2470
2543
|
state.renderClusters = []
|
|
2544
|
+
state.renderClusterEdges = []
|
|
2471
2545
|
state.renderNodes = fallbackNodes
|
|
2472
2546
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2473
2547
|
}
|
|
@@ -2584,6 +2658,17 @@ const render = now => {
|
|
|
2584
2658
|
ctx.translate(state.transform.x, state.transform.y)
|
|
2585
2659
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2586
2660
|
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
2661
|
+
if (state.renderClusterEdges.length > 0) {
|
|
2662
|
+
ctx.beginPath()
|
|
2663
|
+
for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
|
|
2664
|
+
const edge = state.renderClusterEdges[index]
|
|
2665
|
+
ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
|
|
2666
|
+
ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
|
|
2667
|
+
}
|
|
2668
|
+
ctx.lineWidth = 1.2 / safeScale
|
|
2669
|
+
ctx.strokeStyle = 'rgba(153, 165, 181, 0.22)'
|
|
2670
|
+
ctx.stroke()
|
|
2671
|
+
}
|
|
2587
2672
|
state.renderClusters.forEach(cluster => {
|
|
2588
2673
|
const isMacro = cluster.id === 'macro-galaxy'
|
|
2589
2674
|
const radiusPx = isMacro
|
|
@@ -2735,27 +2820,8 @@ const selectNodeById = id => {
|
|
|
2735
2820
|
}
|
|
2736
2821
|
|
|
2737
2822
|
const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
2738
|
-
const resolveZoomFactor = () => {
|
|
2739
|
-
if (state.nodes.length <= massiveGraphNodeThreshold) {
|
|
2740
|
-
return factor
|
|
2741
|
-
}
|
|
2742
|
-
|
|
2743
|
-
const scale = state.transform.scale
|
|
2744
|
-
if (factor > 1) {
|
|
2745
|
-
if (scale < 0.006) return Math.max(factor, 1.48)
|
|
2746
|
-
if (scale < 0.02) return Math.max(factor, 1.34)
|
|
2747
|
-
if (scale < 0.08) return Math.max(factor, 1.22)
|
|
2748
|
-
return factor
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
if (scale < 0.006) return Math.min(factor, 0.68)
|
|
2752
|
-
if (scale < 0.02) return Math.min(factor, 0.78)
|
|
2753
|
-
if (scale < 0.08) return Math.min(factor, 0.86)
|
|
2754
|
-
return factor
|
|
2755
|
-
}
|
|
2756
|
-
|
|
2757
2823
|
state.lastManualZoomAt = performance.now()
|
|
2758
|
-
const effectiveFactor =
|
|
2824
|
+
const effectiveFactor = factor
|
|
2759
2825
|
const nextScale = clampScale(state.transform.scale * effectiveFactor)
|
|
2760
2826
|
if (nextScale === state.transform.scale) {
|
|
2761
2827
|
return
|
package/package.json
CHANGED