@andespindola/brainlink 0.1.0-beta.115 → 0.1.0-beta.116
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 -3
- package/dist/application/frontend/client-js.js +7 -401
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,11 +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
|
|
87
|
+
- Graph rendering uses a rebuilt Bloom-like flat scene: no macro galaxy, no recursive subgraphs, no cluster replacement layer and no synthetic 3D projection.
|
|
88
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.
|
|
89
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).
|
|
90
90
|
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable fitted scene and zoom-in progressively reveals local detail.
|
|
91
|
-
- Zoomed-out graph LOD samples visible nodes and priority links without switching to nested subgraphs.
|
|
91
|
+
- Zoomed-out graph LOD samples visible nodes and priority links without switching to nested subgraphs or cluster markers.
|
|
92
92
|
- Graph reset fits the full graph scene instead of starting in a separate macro overview mode.
|
|
93
93
|
- Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
|
|
94
94
|
- Edge rendering budgets adapt to zoom level to prevent frame spikes on large graph panoramas.
|
|
@@ -604,7 +604,7 @@ The graph UI shows:
|
|
|
604
604
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
605
605
|
- adaptive CPU safeguards for large graphs: idle frame pacing, throttled background physics updates and cached viewport dimensions to reduce redraw/layout overhead while preserving interaction responsiveness
|
|
606
606
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
607
|
-
- large graph LOD keeps the same scene model as smaller graphs: visible nodes are sampled near the viewport/focus, real links are prioritized, and no recursive graph-of-graphs or synthetic 3D projection is applied
|
|
607
|
+
- large graph LOD keeps the same scene model as smaller graphs: visible nodes are sampled near the viewport/focus, real links are prioritized, and no macro galaxy, recursive graph-of-graphs, cluster replacement layer or synthetic 3D projection is applied
|
|
608
608
|
|
|
609
609
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
610
610
|
|
|
@@ -7,25 +7,13 @@ const largeGraphEdgeRenderLimit = 120000
|
|
|
7
7
|
const renderNodeBudget = 900
|
|
8
8
|
const zoomedMassiveRenderNodeBudget = 2200
|
|
9
9
|
const renderEdgeBudget = 2400
|
|
10
|
-
const clusterActivationNodeThreshold = 600
|
|
11
|
-
const clusterZoomThreshold = 0.18
|
|
12
|
-
const macroGalaxyZoomThreshold = 0.012
|
|
13
|
-
const macroGalaxyEnterHysteresis = 0.86
|
|
14
|
-
const macroGalaxyExitHysteresis = 1.24
|
|
15
|
-
const galaxyDiscoveryEnabled = false
|
|
16
10
|
const massiveAutoFitMacroScale = 0.018
|
|
17
|
-
const defaultMacroScale = 0.018
|
|
18
|
-
const clusterCellPixelSize = 64
|
|
19
11
|
const minNodePixelRadius = 2.3
|
|
20
12
|
const viewportPaddingPx = 280
|
|
21
13
|
const worldCoordinateLimit = 5_000_000
|
|
22
14
|
const transformCoordinateLimit = 20_000_000
|
|
23
15
|
const hoverHitTestIntervalMs = 64
|
|
24
|
-
const ecosystemLevelNodeCap = 999
|
|
25
|
-
const ecosystemActivationNodeThreshold = 1000
|
|
26
|
-
const ecosystemSubgraphScaleThreshold = 0.18
|
|
27
16
|
const zoomRecoveryGuardMs = 4200
|
|
28
|
-
const zoomCapTargetViewportShare = 0.72
|
|
29
17
|
const meshEdgeScaleThreshold = 0.09
|
|
30
18
|
const meshEdgeMinBudget = 140
|
|
31
19
|
const meshEdgeMaxBudget = 1400
|
|
@@ -54,8 +42,6 @@ const state = {
|
|
|
54
42
|
visibleEdges: [],
|
|
55
43
|
renderNodes: [],
|
|
56
44
|
renderEdges: [],
|
|
57
|
-
renderClusters: [],
|
|
58
|
-
renderClusterEdges: [],
|
|
59
45
|
renderNodeDepthProjectionById: new Map(),
|
|
60
46
|
nodeDegrees: new Map(),
|
|
61
47
|
selected: null,
|
|
@@ -81,26 +67,12 @@ const state = {
|
|
|
81
67
|
lastViewportKey: '',
|
|
82
68
|
visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
|
|
83
69
|
visibleEdgeByNode: new Map(),
|
|
84
|
-
ecosystemClusters: [],
|
|
85
|
-
ecosystemClustersBySize: new Map(),
|
|
86
|
-
ecosystemNodeClusterBySize: new Map(),
|
|
87
|
-
ecosystemLevelSizes: [],
|
|
88
|
-
ecosystemLevelIndexBySize: new Map(),
|
|
89
|
-
ecosystemHubNodeIds: new Set(),
|
|
90
|
-
ecosystemExpansionLevels: [],
|
|
91
|
-
ecosystemBaseSize: ecosystemLevelNodeCap,
|
|
92
|
-
ecosystemHubCluster: null,
|
|
93
|
-
macroCenter: { x: 0, y: 0 },
|
|
94
|
-
macroRepresentative: null,
|
|
95
70
|
primaryHub: null,
|
|
96
|
-
hubNeighborDistance: Number.POSITIVE_INFINITY,
|
|
97
71
|
filterWorker: null,
|
|
98
72
|
filterReady: false,
|
|
99
73
|
lastHoverHitAt: 0,
|
|
100
74
|
lastManualZoomAt: 0,
|
|
101
75
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
102
|
-
macroViewActive: false,
|
|
103
|
-
ecosystemViewActive: false,
|
|
104
76
|
depthProjectionActive: false,
|
|
105
77
|
zoomTransition: {
|
|
106
78
|
active: false,
|
|
@@ -532,47 +504,6 @@ const filteredNodes = () => {
|
|
|
532
504
|
return withPersistentHubNodes(localFilteredNodes(query))
|
|
533
505
|
}
|
|
534
506
|
|
|
535
|
-
const resolveMacroRepresentative = (nodes) => {
|
|
536
|
-
if (nodes.length === 0) {
|
|
537
|
-
return null
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
const hubCandidate = state.primaryHub && nodes.some(node => node.id === state.primaryHub.id)
|
|
541
|
-
? state.primaryHub
|
|
542
|
-
: null
|
|
543
|
-
let best = hubCandidate ?? nodes[0]
|
|
544
|
-
let bestDegree = state.nodeDegrees.get(best.id) ?? 0
|
|
545
|
-
|
|
546
|
-
for (let index = 1; index < nodes.length; index += 1) {
|
|
547
|
-
const node = nodes[index]
|
|
548
|
-
const degree = state.nodeDegrees.get(node.id) ?? 0
|
|
549
|
-
if (degree > bestDegree) {
|
|
550
|
-
best = node
|
|
551
|
-
bestDegree = degree
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
return best
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
const nearestHubNeighborDistance = (hub, nodes) => {
|
|
559
|
-
if (!hub || nodes.length <= 1) {
|
|
560
|
-
return Number.POSITIVE_INFINITY
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
let minimum = Number.POSITIVE_INFINITY
|
|
564
|
-
for (let index = 0; index < nodes.length; index += 1) {
|
|
565
|
-
const node = nodes[index]
|
|
566
|
-
if (node.id === hub.id) continue
|
|
567
|
-
const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
|
|
568
|
-
if (distance < minimum) {
|
|
569
|
-
minimum = distance
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
|
-
return minimum
|
|
574
|
-
}
|
|
575
|
-
|
|
576
507
|
const isDominantHub = (hub, nodeCount = state.visibleNodes.length) => {
|
|
577
508
|
if (!hub || nodeCount <= 0) {
|
|
578
509
|
return false
|
|
@@ -600,25 +531,6 @@ const recomputeVisibility = () => {
|
|
|
600
531
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
601
532
|
const primaryHub = rankedHubNodes()[0] ?? null
|
|
602
533
|
state.primaryHub = primaryHub
|
|
603
|
-
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
604
|
-
const bounds = graphBounds(nodes)
|
|
605
|
-
const macroHub = isDominantHub(primaryHub, nodes.length) ? primaryHub : null
|
|
606
|
-
state.macroCenter = bounds
|
|
607
|
-
? {
|
|
608
|
-
x: macroHub ? macroHub.x : (bounds.minX + bounds.maxX) / 2,
|
|
609
|
-
y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
|
|
610
|
-
}
|
|
611
|
-
: { x: 0, y: 0 }
|
|
612
|
-
state.ecosystemClusters = []
|
|
613
|
-
state.ecosystemClustersBySize = new Map()
|
|
614
|
-
state.ecosystemNodeClusterBySize = new Map()
|
|
615
|
-
state.ecosystemLevelSizes = []
|
|
616
|
-
state.ecosystemLevelIndexBySize = new Map()
|
|
617
|
-
state.ecosystemHubNodeIds = new Set()
|
|
618
|
-
state.ecosystemExpansionLevels = []
|
|
619
|
-
state.ecosystemBaseSize = ecosystemLevelNodeCap
|
|
620
|
-
state.ecosystemHubCluster = null
|
|
621
|
-
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
622
534
|
markRenderDirty()
|
|
623
535
|
}
|
|
624
536
|
|
|
@@ -729,18 +641,6 @@ const isClusterInViewport = (cluster, viewport) =>
|
|
|
729
641
|
cluster.y >= viewport.minY &&
|
|
730
642
|
cluster.y <= viewport.maxY
|
|
731
643
|
|
|
732
|
-
const ecosystemFocusPoint = () => {
|
|
733
|
-
const cursorPoint = cursorWorldPoint()
|
|
734
|
-
if (cursorPoint) {
|
|
735
|
-
return cursorPoint
|
|
736
|
-
}
|
|
737
|
-
const now = performance.now()
|
|
738
|
-
if (now - state.lastZoomFocus.at <= 1800) {
|
|
739
|
-
return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
740
|
-
}
|
|
741
|
-
return viewportCenterWorldPoint()
|
|
742
|
-
}
|
|
743
|
-
|
|
744
644
|
const edgeBudgetForCurrentFrame = () => {
|
|
745
645
|
const zoom = state.transform.scale
|
|
746
646
|
if (zoom < 0.12) return 380
|
|
@@ -896,30 +796,6 @@ const visibilityScaleBucket = (scale) => {
|
|
|
896
796
|
return Math.round(safeScale * 180_000)
|
|
897
797
|
}
|
|
898
798
|
|
|
899
|
-
const shouldRenderEcosystemClusterView = (nodeCount, scale) => {
|
|
900
|
-
state.ecosystemViewActive = false
|
|
901
|
-
return false
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const shouldRenderMacroGalaxyView = () => {
|
|
905
|
-
if (!galaxyDiscoveryEnabled) {
|
|
906
|
-
state.macroViewActive = false
|
|
907
|
-
return false
|
|
908
|
-
}
|
|
909
|
-
if (state.visibleNodes.length <= 1) {
|
|
910
|
-
state.macroViewActive = false
|
|
911
|
-
return false
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const enterThreshold = macroGalaxyZoomThreshold * macroGalaxyEnterHysteresis
|
|
915
|
-
const exitThreshold = macroGalaxyZoomThreshold * macroGalaxyExitHysteresis
|
|
916
|
-
const shouldRender = state.macroViewActive
|
|
917
|
-
? state.transform.scale <= exitThreshold
|
|
918
|
-
: state.transform.scale <= enterThreshold
|
|
919
|
-
state.macroViewActive = shouldRender
|
|
920
|
-
return shouldRender
|
|
921
|
-
}
|
|
922
|
-
|
|
923
799
|
const mergeUniqueNodes = (leftNodes, rightNodes, limit) => {
|
|
924
800
|
const merged = []
|
|
925
801
|
const ids = new Set()
|
|
@@ -1358,7 +1234,7 @@ const drawGraphLabels = nodes => {
|
|
|
1358
1234
|
}
|
|
1359
1235
|
|
|
1360
1236
|
const drawAcceleratedGraph = (width, height, drawEdges) => {
|
|
1361
|
-
if (!webGlRenderer
|
|
1237
|
+
if (!webGlRenderer) {
|
|
1362
1238
|
return false
|
|
1363
1239
|
}
|
|
1364
1240
|
|
|
@@ -1728,18 +1604,6 @@ const zoomCapByNodeCount = (nodeCount) => {
|
|
|
1728
1604
|
return zoomRange.max
|
|
1729
1605
|
}
|
|
1730
1606
|
|
|
1731
|
-
const zoomCapByHubDistance = (distance) => {
|
|
1732
|
-
if (!Number.isFinite(distance) || distance <= 0) {
|
|
1733
|
-
return zoomRange.max
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
const rect = canvas.getBoundingClientRect()
|
|
1737
|
-
const viewportWidth = Math.max(rect.width, 320)
|
|
1738
|
-
const viewportHeight = Math.max(rect.height, 320)
|
|
1739
|
-
const reference = Math.max(220, Math.min(viewportWidth, viewportHeight) * zoomCapTargetViewportShare)
|
|
1740
|
-
return Math.max(0.3, Math.min(zoomRange.max, reference / distance))
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
1607
|
const currentZoomMax = () => {
|
|
1744
1608
|
const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
|
|
1745
1609
|
return Math.max(zoomRange.min * 2, zoomCapByNodeCount(nodeCount))
|
|
@@ -1748,7 +1612,7 @@ const currentZoomMax = () => {
|
|
|
1748
1612
|
const zoomFloorByNodeCount = (nodeCount) => {
|
|
1749
1613
|
if (nodeCount > massiveGraphNodeThreshold) return 0.018
|
|
1750
1614
|
if (nodeCount > largeGraphNodeThreshold) return 0.0032
|
|
1751
|
-
if (nodeCount >
|
|
1615
|
+
if (nodeCount > 1000) return 0.001
|
|
1752
1616
|
return zoomRange.min
|
|
1753
1617
|
}
|
|
1754
1618
|
|
|
@@ -1860,56 +1724,6 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
|
1860
1724
|
return { min: 0.0085, max: 0.36 }
|
|
1861
1725
|
}
|
|
1862
1726
|
|
|
1863
|
-
const macroFaceToFaceScale = (nodeCount, hubDistance) => {
|
|
1864
|
-
if (!Number.isFinite(hubDistance) || hubDistance <= 0 || nodeCount <= ecosystemActivationNodeThreshold) {
|
|
1865
|
-
return 0
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
const rect = canvas.getBoundingClientRect()
|
|
1869
|
-
const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
|
|
1870
|
-
const share = nodeCount > massiveGraphNodeThreshold ? 0.2 : 0.17
|
|
1871
|
-
const targetPx = Math.max(24, viewportReference * share)
|
|
1872
|
-
return targetPx / hubDistance
|
|
1873
|
-
}
|
|
1874
|
-
|
|
1875
|
-
const nearestClusterNeighborDistance = (clusters) => {
|
|
1876
|
-
if (!Array.isArray(clusters) || clusters.length < 2) {
|
|
1877
|
-
return Number.POSITIVE_INFINITY
|
|
1878
|
-
}
|
|
1879
|
-
|
|
1880
|
-
let nearestDistance = Number.POSITIVE_INFINITY
|
|
1881
|
-
for (let index = 0; index < clusters.length; index += 1) {
|
|
1882
|
-
const source = clusters[index]
|
|
1883
|
-
for (let neighborIndex = index + 1; neighborIndex < clusters.length; neighborIndex += 1) {
|
|
1884
|
-
const target = clusters[neighborIndex]
|
|
1885
|
-
const distance = Math.hypot(source.x - target.x, source.y - target.y)
|
|
1886
|
-
if (distance > 0 && distance < nearestDistance) {
|
|
1887
|
-
nearestDistance = distance
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
|
|
1892
|
-
return nearestDistance
|
|
1893
|
-
}
|
|
1894
|
-
|
|
1895
|
-
const macroEcosystemFaceScale = (nodeCount) => {
|
|
1896
|
-
if (nodeCount <= ecosystemActivationNodeThreshold) {
|
|
1897
|
-
return 0
|
|
1898
|
-
}
|
|
1899
|
-
|
|
1900
|
-
const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
|
|
1901
|
-
const siblingClusters = baseClusters.filter(cluster => !cluster.isHub)
|
|
1902
|
-
const nearestDistance = nearestClusterNeighborDistance(siblingClusters)
|
|
1903
|
-
if (!Number.isFinite(nearestDistance) || nearestDistance <= 0) {
|
|
1904
|
-
return 0
|
|
1905
|
-
}
|
|
1906
|
-
|
|
1907
|
-
const rect = canvas.getBoundingClientRect()
|
|
1908
|
-
const viewportReference = Math.max(320, Math.min(rect.width, rect.height))
|
|
1909
|
-
const targetShare = nodeCount > massiveGraphNodeThreshold ? 0.28 : 0.24
|
|
1910
|
-
const targetPx = Math.max(30, viewportReference * targetShare)
|
|
1911
|
-
return targetPx / nearestDistance
|
|
1912
|
-
}
|
|
1913
1727
|
const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
|
|
1914
1728
|
const rect = canvas.getBoundingClientRect()
|
|
1915
1729
|
const width = Math.max(rect.width, 320)
|
|
@@ -1942,21 +1756,9 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
1942
1756
|
const biasedScale = clampScale(fitScale * fitScaleBiasByNodeCount(nodes.length))
|
|
1943
1757
|
const scaleRange = autoFitScaleRangeByNodeCount(nodes.length)
|
|
1944
1758
|
const baselineScale = clampScale(Math.min(scaleRange.max, Math.max(scaleRange.min, biasedScale)))
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
: nodes.length > massiveGraphNodeThreshold
|
|
1949
|
-
? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
|
|
1950
|
-
: baselineScale
|
|
1951
|
-
const macroFloorScale = options.macro
|
|
1952
|
-
? clampScale(Math.max(
|
|
1953
|
-
macroFaceToFaceScale(nodes.length, state.hubNeighborDistance),
|
|
1954
|
-
macroEcosystemFaceScale(nodes.length)
|
|
1955
|
-
))
|
|
1956
|
-
: 0
|
|
1957
|
-
const resolvedScale = options.macro
|
|
1958
|
-
? clampScale(Math.max(scale, macroFloorScale))
|
|
1959
|
-
: scale
|
|
1759
|
+
const resolvedScale = nodes.length > massiveGraphNodeThreshold
|
|
1760
|
+
? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
|
|
1761
|
+
: baselineScale
|
|
1960
1762
|
const hubCenter =
|
|
1961
1763
|
options.preferHubCenter && isDominantHub(state.primaryHub, nodes.length) && nodes.some((node) => node.id === state.primaryHub.id)
|
|
1962
1764
|
? state.primaryHub
|
|
@@ -2347,9 +2149,6 @@ const settleNeighborhoodAroundNode = (dragNode) => {
|
|
|
2347
2149
|
|
|
2348
2150
|
const hitNode = point => {
|
|
2349
2151
|
computeRenderVisibility()
|
|
2350
|
-
if (state.renderClusters.length > 0) {
|
|
2351
|
-
return null
|
|
2352
|
-
}
|
|
2353
2152
|
const hitScaleFloor = state.nodes.length > massiveGraphNodeThreshold
|
|
2354
2153
|
? 0.2
|
|
2355
2154
|
: state.nodes.length > largeGraphNodeThreshold
|
|
@@ -2377,27 +2176,6 @@ const baseNodeRadius = node => {
|
|
|
2377
2176
|
|
|
2378
2177
|
const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
|
|
2379
2178
|
|
|
2380
|
-
const clusterRadiusPx = cluster => {
|
|
2381
|
-
if (cluster.id === 'macro-galaxy') {
|
|
2382
|
-
return 10
|
|
2383
|
-
}
|
|
2384
|
-
if (cluster.isHub) {
|
|
2385
|
-
return 3.8
|
|
2386
|
-
}
|
|
2387
|
-
if (String(cluster.id).startsWith('ecosystem-')) {
|
|
2388
|
-
const size = Math.max(1, Math.min(ecosystemLevelNodeCap, cluster.size || cluster.count || 1))
|
|
2389
|
-
const sizeBias = 0.56 + Math.log10(size + 1) * 0.28
|
|
2390
|
-
const densityBias = Math.log10((cluster.count || 1) + 1) * 0.12
|
|
2391
|
-
const radius = Math.max(0.62, Math.min(2.4, sizeBias + densityBias))
|
|
2392
|
-
const depthScale = Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
|
|
2393
|
-
return Math.max(0.56, Math.min(3.2, radius * depthScale))
|
|
2394
|
-
}
|
|
2395
|
-
return Math.max(8, Math.min(28, 8 + Math.log2(cluster.count + 1) * 3))
|
|
2396
|
-
}
|
|
2397
|
-
|
|
2398
|
-
const clusterOpacity = cluster =>
|
|
2399
|
-
Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
|
|
2400
|
-
|
|
2401
2179
|
const refreshRenderNodeDepthProjection = () => {
|
|
2402
2180
|
state.renderNodeDepthProjectionById = new Map()
|
|
2403
2181
|
}
|
|
@@ -2460,68 +2238,6 @@ const viewportNodeStride = () => {
|
|
|
2460
2238
|
return 8
|
|
2461
2239
|
}
|
|
2462
2240
|
|
|
2463
|
-
const shouldRenderClusters = viewportNodes =>
|
|
2464
|
-
state.transform.scale <= clusterZoomThreshold && viewportNodes.length >= clusterActivationNodeThreshold
|
|
2465
|
-
|
|
2466
|
-
const clusterViewportNodes = viewportNodes => {
|
|
2467
|
-
if (!shouldRenderClusters(viewportNodes)) {
|
|
2468
|
-
return []
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
const worldCellSize = Math.max(clusterCellPixelSize / Math.max(state.transform.scale, 0.0001), 1)
|
|
2472
|
-
const buckets = new Map()
|
|
2473
|
-
|
|
2474
|
-
for (let index = 0; index < viewportNodes.length; index += 1) {
|
|
2475
|
-
const node = viewportNodes[index]
|
|
2476
|
-
const keyX = Math.floor(node.x / worldCellSize)
|
|
2477
|
-
const keyY = Math.floor(node.y / worldCellSize)
|
|
2478
|
-
const key = keyX + ':' + keyY
|
|
2479
|
-
const current = buckets.get(key)
|
|
2480
|
-
if (current) {
|
|
2481
|
-
current.count += 1
|
|
2482
|
-
current.sumX += node.x
|
|
2483
|
-
current.sumY += node.y
|
|
2484
|
-
if ((state.nodeDegrees.get(node.id) ?? 0) > current.degree) {
|
|
2485
|
-
current.representative = node
|
|
2486
|
-
current.degree = state.nodeDegrees.get(node.id) ?? 0
|
|
2487
|
-
}
|
|
2488
|
-
continue
|
|
2489
|
-
}
|
|
2490
|
-
|
|
2491
|
-
buckets.set(key, {
|
|
2492
|
-
id: key,
|
|
2493
|
-
count: 1,
|
|
2494
|
-
sumX: node.x,
|
|
2495
|
-
sumY: node.y,
|
|
2496
|
-
representative: node,
|
|
2497
|
-
degree: state.nodeDegrees.get(node.id) ?? 0
|
|
2498
|
-
})
|
|
2499
|
-
}
|
|
2500
|
-
|
|
2501
|
-
return Array.from(buckets.values())
|
|
2502
|
-
.sort((left, right) => right.count - left.count)
|
|
2503
|
-
.slice(0, Math.min(renderNodeBudget, 900))
|
|
2504
|
-
.map((cluster) => ({
|
|
2505
|
-
id: cluster.id,
|
|
2506
|
-
x: cluster.sumX / Math.max(cluster.count, 1),
|
|
2507
|
-
y: cluster.sumY / Math.max(cluster.count, 1),
|
|
2508
|
-
count: cluster.count,
|
|
2509
|
-
representative: cluster.representative
|
|
2510
|
-
}))
|
|
2511
|
-
}
|
|
2512
|
-
|
|
2513
|
-
const representativeNodesFromClusters = (clusters, limit) => {
|
|
2514
|
-
const representatives = clusters
|
|
2515
|
-
.map((cluster) => cluster.representative)
|
|
2516
|
-
.filter((node) => Boolean(node))
|
|
2517
|
-
const merged = mergeUniqueNodes(
|
|
2518
|
-
representatives,
|
|
2519
|
-
state.renderNodes ?? [],
|
|
2520
|
-
Math.max(1, limit)
|
|
2521
|
-
)
|
|
2522
|
-
return ensureHubNodesInRenderedSet(merged)
|
|
2523
|
-
}
|
|
2524
|
-
|
|
2525
2241
|
const computeRenderVisibility = () => {
|
|
2526
2242
|
if (!hasValidTransform()) {
|
|
2527
2243
|
fitView({ useFiltered: true })
|
|
@@ -2539,40 +2255,9 @@ const computeRenderVisibility = () => {
|
|
|
2539
2255
|
}
|
|
2540
2256
|
state.lastViewportKey = viewportKey
|
|
2541
2257
|
state.renderVisibilityDirty = false
|
|
2542
|
-
state.renderClusterEdges = []
|
|
2543
|
-
|
|
2544
|
-
const shouldRenderMacroGalaxy = shouldRenderMacroGalaxyView()
|
|
2545
|
-
|
|
2546
|
-
if (shouldRenderMacroGalaxy) {
|
|
2547
|
-
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2548
|
-
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
2549
|
-
const representative = state.primaryHub ?? state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
2550
|
-
if (representative) {
|
|
2551
|
-
state.renderClusters = [
|
|
2552
|
-
{
|
|
2553
|
-
id: 'macro-galaxy',
|
|
2554
|
-
x: state.macroCenter.x,
|
|
2555
|
-
y: state.macroCenter.y,
|
|
2556
|
-
count: sourceNodes.length,
|
|
2557
|
-
representative
|
|
2558
|
-
}
|
|
2559
|
-
]
|
|
2560
|
-
state.renderNodes = [representative]
|
|
2561
|
-
} else {
|
|
2562
|
-
state.renderClusters = []
|
|
2563
|
-
state.renderNodes = []
|
|
2564
|
-
}
|
|
2565
|
-
state.renderEdges = []
|
|
2566
|
-
state.renderClusterEdges = []
|
|
2567
|
-
return
|
|
2568
|
-
}
|
|
2569
|
-
|
|
2570
|
-
state.ecosystemViewActive = false
|
|
2571
2258
|
|
|
2572
2259
|
if (state.visibleNodes.length <= 2000) {
|
|
2573
2260
|
state.renderNodes = state.visibleNodes
|
|
2574
|
-
state.renderClusters = []
|
|
2575
|
-
state.renderClusterEdges = []
|
|
2576
2261
|
const ids = new Set(state.renderNodes.map((node) => node.id))
|
|
2577
2262
|
state.renderEdges = withMeshEdges(state.renderNodes, collectVisibleEdgesForNodes(ids))
|
|
2578
2263
|
return
|
|
@@ -2623,9 +2308,6 @@ const computeRenderVisibility = () => {
|
|
|
2623
2308
|
const sampledWithHubsIds = new Set(sampledNodes.map((node) => node.id))
|
|
2624
2309
|
sampledEdges = collectVisibleEdgesForNodes(sampledWithHubsIds)
|
|
2625
2310
|
}
|
|
2626
|
-
|
|
2627
|
-
state.renderClusters = []
|
|
2628
|
-
state.renderClusterEdges = []
|
|
2629
2311
|
state.renderNodes = sampledNodes
|
|
2630
2312
|
state.renderEdges = withMeshEdges(sampledNodes, sampledEdges)
|
|
2631
2313
|
return
|
|
@@ -2634,24 +2316,12 @@ const computeRenderVisibility = () => {
|
|
|
2634
2316
|
if (state.transform.scale <= 0.0015) {
|
|
2635
2317
|
const sampled = sampleVisibleNodes(Math.min(renderNodeBudget, 900))
|
|
2636
2318
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
2637
|
-
state.renderClusters = []
|
|
2638
|
-
state.renderClusterEdges = []
|
|
2639
2319
|
state.renderNodes = sampled
|
|
2640
2320
|
state.renderEdges = withMeshEdges(sampled, collectVisibleEdgesForNodes(sampledIds))
|
|
2641
2321
|
return
|
|
2642
2322
|
}
|
|
2643
2323
|
|
|
2644
2324
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
2645
|
-
const clusters = clusterViewportNodes(viewportNodes)
|
|
2646
|
-
if (clusters.length > 0) {
|
|
2647
|
-
state.renderClusters = []
|
|
2648
|
-
state.renderClusterEdges = []
|
|
2649
|
-
state.renderNodes = representativeNodesFromClusters(clusters, Math.min(renderNodeBudget, 900))
|
|
2650
|
-
state.renderEdges = []
|
|
2651
|
-
return
|
|
2652
|
-
}
|
|
2653
|
-
state.renderClusters = []
|
|
2654
|
-
state.renderClusterEdges = []
|
|
2655
2325
|
const stride = viewportNodeStride()
|
|
2656
2326
|
const picked = []
|
|
2657
2327
|
|
|
@@ -2674,8 +2344,6 @@ const computeRenderVisibility = () => {
|
|
|
2674
2344
|
const fallbackNodes = fallbackViewportNodes()
|
|
2675
2345
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2676
2346
|
state.renderNodes = fallbackNodes
|
|
2677
|
-
state.renderClusters = []
|
|
2678
|
-
state.renderClusterEdges = []
|
|
2679
2347
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2680
2348
|
return
|
|
2681
2349
|
}
|
|
@@ -2690,8 +2358,6 @@ const computeRenderVisibility = () => {
|
|
|
2690
2358
|
if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
|
|
2691
2359
|
const fallbackNodes = sampleVisibleNodes(Math.min(renderNodeBudget, 260))
|
|
2692
2360
|
const fallbackIds = new Set(fallbackNodes.map((node) => node.id))
|
|
2693
|
-
state.renderClusters = []
|
|
2694
|
-
state.renderClusterEdges = []
|
|
2695
2361
|
state.renderNodes = fallbackNodes
|
|
2696
2362
|
state.renderEdges = withMeshEdges(fallbackNodes, collectVisibleEdgesForNodes(fallbackIds))
|
|
2697
2363
|
}
|
|
@@ -2802,69 +2468,9 @@ const render = now => {
|
|
|
2802
2468
|
: state.renderNodes.length > 500
|
|
2803
2469
|
? 0.05
|
|
2804
2470
|
: 0
|
|
2805
|
-
const drawEdges =
|
|
2806
|
-
state.renderClusters.length === 0 &&
|
|
2807
|
-
state.transform.scale >= minimumEdgeScale
|
|
2471
|
+
const drawEdges = state.transform.scale >= minimumEdgeScale
|
|
2808
2472
|
if (drawAcceleratedGraph(width, height, drawEdges)) {
|
|
2809
2473
|
// WebGL handles the dense node/edge layer; the 2D canvas remains the interaction overlay.
|
|
2810
|
-
} else if (state.renderClusters.length > 0) {
|
|
2811
|
-
ctx.save()
|
|
2812
|
-
ctx.translate(state.transform.x, state.transform.y)
|
|
2813
|
-
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2814
|
-
const orderedClusters = [...state.renderClusters]
|
|
2815
|
-
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
2816
|
-
if (state.renderClusterEdges.length > 0) {
|
|
2817
|
-
for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
|
|
2818
|
-
const edge = state.renderClusterEdges[index]
|
|
2819
|
-
const edgeOpacity = Math.min(clusterOpacity(edge.sourceCluster), clusterOpacity(edge.targetCluster))
|
|
2820
|
-
if (edgeOpacity <= 0.01) {
|
|
2821
|
-
continue
|
|
2822
|
-
}
|
|
2823
|
-
const widthScale = 1
|
|
2824
|
-
ctx.beginPath()
|
|
2825
|
-
ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
|
|
2826
|
-
ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
|
|
2827
|
-
ctx.lineWidth = (1.2 * widthScale) / safeScale
|
|
2828
|
-
ctx.strokeStyle = 'rgba(153, 165, 181, ' + (edge.inferred ? 0.14 : 0.22) * edgeOpacity + ')'
|
|
2829
|
-
ctx.stroke()
|
|
2830
|
-
}
|
|
2831
|
-
}
|
|
2832
|
-
orderedClusters.forEach(cluster => {
|
|
2833
|
-
const isMacro = cluster.id === 'macro-galaxy'
|
|
2834
|
-
const isEcosystem = String(cluster.id).startsWith('ecosystem-')
|
|
2835
|
-
const isHub = Boolean(cluster.isHub)
|
|
2836
|
-
const opacity = clusterOpacity(cluster)
|
|
2837
|
-
if (opacity <= 0.01) {
|
|
2838
|
-
return
|
|
2839
|
-
}
|
|
2840
|
-
const radiusPx = clusterRadiusPx(cluster)
|
|
2841
|
-
const radius = radiusPx / safeScale
|
|
2842
|
-
const haloRadius = (radiusPx + (isMacro ? 8 : isHub ? 4 : isEcosystem ? 1.1 : 4)) / safeScale
|
|
2843
|
-
ctx.globalAlpha = opacity
|
|
2844
|
-
if (isHub || !isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
|
|
2845
|
-
ctx.beginPath()
|
|
2846
|
-
ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
|
|
2847
|
-
ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
|
|
2848
|
-
ctx.fill()
|
|
2849
|
-
}
|
|
2850
|
-
ctx.beginPath()
|
|
2851
|
-
ctx.arc(cluster.x, cluster.y, radius, 0, Math.PI * 2)
|
|
2852
|
-
ctx.fillStyle = isMacro ? '#f3f7fb' : graphTheme.node
|
|
2853
|
-
ctx.fill()
|
|
2854
|
-
ctx.lineWidth = (isEcosystem && !isHub ? 0.7 : 1.4) / safeScale
|
|
2855
|
-
ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
|
|
2856
|
-
ctx.stroke()
|
|
2857
|
-
if (isMacro && cluster.representative?.title) {
|
|
2858
|
-
ctx.fillStyle = '#edf2f7'
|
|
2859
|
-
ctx.font = 12 / safeScale + 'px Inter, system-ui, sans-serif'
|
|
2860
|
-
ctx.textAlign = 'center'
|
|
2861
|
-
ctx.textBaseline = 'top'
|
|
2862
|
-
ctx.fillText(cluster.representative.title.slice(0, 28), cluster.x, cluster.y + (radiusPx + 9) / safeScale)
|
|
2863
|
-
}
|
|
2864
|
-
ctx.globalAlpha = 1
|
|
2865
|
-
// Keep cluster markers minimal and faster to draw on large graphs.
|
|
2866
|
-
})
|
|
2867
|
-
ctx.restore()
|
|
2868
2474
|
} else {
|
|
2869
2475
|
ctx.save()
|
|
2870
2476
|
ctx.translate(state.transform.x, state.transform.y)
|
|
@@ -2875,7 +2481,7 @@ const render = now => {
|
|
|
2875
2481
|
drawGraphNodes()
|
|
2876
2482
|
ctx.restore()
|
|
2877
2483
|
}
|
|
2878
|
-
if (state.renderNodes.length === 0
|
|
2484
|
+
if (state.renderNodes.length === 0) {
|
|
2879
2485
|
ctx.fillStyle = '#99a5b5'
|
|
2880
2486
|
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
2881
2487
|
ctx.textAlign = 'center'
|
package/package.json
CHANGED