@andespindola/brainlink 0.1.0-beta.114 → 0.1.0-beta.115
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 +8 -14
- package/dist/application/frontend/client-js.js +22 -975
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,12 +84,12 @@ 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
|
|
87
|
+
- Graph zoom-out keeps the same Bloom-like scene model for every graph size: nodes remain part of one flat exploration scene while rendering budgets decide how much visible detail is drawn.
|
|
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
|
-
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable
|
|
91
|
-
- Zoomed-out graph LOD
|
|
92
|
-
- Graph reset
|
|
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.
|
|
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.
|
|
95
95
|
|
|
@@ -595,22 +595,16 @@ The graph UI shows:
|
|
|
595
595
|
- realtime refresh while `--watch` is enabled
|
|
596
596
|
- graph controls for zoom in, zoom out, fit visible nodes and reset-to-fit-all
|
|
597
597
|
- wheel zoom (including `cmd+scroll` and `ctrl+scroll`) anchored to cursor position for faster navigation in large graphs
|
|
598
|
-
- continuous target-scale interpolation for wheel/button zoom to avoid abrupt
|
|
599
|
-
-
|
|
600
|
-
- zoom-out floor for large and massive graphs
|
|
598
|
+
- continuous target-scale interpolation for wheel/button zoom to avoid abrupt jumps while keeping cursor-anchored focus
|
|
599
|
+
- Bloom-like scene navigation: reset fits the current graph scene, wheel zoom stays anchored to the cursor, and WebGL acceleration is used only to draw the same flat graph faster
|
|
600
|
+
- zoom-out floor for large and massive graphs to keep the scene reachable without switching into a separate macro graph mode
|
|
601
601
|
- keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
|
|
602
602
|
- double-click on canvas zooms in at cursor position
|
|
603
603
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
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
|
-
- hierarchical hot-path optimizations reduce per-frame allocations and repeated scans during layered cluster expansion and edge projection
|
|
607
|
-
- hierarchical edge projection now caches hub membership and node-to-cluster resolution per frame to keep large recursive subgraph rendering smooth during continuous zoom and pan
|
|
608
|
-
- hierarchical projection now uses stronger perspective yaw/pitch and depth-based render ordering so layered subgraphs read as a true 3D field instead of a flat expansion
|
|
609
|
-
- node rendering also applies depth projection cues on large visible sets (position, scale, opacity and edge depth weighting) so the graph keeps 3D perception when leaving cluster-only LOD layers
|
|
610
606
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
611
|
-
-
|
|
612
|
-
- 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
|
|
613
|
-
- graph LOD progression: hierarchical rendering now follows one recursive graph-of-graphs standard whenever a graph has more than one hierarchy level; each level expands through intermediate subgraph sizes (instead of jumping directly to leaves), starts from a memory-hub-centered mesh, and each supernode can expand into another same-shape subgraph level (up to 999 children) with latent fade-in, aggregated real links and local sibling mesh links so org-heavy-like and stress-50k-like structures share the same layered behavior at different depths; layered clusters also receive stronger perspective depth projection (Z-depth) with vertical camera tilt/parallax so expansion reads as a true depth field instead of a flat 2D switch; for massive graphs the first expansion starts deeper in zoom and is additionally gated by focus readiness (screen-space isolation of the focused parent) so child levels open only when that subgraph is truly centered and separated in view
|
|
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
|
|
614
608
|
|
|
615
609
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
616
610
|
|
|
@@ -23,43 +23,7 @@ const transformCoordinateLimit = 20_000_000
|
|
|
23
23
|
const hoverHitTestIntervalMs = 64
|
|
24
24
|
const ecosystemLevelNodeCap = 999
|
|
25
25
|
const ecosystemActivationNodeThreshold = 1000
|
|
26
|
-
const ecosystemClusterEdgeLimit = 520
|
|
27
|
-
const ecosystemHubEdgeLimit = 120
|
|
28
|
-
const ecosystemSiblingEdgeLimit = 180
|
|
29
|
-
const ecosystemClusterScaleThreshold = 0.78
|
|
30
|
-
const massiveEcosystemClusterScaleThreshold = 4.2
|
|
31
|
-
const ecosystemClusterEnterHysteresis = 0.94
|
|
32
|
-
const ecosystemClusterExitHysteresis = 1.1
|
|
33
26
|
const ecosystemSubgraphScaleThreshold = 0.18
|
|
34
|
-
const ecosystemMicroScaleThreshold = 0.08
|
|
35
|
-
const ecosystemFocusedParentLimit = 2
|
|
36
|
-
const ecosystemDepthNear = 80
|
|
37
|
-
const ecosystemDepthFar = 2600
|
|
38
|
-
const ecosystemDepthPerspective = 560
|
|
39
|
-
const ecosystemDepthTiltY = 0.3
|
|
40
|
-
const ecosystemDepthYaw = 0.3
|
|
41
|
-
const ecosystemDepthPitch = 0.24
|
|
42
|
-
const ecosystemDepthRadialGain = 0.13
|
|
43
|
-
const ecosystemDepthOrbitalMaxOffset = 160
|
|
44
|
-
const ecosystemDepthMinScale = 0.2
|
|
45
|
-
const ecosystemDepthOpacityFloor = 0.16
|
|
46
|
-
const graphDepthNear = 40
|
|
47
|
-
const graphDepthFar = 1320
|
|
48
|
-
const graphDepthPerspective = 430
|
|
49
|
-
const graphDepthYaw = 0.42
|
|
50
|
-
const graphDepthPitch = 0.3
|
|
51
|
-
const graphDepthRadialGain = 0.24
|
|
52
|
-
const graphDepthMinScale = 0.34
|
|
53
|
-
const graphDepthOpacityFloor = 0.22
|
|
54
|
-
const graphDepthEdgeOpacityFloor = 0.12
|
|
55
|
-
const graphDepthProjectionNodeThreshold = 40
|
|
56
|
-
const graphDepthProjectionNodeCap = 2600
|
|
57
|
-
const graphDepthProjectionMinScale = 0.03
|
|
58
|
-
const graphDepthProjectionMaxScale = 1.7
|
|
59
|
-
const graphDepthProjectionEnterMinScale = graphDepthProjectionMinScale * 1.08
|
|
60
|
-
const graphDepthProjectionExitMinScale = graphDepthProjectionMinScale * 0.88
|
|
61
|
-
const graphDepthProjectionEnterMaxScale = graphDepthProjectionMaxScale * 0.92
|
|
62
|
-
const graphDepthProjectionExitMaxScale = graphDepthProjectionMaxScale * 1.16
|
|
63
27
|
const zoomRecoveryGuardMs = 4200
|
|
64
28
|
const zoomCapTargetViewportShare = 0.72
|
|
65
29
|
const meshEdgeScaleThreshold = 0.09
|
|
@@ -77,11 +41,6 @@ const zoomAnimationSlowLerp = 0.18
|
|
|
77
41
|
const zoomAnimationFastLerp = 0.36
|
|
78
42
|
const zoomAnimationScaleSnap = 0.00008
|
|
79
43
|
const zoomAnimationPositionSnap = 0.14
|
|
80
|
-
const bloomZoomFocusMaxScreenDistance = 260
|
|
81
|
-
const bloomZoomFocusFarStrength = 0.66
|
|
82
|
-
const bloomZoomFocusNearStrength = 0.34
|
|
83
|
-
const bloomCameraParallaxNearStrength = 0.2
|
|
84
|
-
const bloomCameraParallaxFarStrength = 0.06
|
|
85
44
|
const physicsDragFrameIntervalMs = 16
|
|
86
45
|
const physicsIdleFrameIntervalMs = 78
|
|
87
46
|
const physicsLargeGraphIdleFrameIntervalMs = 108
|
|
@@ -650,29 +609,15 @@ const recomputeVisibility = () => {
|
|
|
650
609
|
y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
|
|
651
610
|
}
|
|
652
611
|
: { x: 0, y: 0 }
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
hubCluster: null
|
|
663
|
-
}
|
|
664
|
-
state.ecosystemClusters = ecosystemGraph.clusters
|
|
665
|
-
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
666
|
-
state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
|
|
667
|
-
state.ecosystemLevelSizes = ecosystemGraph.levelSizes
|
|
668
|
-
state.ecosystemLevelIndexBySize = ecosystemGraph.levelSizes.reduce((map, size, index) => {
|
|
669
|
-
map.set(size, index)
|
|
670
|
-
return map
|
|
671
|
-
}, new Map())
|
|
672
|
-
state.ecosystemHubNodeIds = new Set(ecosystemGraph.hubCluster?.nodeIds ?? [])
|
|
673
|
-
state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
|
|
674
|
-
state.ecosystemBaseSize = ecosystemGraph.baseSize
|
|
675
|
-
state.ecosystemHubCluster = ecosystemGraph.hubCluster
|
|
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
|
|
676
621
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
677
622
|
markRenderDirty()
|
|
678
623
|
}
|
|
@@ -778,271 +723,12 @@ const createVisibleEdgeLookup = edges => {
|
|
|
778
723
|
return lookup
|
|
779
724
|
}
|
|
780
725
|
|
|
781
|
-
const ecosystemKeyForNode = node => {
|
|
782
|
-
if (typeof node.segment === 'string' && node.segment.trim()) {
|
|
783
|
-
return node.segment.trim()
|
|
784
|
-
}
|
|
785
|
-
if (typeof node.group === 'string' && node.group.trim()) {
|
|
786
|
-
return node.group.trim()
|
|
787
|
-
}
|
|
788
|
-
const pathParts = String(node.path || '')
|
|
789
|
-
.split('/')
|
|
790
|
-
.filter(part => part.trim())
|
|
791
|
-
.slice(0, 2)
|
|
792
|
-
return pathParts.length > 0 ? pathParts.join('/') : 'root'
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const compareNodesForEcosystem = (left, right) => {
|
|
796
|
-
const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
|
|
797
|
-
if (keyComparison !== 0) return keyComparison
|
|
798
|
-
const leftDegree = state.nodeDegrees.get(left.id) ?? 0
|
|
799
|
-
const rightDegree = state.nodeDegrees.get(right.id) ?? 0
|
|
800
|
-
if (leftDegree !== rightDegree) return rightDegree - leftDegree
|
|
801
|
-
return String(left.title || left.id).localeCompare(String(right.title || right.id))
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const selectEcosystemRepresentative = nodes => {
|
|
805
|
-
let representative = nodes[0] ?? null
|
|
806
|
-
let representativeScore = Number.NEGATIVE_INFINITY
|
|
807
|
-
|
|
808
|
-
for (let index = 0; index < nodes.length; index += 1) {
|
|
809
|
-
const node = nodes[index]
|
|
810
|
-
const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
|
|
811
|
-
if (score > representativeScore) {
|
|
812
|
-
representative = node
|
|
813
|
-
representativeScore = score
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
return representative
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
const ecosystemLayoutSpacingForSize = size => {
|
|
821
|
-
if (size >= ecosystemLevelNodeCap) return 260
|
|
822
|
-
if (size >= 320) return 110
|
|
823
|
-
if (size >= 120) return 64
|
|
824
|
-
if (size >= 48) return 34
|
|
825
|
-
if (size >= 18) return 18
|
|
826
|
-
if (size >= 8) return 11
|
|
827
|
-
return 7
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const buildIntermediateEcosystemSizes = (fromSize, toSize) => {
|
|
831
|
-
if (fromSize <= toSize + 1) {
|
|
832
|
-
return []
|
|
833
|
-
}
|
|
834
|
-
const intermediate = []
|
|
835
|
-
let current = fromSize
|
|
836
|
-
while (current > toSize + 1) {
|
|
837
|
-
const stepped = Math.max(toSize + 1, Math.ceil(current / 3))
|
|
838
|
-
if (stepped >= current) {
|
|
839
|
-
break
|
|
840
|
-
}
|
|
841
|
-
intermediate.push(stepped)
|
|
842
|
-
current = stepped
|
|
843
|
-
}
|
|
844
|
-
return intermediate
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
const buildEcosystemLevelSizes = nodeCount => {
|
|
848
|
-
if (nodeCount <= 0) return []
|
|
849
|
-
const primarySizes = []
|
|
850
|
-
let currentSize = Math.max(1, Math.ceil(nodeCount / ecosystemLevelNodeCap))
|
|
851
|
-
while (currentSize >= 1) {
|
|
852
|
-
primarySizes.push(currentSize)
|
|
853
|
-
if (currentSize === 1) {
|
|
854
|
-
break
|
|
855
|
-
}
|
|
856
|
-
const nextSize = Math.max(1, Math.ceil(currentSize / ecosystemLevelNodeCap))
|
|
857
|
-
if (nextSize === currentSize) {
|
|
858
|
-
break
|
|
859
|
-
}
|
|
860
|
-
currentSize = nextSize
|
|
861
|
-
}
|
|
862
|
-
const expandedSizes = []
|
|
863
|
-
for (let index = 0; index < primarySizes.length; index += 1) {
|
|
864
|
-
const size = primarySizes[index]
|
|
865
|
-
if (expandedSizes.length === 0 || expandedSizes[expandedSizes.length - 1] !== size) {
|
|
866
|
-
expandedSizes.push(size)
|
|
867
|
-
}
|
|
868
|
-
const nextSize = primarySizes[index + 1]
|
|
869
|
-
if (!Number.isFinite(nextSize)) {
|
|
870
|
-
continue
|
|
871
|
-
}
|
|
872
|
-
const intermediate = buildIntermediateEcosystemSizes(size, nextSize)
|
|
873
|
-
for (let intermediateIndex = 0; intermediateIndex < intermediate.length; intermediateIndex += 1) {
|
|
874
|
-
const candidate = intermediate[intermediateIndex]
|
|
875
|
-
if (expandedSizes[expandedSizes.length - 1] !== candidate) {
|
|
876
|
-
expandedSizes.push(candidate)
|
|
877
|
-
}
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
if (expandedSizes[expandedSizes.length - 1] !== 1) {
|
|
881
|
-
expandedSizes.push(1)
|
|
882
|
-
}
|
|
883
|
-
return expandedSizes
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
|
|
887
|
-
if (levelSizes.length <= 1) {
|
|
888
|
-
return []
|
|
889
|
-
}
|
|
890
|
-
const isMassive = nodeCount > massiveGraphNodeThreshold
|
|
891
|
-
const maxScale = isMassive
|
|
892
|
-
? massiveEcosystemClusterScaleThreshold
|
|
893
|
-
: ecosystemClusterScaleThreshold
|
|
894
|
-
const startScale = isMassive ? 1.12 : 0.24
|
|
895
|
-
const transitionCount = levelSizes.length - 1
|
|
896
|
-
const usableScale = Math.max(0.08, maxScale - startScale)
|
|
897
|
-
const step = usableScale / transitionCount
|
|
898
|
-
const stride = isMassive ? 0.93 : 0.82
|
|
899
|
-
const overlap = isMassive ? 1.22 : 1.62
|
|
900
|
-
const levels = []
|
|
901
|
-
for (let index = 0; index < transitionCount; index += 1) {
|
|
902
|
-
const start = startScale + step * index * stride
|
|
903
|
-
const end = Math.min(maxScale, start + step * overlap)
|
|
904
|
-
levels.push({
|
|
905
|
-
parentSize: levelSizes[index],
|
|
906
|
-
childSize: levelSizes[index + 1],
|
|
907
|
-
start,
|
|
908
|
-
end
|
|
909
|
-
})
|
|
910
|
-
}
|
|
911
|
-
return levels
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const ecosystemCompactPoint = (index, total, center, spacing) => {
|
|
915
|
-
if (total <= 1) {
|
|
916
|
-
return { x: center.x, y: center.y }
|
|
917
|
-
}
|
|
918
|
-
const angle = index * 2.399963229728653
|
|
919
|
-
const radius = spacing * Math.sqrt(index + 1)
|
|
920
|
-
return {
|
|
921
|
-
x: center.x + Math.cos(angle) * radius,
|
|
922
|
-
y: center.y + Math.sin(angle) * radius
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const buildEcosystemCluster = (nodes, index, point) => {
|
|
927
|
-
const count = Math.max(nodes.length, 1)
|
|
928
|
-
const representative = selectEcosystemRepresentative(nodes)
|
|
929
|
-
|
|
930
|
-
return {
|
|
931
|
-
id: 'ecosystem-' + index,
|
|
932
|
-
x: point.x,
|
|
933
|
-
y: point.y,
|
|
934
|
-
count,
|
|
935
|
-
nodeIds: nodes.map(node => node.id),
|
|
936
|
-
representative,
|
|
937
|
-
label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
const buildEcosystemHubCluster = (hub, center) => hub
|
|
942
|
-
? {
|
|
943
|
-
id: 'ecosystem-hub',
|
|
944
|
-
x: center.x,
|
|
945
|
-
y: center.y,
|
|
946
|
-
count: 1,
|
|
947
|
-
size: 1,
|
|
948
|
-
nodeIds: [hub.id],
|
|
949
|
-
representative: hub,
|
|
950
|
-
label: hub.title || 'Memory Hub',
|
|
951
|
-
parentId: null,
|
|
952
|
-
parentX: null,
|
|
953
|
-
parentY: null,
|
|
954
|
-
isHub: true
|
|
955
|
-
}
|
|
956
|
-
: null
|
|
957
|
-
|
|
958
|
-
const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
|
|
959
|
-
const clusters = []
|
|
960
|
-
const clusterByNodeId = new Map()
|
|
961
|
-
const parentChildIndex = new Map()
|
|
962
|
-
|
|
963
|
-
for (let offset = 0; offset < sortedNodes.length; offset += size) {
|
|
964
|
-
const clusterNodes = sortedNodes.slice(offset, offset + size)
|
|
965
|
-
const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
|
|
966
|
-
const siblingIndex = parentCluster
|
|
967
|
-
? (parentChildIndex.get(parentCluster.id) ?? 0)
|
|
968
|
-
: clusters.length
|
|
969
|
-
if (parentCluster) {
|
|
970
|
-
parentChildIndex.set(parentCluster.id, siblingIndex + 1)
|
|
971
|
-
}
|
|
972
|
-
const point = parentCluster
|
|
973
|
-
? ecosystemCompactPoint(siblingIndex, Math.ceil((parentCluster.count || size) / size), parentCluster, ecosystemLayoutSpacingForSize(size))
|
|
974
|
-
: ecosystemCompactPoint(clusters.length, Math.ceil(sortedNodes.length / size), center, ecosystemLayoutSpacingForSize(size))
|
|
975
|
-
const cluster = {
|
|
976
|
-
...buildEcosystemCluster(clusterNodes, clusters.length, point),
|
|
977
|
-
id: 'ecosystem-' + size + '-' + clusters.length,
|
|
978
|
-
size,
|
|
979
|
-
parentId: parentCluster?.id ?? null,
|
|
980
|
-
parentX: parentCluster?.x ?? null,
|
|
981
|
-
parentY: parentCluster?.y ?? null
|
|
982
|
-
}
|
|
983
|
-
clusters.push(cluster)
|
|
984
|
-
for (let index = 0; index < clusterNodes.length; index += 1) {
|
|
985
|
-
clusterByNodeId.set(clusterNodes[index].id, cluster)
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
|
|
989
|
-
return { clusters, clusterByNodeId }
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
const buildEcosystemGraph = (nodes, center, hub) => {
|
|
993
|
-
if (nodes.length === 0) {
|
|
994
|
-
return {
|
|
995
|
-
clusters: [],
|
|
996
|
-
clustersBySize: new Map(),
|
|
997
|
-
nodeClusterBySize: new Map(),
|
|
998
|
-
levelSizes: [],
|
|
999
|
-
expansionLevels: [],
|
|
1000
|
-
baseSize: ecosystemLevelNodeCap,
|
|
1001
|
-
hubCluster: null
|
|
1002
|
-
}
|
|
1003
|
-
}
|
|
1004
|
-
|
|
1005
|
-
const hubCluster = buildEcosystemHubCluster(hub, center)
|
|
1006
|
-
const sortedNodes = nodes
|
|
1007
|
-
.filter(node => node.id !== hub?.id)
|
|
1008
|
-
.sort(compareNodesForEcosystem)
|
|
1009
|
-
const levelSizes = buildEcosystemLevelSizes(sortedNodes.length)
|
|
1010
|
-
const expansionLevels = buildEcosystemExpansionLevels(levelSizes, nodes.length)
|
|
1011
|
-
const baseSize = levelSizes[0] ?? ecosystemLevelNodeCap
|
|
1012
|
-
const clustersBySize = new Map()
|
|
1013
|
-
const nodeClusterBySize = new Map()
|
|
1014
|
-
let parentLookup = null
|
|
1015
|
-
|
|
1016
|
-
for (let index = 0; index < levelSizes.length; index += 1) {
|
|
1017
|
-
const size = levelSizes[index]
|
|
1018
|
-
const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
|
|
1019
|
-
clustersBySize.set(size, level.clusters)
|
|
1020
|
-
nodeClusterBySize.set(size, level.clusterByNodeId)
|
|
1021
|
-
parentLookup = level.clusterByNodeId
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
return {
|
|
1025
|
-
clusters: clustersBySize.get(baseSize) ?? [],
|
|
1026
|
-
clustersBySize,
|
|
1027
|
-
nodeClusterBySize,
|
|
1028
|
-
levelSizes,
|
|
1029
|
-
expansionLevels,
|
|
1030
|
-
baseSize,
|
|
1031
|
-
hubCluster
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
726
|
const isClusterInViewport = (cluster, viewport) =>
|
|
1036
727
|
cluster.x >= viewport.minX &&
|
|
1037
728
|
cluster.x <= viewport.maxX &&
|
|
1038
729
|
cluster.y >= viewport.minY &&
|
|
1039
730
|
cluster.y <= viewport.maxY
|
|
1040
731
|
|
|
1041
|
-
const filterEcosystemClustersByViewport = (clusters, viewport) => {
|
|
1042
|
-
const visible = clusters.filter(cluster => isClusterInViewport(cluster, viewport))
|
|
1043
|
-
return visible.length > 0 ? visible : [...clusters]
|
|
1044
|
-
}
|
|
1045
|
-
|
|
1046
732
|
const ecosystemFocusPoint = () => {
|
|
1047
733
|
const cursorPoint = cursorWorldPoint()
|
|
1048
734
|
if (cursorPoint) {
|
|
@@ -1055,423 +741,6 @@ const ecosystemFocusPoint = () => {
|
|
|
1055
741
|
return viewportCenterWorldPoint()
|
|
1056
742
|
}
|
|
1057
743
|
|
|
1058
|
-
const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
|
|
1059
|
-
clusters
|
|
1060
|
-
.map(cluster => ({
|
|
1061
|
-
cluster,
|
|
1062
|
-
distance: Math.max(
|
|
1063
|
-
0,
|
|
1064
|
-
Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y) -
|
|
1065
|
-
clusterRadiusPx(cluster) / Math.max(state.transform.scale, 0.0001)
|
|
1066
|
-
)
|
|
1067
|
-
}))
|
|
1068
|
-
.sort((left, right) => left.distance - right.distance)
|
|
1069
|
-
.slice(0, limit)
|
|
1070
|
-
.map(item => item.cluster.id)
|
|
1071
|
-
|
|
1072
|
-
const focusedParentCluster = (clusters, focusPoint) => {
|
|
1073
|
-
if (clusters.length === 0) {
|
|
1074
|
-
return null
|
|
1075
|
-
}
|
|
1076
|
-
let focused = clusters[0]
|
|
1077
|
-
let nearestDistance = Number.POSITIVE_INFINITY
|
|
1078
|
-
for (let index = 0; index < clusters.length; index += 1) {
|
|
1079
|
-
const cluster = clusters[index]
|
|
1080
|
-
const distance = Math.max(
|
|
1081
|
-
0,
|
|
1082
|
-
Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y) -
|
|
1083
|
-
clusterRadiusPx(cluster) / Math.max(state.transform.scale, 0.0001)
|
|
1084
|
-
)
|
|
1085
|
-
if (distance < nearestDistance) {
|
|
1086
|
-
nearestDistance = distance
|
|
1087
|
-
focused = cluster
|
|
1088
|
-
}
|
|
1089
|
-
}
|
|
1090
|
-
return focused
|
|
1091
|
-
}
|
|
1092
|
-
|
|
1093
|
-
const nearestSiblingScreenDistancePx = (focusedCluster, clusters) => {
|
|
1094
|
-
let nearestDistancePx = Number.POSITIVE_INFINITY
|
|
1095
|
-
for (let index = 0; index < clusters.length; index += 1) {
|
|
1096
|
-
const cluster = clusters[index]
|
|
1097
|
-
if (cluster.id === focusedCluster.id) continue
|
|
1098
|
-
const distancePx = Math.hypot(
|
|
1099
|
-
(cluster.x - focusedCluster.x) * state.transform.scale,
|
|
1100
|
-
(cluster.y - focusedCluster.y) * state.transform.scale
|
|
1101
|
-
)
|
|
1102
|
-
if (distancePx < nearestDistancePx) {
|
|
1103
|
-
nearestDistancePx = distancePx
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
return nearestDistancePx
|
|
1107
|
-
}
|
|
1108
|
-
|
|
1109
|
-
const ecosystemFocusReadiness = (parentClusters, focusPoint, childSize) => {
|
|
1110
|
-
if (parentClusters.length <= 1) {
|
|
1111
|
-
return 1
|
|
1112
|
-
}
|
|
1113
|
-
const focusedCluster = focusedParentCluster(parentClusters, focusPoint)
|
|
1114
|
-
if (!focusedCluster) {
|
|
1115
|
-
return 0
|
|
1116
|
-
}
|
|
1117
|
-
const nearestDistancePx = nearestSiblingScreenDistancePx(focusedCluster, parentClusters)
|
|
1118
|
-
const sizeHalfCap = Math.ceil(ecosystemLevelNodeCap / 2)
|
|
1119
|
-
const sizeEighthCap = Math.ceil(ecosystemLevelNodeCap / 8)
|
|
1120
|
-
const focusDistanceTargetPx = childSize >= sizeHalfCap
|
|
1121
|
-
? 680
|
|
1122
|
-
: childSize >= sizeEighthCap
|
|
1123
|
-
? 520
|
|
1124
|
-
: 380
|
|
1125
|
-
const focusDistanceRangePx = childSize >= sizeHalfCap ? 260 : 220
|
|
1126
|
-
return smoothStep((nearestDistancePx - focusDistanceTargetPx) / focusDistanceRangePx)
|
|
1127
|
-
}
|
|
1128
|
-
const smoothStep = value => {
|
|
1129
|
-
const clamped = Math.max(0, Math.min(1, value))
|
|
1130
|
-
return clamped * clamped * (3 - clamped * 2)
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
const zoomProgress = (scale, start, end) =>
|
|
1134
|
-
smoothStep((scale - start) / Math.max(end - start, 0.0001))
|
|
1135
|
-
|
|
1136
|
-
const semanticZoomSpread = (progress, childSize) => {
|
|
1137
|
-
const spreadExponent = childSize <= Math.ceil(ecosystemLevelNodeCap / 12) ? 5.6 : 4.2
|
|
1138
|
-
const curve = Math.pow(progress, spreadExponent)
|
|
1139
|
-
if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
|
|
1140
|
-
return 0.12 + curve * 0.88
|
|
1141
|
-
}
|
|
1142
|
-
return curve
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const opacityForProgress = (progress, childSize) => {
|
|
1146
|
-
const opacityExponent = childSize <= Math.ceil(ecosystemLevelNodeCap / 12) ? 2.8 : 2.1
|
|
1147
|
-
const eased = Math.pow(progress, opacityExponent)
|
|
1148
|
-
if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
|
|
1149
|
-
return 0.22 + eased * 0.78
|
|
1150
|
-
}
|
|
1151
|
-
return eased
|
|
1152
|
-
}
|
|
1153
|
-
|
|
1154
|
-
const expandFocusedClusters = (parentClusters, focusPoint, childSize, progress, spread, viewport) => {
|
|
1155
|
-
const expandedParentIds = new Set(nearestEcosystemParentIds(
|
|
1156
|
-
parentClusters,
|
|
1157
|
-
focusPoint,
|
|
1158
|
-
ecosystemFocusedParentLimit
|
|
1159
|
-
))
|
|
1160
|
-
const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
|
|
1161
|
-
const visibleChildClusters = []
|
|
1162
|
-
for (let index = 0; index < childClusters.length; index += 1) {
|
|
1163
|
-
const cluster = childClusters[index]
|
|
1164
|
-
if (!expandedParentIds.has(cluster.parentId)) {
|
|
1165
|
-
continue
|
|
1166
|
-
}
|
|
1167
|
-
const spreadCluster = spreadChildClusterFromParent(cluster, childSize, progress, spread)
|
|
1168
|
-
if (!isClusterInViewport(spreadCluster, viewport)) {
|
|
1169
|
-
continue
|
|
1170
|
-
}
|
|
1171
|
-
visibleChildClusters.push(spreadCluster)
|
|
1172
|
-
}
|
|
1173
|
-
|
|
1174
|
-
return {
|
|
1175
|
-
expandedParentIds,
|
|
1176
|
-
childClusters: visibleChildClusters
|
|
1177
|
-
}
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
const spreadChildClusterFromParent = (cluster, childSize, progress, spread) => {
|
|
1181
|
-
if (!Number.isFinite(cluster.parentX) || !Number.isFinite(cluster.parentY)) {
|
|
1182
|
-
return {
|
|
1183
|
-
...cluster,
|
|
1184
|
-
lodOpacity: opacityForProgress(progress, childSize)
|
|
1185
|
-
}
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
return {
|
|
1189
|
-
...cluster,
|
|
1190
|
-
x: cluster.parentX + (cluster.x - cluster.parentX) * spread,
|
|
1191
|
-
y: cluster.parentY + (cluster.y - cluster.parentY) * spread,
|
|
1192
|
-
lodOpacity: opacityForProgress(progress, childSize)
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
const selectHierarchicalEcosystemClusters = viewport => {
|
|
1197
|
-
const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
|
|
1198
|
-
const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
|
|
1199
|
-
const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
|
|
1200
|
-
const visibleClusters = [...visibleBaseClusters]
|
|
1201
|
-
const clustersBySize = new Map()
|
|
1202
|
-
for (let index = 0; index < visibleBaseClusters.length; index += 1) {
|
|
1203
|
-
const cluster = visibleBaseClusters[index]
|
|
1204
|
-
const levelClusters = clustersBySize.get(cluster.size)
|
|
1205
|
-
if (levelClusters) {
|
|
1206
|
-
levelClusters.push(cluster)
|
|
1207
|
-
} else {
|
|
1208
|
-
clustersBySize.set(cluster.size, [cluster])
|
|
1209
|
-
}
|
|
1210
|
-
}
|
|
1211
|
-
const focusPoint = ecosystemFocusPoint()
|
|
1212
|
-
|
|
1213
|
-
for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
|
|
1214
|
-
const level = state.ecosystemExpansionLevels[index]
|
|
1215
|
-
const parentClusters = clustersBySize.get(level.parentSize) ?? []
|
|
1216
|
-
if (parentClusters.length === 0) {
|
|
1217
|
-
continue
|
|
1218
|
-
}
|
|
1219
|
-
const zoomLevelProgress = zoomProgress(state.transform.scale, level.start, level.end)
|
|
1220
|
-
const focusReadiness = ecosystemFocusReadiness(parentClusters, focusPoint, level.childSize)
|
|
1221
|
-
const progress = zoomLevelProgress * focusReadiness
|
|
1222
|
-
if (progress <= 0.002) {
|
|
1223
|
-
continue
|
|
1224
|
-
}
|
|
1225
|
-
const spread = semanticZoomSpread(progress, level.childSize)
|
|
1226
|
-
const expansion = expandFocusedClusters(parentClusters, focusPoint, level.childSize, progress, spread, viewport)
|
|
1227
|
-
visibleClusters.push(...expansion.childClusters)
|
|
1228
|
-
if (expansion.childClusters.length > 0) {
|
|
1229
|
-
const levelClusters = clustersBySize.get(level.childSize)
|
|
1230
|
-
if (levelClusters) {
|
|
1231
|
-
levelClusters.push(...expansion.childClusters)
|
|
1232
|
-
} else {
|
|
1233
|
-
clustersBySize.set(level.childSize, [...expansion.childClusters])
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
return [...hubClusters, ...visibleClusters]
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
const ecosystemLevelIndexBySize = () => state.ecosystemLevelIndexBySize
|
|
1242
|
-
|
|
1243
|
-
const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
|
|
1244
|
-
if (cluster.isHub) {
|
|
1245
|
-
return ecosystemDepthNear
|
|
1246
|
-
}
|
|
1247
|
-
const maxLevelIndex = Math.max(state.ecosystemLevelSizes.length - 1, 0)
|
|
1248
|
-
const levelIndex = levelIndexMap.get(cluster.size) ?? 0
|
|
1249
|
-
const reverseIndex = Math.max(0, maxLevelIndex - levelIndex)
|
|
1250
|
-
const normalized = maxLevelIndex === 0 ? 0 : reverseIndex / maxLevelIndex
|
|
1251
|
-
return ecosystemDepthNear + normalized * (ecosystemDepthFar - ecosystemDepthNear)
|
|
1252
|
-
}
|
|
1253
|
-
|
|
1254
|
-
const projectEcosystemPoint = (x, y, depth, anchor) => {
|
|
1255
|
-
const safeDepth = Math.max(0, depth)
|
|
1256
|
-
const dx = x - anchor.x
|
|
1257
|
-
const dy = y - anchor.y
|
|
1258
|
-
const yawSin = Math.sin(ecosystemDepthYaw)
|
|
1259
|
-
const yawCos = Math.cos(ecosystemDepthYaw)
|
|
1260
|
-
const pitchSin = Math.sin(ecosystemDepthPitch)
|
|
1261
|
-
const pitchCos = Math.cos(ecosystemDepthPitch)
|
|
1262
|
-
const rotatedX = dx * yawCos + safeDepth * yawSin
|
|
1263
|
-
const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
|
|
1264
|
-
const rotatedY = dy * pitchCos - rotatedZ * pitchSin
|
|
1265
|
-
const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
|
|
1266
|
-
const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + projectedDepth)
|
|
1267
|
-
const verticalTilt = projectedDepth * ecosystemDepthTiltY
|
|
1268
|
-
return {
|
|
1269
|
-
x: anchor.x + rotatedX * factor,
|
|
1270
|
-
y: anchor.y + rotatedY * factor - verticalTilt,
|
|
1271
|
-
factor,
|
|
1272
|
-
projectedDepth
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
|
|
1277
|
-
const levelIndexMap = ecosystemLevelIndexBySize()
|
|
1278
|
-
const effectiveAnchor = applyBloomCameraParallax(anchor)
|
|
1279
|
-
const projectedClusters = []
|
|
1280
|
-
const clusterById = new Map()
|
|
1281
|
-
|
|
1282
|
-
for (let index = 0; index < clusters.length; index += 1) {
|
|
1283
|
-
const cluster = clusters[index]
|
|
1284
|
-
const baseDepth = ecosystemDepthForCluster(cluster, levelIndexMap)
|
|
1285
|
-
const radialDistance = Math.hypot(cluster.x - effectiveAnchor.x, cluster.y - effectiveAnchor.y)
|
|
1286
|
-
const radialOffset = cluster.isHub ? 0 : Math.min(320, radialDistance * ecosystemDepthRadialGain)
|
|
1287
|
-
const orbitalOffset = cluster.isHub
|
|
1288
|
-
? 0
|
|
1289
|
-
: Math.sin(Math.atan2(cluster.y - effectiveAnchor.y, cluster.x - effectiveAnchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
|
|
1290
|
-
const depth = Math.max(0, baseDepth + radialOffset + orbitalOffset)
|
|
1291
|
-
const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, effectiveAnchor)
|
|
1292
|
-
const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
|
|
1293
|
-
const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
|
|
1294
|
-
const depthOpacity = Math.max(
|
|
1295
|
-
ecosystemDepthOpacityFloor,
|
|
1296
|
-
Math.min(1, depthScale * 1.08)
|
|
1297
|
-
)
|
|
1298
|
-
const projectedCluster = {
|
|
1299
|
-
...cluster,
|
|
1300
|
-
x: projected.x,
|
|
1301
|
-
y: projected.y,
|
|
1302
|
-
lodOpacity: baseOpacity * depthOpacity,
|
|
1303
|
-
depth: projected.projectedDepth,
|
|
1304
|
-
depthScale
|
|
1305
|
-
}
|
|
1306
|
-
projectedClusters.push(projectedCluster)
|
|
1307
|
-
clusterById.set(projectedCluster.id, projectedCluster)
|
|
1308
|
-
}
|
|
1309
|
-
|
|
1310
|
-
const projectedEdges = []
|
|
1311
|
-
for (let index = 0; index < edges.length; index += 1) {
|
|
1312
|
-
const edge = edges[index]
|
|
1313
|
-
const sourceCluster = clusterById.get(edge.sourceCluster.id)
|
|
1314
|
-
const targetCluster = clusterById.get(edge.targetCluster.id)
|
|
1315
|
-
if (!sourceCluster || !targetCluster) {
|
|
1316
|
-
continue
|
|
1317
|
-
}
|
|
1318
|
-
projectedEdges.push({
|
|
1319
|
-
...edge,
|
|
1320
|
-
sourceCluster,
|
|
1321
|
-
targetCluster
|
|
1322
|
-
})
|
|
1323
|
-
}
|
|
1324
|
-
|
|
1325
|
-
return {
|
|
1326
|
-
clusters: projectedClusters,
|
|
1327
|
-
edges: projectedEdges
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
const ecosystemSiblingEdgesForClusters = (clusters, existingEdges) => {
|
|
1332
|
-
const byParent = new Map()
|
|
1333
|
-
for (let index = 0; index < clusters.length; index += 1) {
|
|
1334
|
-
const cluster = clusters[index]
|
|
1335
|
-
if (cluster.isHub || !cluster.parentId) {
|
|
1336
|
-
continue
|
|
1337
|
-
}
|
|
1338
|
-
const siblings = byParent.get(cluster.parentId)
|
|
1339
|
-
if (siblings) {
|
|
1340
|
-
siblings.push(cluster)
|
|
1341
|
-
} else {
|
|
1342
|
-
byParent.set(cluster.parentId, [cluster])
|
|
1343
|
-
}
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
const edges = []
|
|
1347
|
-
for (const siblings of byParent.values()) {
|
|
1348
|
-
const ordered = [...siblings]
|
|
1349
|
-
.sort((left, right) => Math.atan2(left.y - (left.parentY ?? 0), left.x - (left.parentX ?? 0)) - Math.atan2(right.y - (right.parentY ?? 0), right.x - (right.parentX ?? 0)))
|
|
1350
|
-
for (let index = 0; index < ordered.length && edges.length < ecosystemSiblingEdgeLimit; index += 1) {
|
|
1351
|
-
const sourceCluster = ordered[index]
|
|
1352
|
-
const targetCluster = ordered[(index + 1) % ordered.length]
|
|
1353
|
-
if (!targetCluster || sourceCluster.id === targetCluster.id) {
|
|
1354
|
-
continue
|
|
1355
|
-
}
|
|
1356
|
-
const orderedIds = sourceCluster.id < targetCluster.id
|
|
1357
|
-
? [sourceCluster.id, targetCluster.id]
|
|
1358
|
-
: [targetCluster.id, sourceCluster.id]
|
|
1359
|
-
const key = orderedIds.join(':')
|
|
1360
|
-
if (existingEdges.has(key)) {
|
|
1361
|
-
continue
|
|
1362
|
-
}
|
|
1363
|
-
const edge = {
|
|
1364
|
-
id: key,
|
|
1365
|
-
sourceCluster,
|
|
1366
|
-
targetCluster,
|
|
1367
|
-
weight: 0.7,
|
|
1368
|
-
inferred: true
|
|
1369
|
-
}
|
|
1370
|
-
existingEdges.set(key, edge)
|
|
1371
|
-
edges.push(edge)
|
|
1372
|
-
}
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
return edges
|
|
1376
|
-
}
|
|
1377
|
-
|
|
1378
|
-
const ecosystemEdgesForClusters = clusters => {
|
|
1379
|
-
const edgeClusters = clusters.filter(cluster => cluster.isHub || clusterOpacity(cluster) > 0.018)
|
|
1380
|
-
const clusterById = new Map(edgeClusters.map(cluster => [cluster.id, cluster]))
|
|
1381
|
-
const clusterIds = new Set(clusterById.keys())
|
|
1382
|
-
const levelsBySize = []
|
|
1383
|
-
const seenSizes = new Set()
|
|
1384
|
-
for (let index = 0; index < edgeClusters.length; index += 1) {
|
|
1385
|
-
const cluster = edgeClusters[index]
|
|
1386
|
-
if (!cluster.size || cluster.isHub) continue
|
|
1387
|
-
if (seenSizes.has(cluster.size)) continue
|
|
1388
|
-
seenSizes.add(cluster.size)
|
|
1389
|
-
levelsBySize.push({
|
|
1390
|
-
size: cluster.size,
|
|
1391
|
-
lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
|
|
1392
|
-
})
|
|
1393
|
-
}
|
|
1394
|
-
levelsBySize.sort((left, right) => left.size - right.size)
|
|
1395
|
-
const resolvedNodeClusterById = new Map()
|
|
1396
|
-
const resolveClusterForNode = nodeId => {
|
|
1397
|
-
if (resolvedNodeClusterById.has(nodeId)) {
|
|
1398
|
-
return resolvedNodeClusterById.get(nodeId)
|
|
1399
|
-
}
|
|
1400
|
-
if (state.ecosystemHubNodeIds.has(nodeId) && state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)) {
|
|
1401
|
-
resolvedNodeClusterById.set(nodeId, state.ecosystemHubCluster)
|
|
1402
|
-
return state.ecosystemHubCluster
|
|
1403
|
-
}
|
|
1404
|
-
for (let index = 0; index < levelsBySize.length; index += 1) {
|
|
1405
|
-
const lookup = levelsBySize[index].lookup
|
|
1406
|
-
const cluster = lookup.get(nodeId)
|
|
1407
|
-
if (cluster && clusterIds.has(cluster.id)) {
|
|
1408
|
-
const resolvedCluster = clusterById.get(cluster.id) ?? cluster
|
|
1409
|
-
resolvedNodeClusterById.set(nodeId, resolvedCluster)
|
|
1410
|
-
return resolvedCluster
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
resolvedNodeClusterById.set(nodeId, null)
|
|
1414
|
-
return null
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
|
-
const edgeByClusterPair = new Map()
|
|
1418
|
-
for (let index = 0; index < state.visibleEdges.length; index += 1) {
|
|
1419
|
-
const edge = state.visibleEdges[index]
|
|
1420
|
-
const sourceCluster = resolveClusterForNode(edge.source)
|
|
1421
|
-
const targetCluster = resolveClusterForNode(edge.target)
|
|
1422
|
-
if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
|
|
1423
|
-
continue
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
const orderedIds = sourceCluster.id < targetCluster.id
|
|
1427
|
-
? [sourceCluster.id, targetCluster.id]
|
|
1428
|
-
: [targetCluster.id, sourceCluster.id]
|
|
1429
|
-
const key = orderedIds.join(':')
|
|
1430
|
-
const current = edgeByClusterPair.get(key)
|
|
1431
|
-
if (current) {
|
|
1432
|
-
current.weight += edgeWeight(edge)
|
|
1433
|
-
continue
|
|
1434
|
-
}
|
|
1435
|
-
|
|
1436
|
-
edgeByClusterPair.set(key, {
|
|
1437
|
-
id: key,
|
|
1438
|
-
sourceCluster,
|
|
1439
|
-
targetCluster,
|
|
1440
|
-
weight: edgeWeight(edge)
|
|
1441
|
-
})
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
ecosystemSiblingEdgesForClusters(edgeClusters, edgeByClusterPair)
|
|
1445
|
-
const edges = Array.from(edgeByClusterPair.values())
|
|
1446
|
-
.sort((left, right) => right.weight - left.weight)
|
|
1447
|
-
.slice(0, ecosystemClusterEdgeLimit)
|
|
1448
|
-
const hubCluster = state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)
|
|
1449
|
-
? state.ecosystemHubCluster
|
|
1450
|
-
: null
|
|
1451
|
-
if (!hubCluster) {
|
|
1452
|
-
return edges
|
|
1453
|
-
}
|
|
1454
|
-
|
|
1455
|
-
const existingHubTargets = new Set(edges.flatMap(edge =>
|
|
1456
|
-
edge.sourceCluster.id === hubCluster.id
|
|
1457
|
-
? [edge.targetCluster.id]
|
|
1458
|
-
: edge.targetCluster.id === hubCluster.id
|
|
1459
|
-
? [edge.sourceCluster.id]
|
|
1460
|
-
: []
|
|
1461
|
-
))
|
|
1462
|
-
const syntheticHubEdges = edgeClusters
|
|
1463
|
-
.filter(cluster => cluster.id !== hubCluster.id && !existingHubTargets.has(cluster.id))
|
|
1464
|
-
.slice(0, ecosystemHubEdgeLimit)
|
|
1465
|
-
.map(cluster => ({
|
|
1466
|
-
id: hubCluster.id + ':' + cluster.id,
|
|
1467
|
-
sourceCluster: hubCluster,
|
|
1468
|
-
targetCluster: cluster,
|
|
1469
|
-
weight: 1,
|
|
1470
|
-
inferred: true
|
|
1471
|
-
}))
|
|
1472
|
-
return edges.concat(syntheticHubEdges)
|
|
1473
|
-
}
|
|
1474
|
-
|
|
1475
744
|
const edgeBudgetForCurrentFrame = () => {
|
|
1476
745
|
const zoom = state.transform.scale
|
|
1477
746
|
if (zoom < 0.12) return 380
|
|
@@ -1620,90 +889,7 @@ const cursorWorldPoint = () => {
|
|
|
1620
889
|
return screenToWorldPoint(screenX, screenY)
|
|
1621
890
|
}
|
|
1622
891
|
|
|
1623
|
-
const
|
|
1624
|
-
if (scale <= 0.08) return bloomZoomFocusFarStrength
|
|
1625
|
-
if (scale <= 0.2) return 0.58
|
|
1626
|
-
if (scale <= 0.45) return 0.5
|
|
1627
|
-
if (scale <= 0.9) return 0.42
|
|
1628
|
-
return bloomZoomFocusNearStrength
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
const bloomCameraParallaxStrength = (scale) => {
|
|
1632
|
-
if (scale <= 0.08) return bloomCameraParallaxNearStrength
|
|
1633
|
-
if (scale <= 0.2) return 0.16
|
|
1634
|
-
if (scale <= 0.45) return 0.12
|
|
1635
|
-
return bloomCameraParallaxFarStrength
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
const screenPointForWorld = (worldX, worldY) => ({
|
|
1639
|
-
x: worldX * state.transform.scale + state.transform.x,
|
|
1640
|
-
y: worldY * state.transform.scale + state.transform.y
|
|
1641
|
-
})
|
|
1642
|
-
|
|
1643
|
-
const bloomZoomFocusCandidate = (screenX, screenY) => {
|
|
1644
|
-
const candidates = [state.hovered, state.selected, state.primaryHub].filter(Boolean)
|
|
1645
|
-
if (candidates.length === 0) {
|
|
1646
|
-
return null
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
let bestNode = null
|
|
1650
|
-
let bestDistance = Number.POSITIVE_INFINITY
|
|
1651
|
-
for (let index = 0; index < candidates.length; index += 1) {
|
|
1652
|
-
const node = candidates[index]
|
|
1653
|
-
const point = screenPointForWorld(nodeRenderX(node), nodeRenderY(node))
|
|
1654
|
-
const distance = Math.hypot(point.x - screenX, point.y - screenY)
|
|
1655
|
-
if (distance < bestDistance) {
|
|
1656
|
-
bestDistance = distance
|
|
1657
|
-
bestNode = node
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
|
|
1661
|
-
if (!bestNode) {
|
|
1662
|
-
return null
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
const scale = state.transform.scale
|
|
1666
|
-
const allowDistance = scale <= 0.16 ? bloomZoomFocusMaxScreenDistance * 1.7 : bloomZoomFocusMaxScreenDistance
|
|
1667
|
-
if (bestDistance > allowDistance) {
|
|
1668
|
-
return null
|
|
1669
|
-
}
|
|
1670
|
-
|
|
1671
|
-
return {
|
|
1672
|
-
x: nodeRenderX(bestNode),
|
|
1673
|
-
y: nodeRenderY(bestNode)
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
|
|
1677
|
-
const resolveZoomAnchorWorldPoint = (screenX, screenY, source) => {
|
|
1678
|
-
const cursorPoint = screenToWorldPoint(screenX, screenY)
|
|
1679
|
-
if (source !== 'wheel') {
|
|
1680
|
-
return cursorPoint
|
|
1681
|
-
}
|
|
1682
|
-
|
|
1683
|
-
const focusCandidate = bloomZoomFocusCandidate(screenX, screenY)
|
|
1684
|
-
if (!focusCandidate) {
|
|
1685
|
-
return cursorPoint
|
|
1686
|
-
}
|
|
1687
|
-
|
|
1688
|
-
const strength = bloomZoomFocusStrength(state.transform.scale)
|
|
1689
|
-
return {
|
|
1690
|
-
x: cursorPoint.x + (focusCandidate.x - cursorPoint.x) * strength,
|
|
1691
|
-
y: cursorPoint.y + (focusCandidate.y - cursorPoint.y) * strength
|
|
1692
|
-
}
|
|
1693
|
-
}
|
|
1694
|
-
|
|
1695
|
-
const applyBloomCameraParallax = (anchor) => {
|
|
1696
|
-
const cursorPoint = cursorWorldPoint()
|
|
1697
|
-
if (!cursorPoint) {
|
|
1698
|
-
return anchor
|
|
1699
|
-
}
|
|
1700
|
-
|
|
1701
|
-
const strength = bloomCameraParallaxStrength(state.transform.scale)
|
|
1702
|
-
return {
|
|
1703
|
-
x: anchor.x + (cursorPoint.x - anchor.x) * strength,
|
|
1704
|
-
y: anchor.y + (cursorPoint.y - anchor.y) * strength
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
892
|
+
const resolveZoomAnchorWorldPoint = (screenX, screenY) => screenToWorldPoint(screenX, screenY)
|
|
1707
893
|
|
|
1708
894
|
const visibilityScaleBucket = (scale) => {
|
|
1709
895
|
const safeScale = Math.max(zoomRange.min, scale)
|
|
@@ -1711,16 +897,8 @@ const visibilityScaleBucket = (scale) => {
|
|
|
1711
897
|
}
|
|
1712
898
|
|
|
1713
899
|
const shouldRenderEcosystemClusterView = (nodeCount, scale) => {
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
: ecosystemClusterScaleThreshold
|
|
1717
|
-
const enterThreshold = baseThreshold * ecosystemClusterEnterHysteresis
|
|
1718
|
-
const exitThreshold = baseThreshold * ecosystemClusterExitHysteresis
|
|
1719
|
-
const shouldRender = state.ecosystemViewActive
|
|
1720
|
-
? scale <= exitThreshold
|
|
1721
|
-
: scale <= enterThreshold
|
|
1722
|
-
state.ecosystemViewActive = shouldRender
|
|
1723
|
-
return shouldRender
|
|
900
|
+
state.ecosystemViewActive = false
|
|
901
|
+
return false
|
|
1724
902
|
}
|
|
1725
903
|
|
|
1726
904
|
const shouldRenderMacroGalaxyView = () => {
|
|
@@ -1917,20 +1095,11 @@ const edgeOpacityForScale = (edge, scale) => {
|
|
|
1917
1095
|
}
|
|
1918
1096
|
|
|
1919
1097
|
const edgeDepthOpacity = edge => {
|
|
1920
|
-
|
|
1921
|
-
return 1
|
|
1922
|
-
}
|
|
1923
|
-
return Math.max(
|
|
1924
|
-
graphDepthEdgeOpacityFloor,
|
|
1925
|
-
Math.min(nodeRenderOpacity(edge.sourceNode), nodeRenderOpacity(edge.targetNode))
|
|
1926
|
-
)
|
|
1098
|
+
return 1
|
|
1927
1099
|
}
|
|
1928
1100
|
|
|
1929
1101
|
const edgeDepthScale = edge => {
|
|
1930
|
-
|
|
1931
|
-
return 1
|
|
1932
|
-
}
|
|
1933
|
-
return Math.max(0.62, Math.min(1.18, Math.min(nodeRenderScale(edge.sourceNode), nodeRenderScale(edge.targetNode))))
|
|
1102
|
+
return 1
|
|
1934
1103
|
}
|
|
1935
1104
|
|
|
1936
1105
|
const edgeStrokeFor = (edge, selectedEdge) => {
|
|
@@ -2113,10 +1282,6 @@ const drawGraphNodes = () => {
|
|
|
2113
1282
|
}
|
|
2114
1283
|
}
|
|
2115
1284
|
|
|
2116
|
-
if (shouldProjectRenderNodesInDepth()) {
|
|
2117
|
-
regularNodes.sort((left, right) => nodeRenderDepth(right) - nodeRenderDepth(left))
|
|
2118
|
-
}
|
|
2119
|
-
|
|
2120
1285
|
drawNodeBatch(regularNodes)
|
|
2121
1286
|
|
|
2122
1287
|
if (state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) {
|
|
@@ -2193,7 +1358,7 @@ const drawGraphLabels = nodes => {
|
|
|
2193
1358
|
}
|
|
2194
1359
|
|
|
2195
1360
|
const drawAcceleratedGraph = (width, height, drawEdges) => {
|
|
2196
|
-
if (!webGlRenderer || state.renderClusters.length > 0
|
|
1361
|
+
if (!webGlRenderer || state.renderClusters.length > 0) {
|
|
2197
1362
|
return false
|
|
2198
1363
|
}
|
|
2199
1364
|
|
|
@@ -2577,12 +1742,7 @@ const zoomCapByHubDistance = (distance) => {
|
|
|
2577
1742
|
|
|
2578
1743
|
const currentZoomMax = () => {
|
|
2579
1744
|
const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
|
|
2580
|
-
|
|
2581
|
-
? zoomCapByHubDistance(state.hubNeighborDistance)
|
|
2582
|
-
: zoomRange.max
|
|
2583
|
-
const minimumUsefulCap = nodeCount > massiveGraphNodeThreshold ? 1.9 : nodeCount > largeGraphNodeThreshold ? 1.35 : 0.8
|
|
2584
|
-
const capped = Math.min(zoomCapByNodeCount(nodeCount), Math.max(minimumUsefulCap, hubDistanceCap))
|
|
2585
|
-
return Math.max(zoomRange.min * 2, capped)
|
|
1745
|
+
return Math.max(zoomRange.min * 2, zoomCapByNodeCount(nodeCount))
|
|
2586
1746
|
}
|
|
2587
1747
|
|
|
2588
1748
|
const zoomFloorByNodeCount = (nodeCount) => {
|
|
@@ -2815,7 +1975,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
|
|
|
2815
1975
|
markRenderDirty()
|
|
2816
1976
|
}
|
|
2817
1977
|
|
|
2818
|
-
const resetView = () => fitView({ useFiltered: false, macro:
|
|
1978
|
+
const resetView = () => fitView({ useFiltered: false, macro: false, preferHubCenter: false })
|
|
2819
1979
|
|
|
2820
1980
|
const focusPrimaryHub = () => {
|
|
2821
1981
|
const hub = state.primaryHub
|
|
@@ -3238,103 +2398,13 @@ const clusterRadiusPx = cluster => {
|
|
|
3238
2398
|
const clusterOpacity = cluster =>
|
|
3239
2399
|
Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
|
|
3240
2400
|
|
|
3241
|
-
const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
|
|
3242
|
-
const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
|
|
3243
|
-
|
|
3244
|
-
const shouldProjectRenderNodesInDepth = () => {
|
|
3245
|
-
const withinNodeCountWindow =
|
|
3246
|
-
state.renderClusters.length === 0 &&
|
|
3247
|
-
state.renderNodes.length >= graphDepthProjectionNodeThreshold &&
|
|
3248
|
-
state.renderNodes.length <= graphDepthProjectionNodeCap &&
|
|
3249
|
-
!state.pointer.down
|
|
3250
|
-
|
|
3251
|
-
if (!withinNodeCountWindow) {
|
|
3252
|
-
state.depthProjectionActive = false
|
|
3253
|
-
return false
|
|
3254
|
-
}
|
|
3255
|
-
|
|
3256
|
-
const scale = state.transform.scale
|
|
3257
|
-
const shouldProject = state.depthProjectionActive
|
|
3258
|
-
? scale >= graphDepthProjectionExitMinScale && scale <= graphDepthProjectionExitMaxScale
|
|
3259
|
-
: scale >= graphDepthProjectionEnterMinScale && scale <= graphDepthProjectionEnterMaxScale
|
|
3260
|
-
state.depthProjectionActive = shouldProject
|
|
3261
|
-
return shouldProject
|
|
3262
|
-
}
|
|
3263
|
-
|
|
3264
|
-
const nodeProjectionAnchor = () => {
|
|
3265
|
-
const hub = state.primaryHub
|
|
3266
|
-
if (hub) {
|
|
3267
|
-
return { x: hub.x, y: hub.y }
|
|
3268
|
-
}
|
|
3269
|
-
if (state.macroRepresentative) {
|
|
3270
|
-
return { x: state.macroRepresentative.x, y: state.macroRepresentative.y }
|
|
3271
|
-
}
|
|
3272
|
-
return ecosystemFocusPoint()
|
|
3273
|
-
}
|
|
3274
|
-
|
|
3275
|
-
const projectGraphNodePoint = (x, y, depth, anchor) => {
|
|
3276
|
-
const safeDepth = Math.max(0, depth)
|
|
3277
|
-
const dx = x - anchor.x
|
|
3278
|
-
const dy = y - anchor.y
|
|
3279
|
-
const yawSin = Math.sin(graphDepthYaw)
|
|
3280
|
-
const yawCos = Math.cos(graphDepthYaw)
|
|
3281
|
-
const pitchSin = Math.sin(graphDepthPitch)
|
|
3282
|
-
const pitchCos = Math.cos(graphDepthPitch)
|
|
3283
|
-
const rotatedX = dx * yawCos + safeDepth * yawSin
|
|
3284
|
-
const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
|
|
3285
|
-
const rotatedY = dy * pitchCos - rotatedZ * pitchSin
|
|
3286
|
-
const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
|
|
3287
|
-
const factor = graphDepthPerspective / (graphDepthPerspective + projectedDepth)
|
|
3288
|
-
return {
|
|
3289
|
-
x: anchor.x + rotatedX * factor,
|
|
3290
|
-
y: anchor.y + rotatedY * factor,
|
|
3291
|
-
depth: projectedDepth,
|
|
3292
|
-
scale: graphDepthMinScale + (1 - graphDepthMinScale) * factor
|
|
3293
|
-
}
|
|
3294
|
-
}
|
|
3295
|
-
|
|
3296
2401
|
const refreshRenderNodeDepthProjection = () => {
|
|
3297
|
-
|
|
3298
|
-
state.renderNodeDepthProjectionById = new Map()
|
|
3299
|
-
return
|
|
3300
|
-
}
|
|
3301
|
-
|
|
3302
|
-
const anchor = applyBloomCameraParallax(nodeProjectionAnchor())
|
|
3303
|
-
let maxDistance = 1
|
|
3304
|
-
for (let index = 0; index < state.renderNodes.length; index += 1) {
|
|
3305
|
-
const node = state.renderNodes[index]
|
|
3306
|
-
const distance = Math.hypot(node.x - anchor.x, node.y - anchor.y)
|
|
3307
|
-
if (distance > maxDistance) {
|
|
3308
|
-
maxDistance = distance
|
|
3309
|
-
}
|
|
3310
|
-
}
|
|
3311
|
-
|
|
3312
|
-
const projectionById = new Map()
|
|
3313
|
-
for (let index = 0; index < state.renderNodes.length; index += 1) {
|
|
3314
|
-
const node = state.renderNodes[index]
|
|
3315
|
-
const radialDistance = Math.hypot(node.x - anchor.x, node.y - anchor.y)
|
|
3316
|
-
const radialRatio = radialDistance / maxDistance
|
|
3317
|
-
const degree = state.nodeDegrees.get(node.id) ?? 0
|
|
3318
|
-
const degreeLift = Math.min(60, degree * 2.2)
|
|
3319
|
-
const radialDepth = radialRatio * graphDepthFar * graphDepthRadialGain
|
|
3320
|
-
const depth = Math.max(0, graphDepthNear + radialDepth - degreeLift)
|
|
3321
|
-
const projected = projectGraphNodePoint(node.x, node.y, depth, anchor)
|
|
3322
|
-
const opacity = Math.max(graphDepthOpacityFloor, Math.min(1, projected.scale * 1.08))
|
|
3323
|
-
projectionById.set(node.id, {
|
|
3324
|
-
x: projected.x,
|
|
3325
|
-
y: projected.y,
|
|
3326
|
-
depth: projected.depth,
|
|
3327
|
-
scale: projected.scale,
|
|
3328
|
-
opacity
|
|
3329
|
-
})
|
|
3330
|
-
}
|
|
3331
|
-
state.renderNodeDepthProjectionById = projectionById
|
|
2402
|
+
state.renderNodeDepthProjectionById = new Map()
|
|
3332
2403
|
}
|
|
3333
2404
|
|
|
3334
2405
|
const projectedNode = node => state.renderNodeDepthProjectionById.get(node.id) ?? null
|
|
3335
2406
|
const nodeRenderX = node => projectedNode(node)?.x ?? node.x
|
|
3336
2407
|
const nodeRenderY = node => projectedNode(node)?.y ?? node.y
|
|
3337
|
-
const nodeRenderDepth = node => projectedNode(node)?.depth ?? graphDepthNear
|
|
3338
2408
|
const nodeRenderScale = node => projectedNode(node)?.scale ?? 1
|
|
3339
2409
|
const nodeRenderOpacity = node => projectedNode(node)?.opacity ?? 1
|
|
3340
2410
|
const worldViewportBounds = () => {
|
|
@@ -3497,22 +2567,6 @@ const computeRenderVisibility = () => {
|
|
|
3497
2567
|
return
|
|
3498
2568
|
}
|
|
3499
2569
|
|
|
3500
|
-
if (
|
|
3501
|
-
state.ecosystemExpansionLevels.length > 0 &&
|
|
3502
|
-
shouldRenderEcosystemClusterView(state.visibleNodes.length, state.transform.scale) &&
|
|
3503
|
-
state.ecosystemClusters.length > 0
|
|
3504
|
-
) {
|
|
3505
|
-
const clusters = selectHierarchicalEcosystemClusters(viewport)
|
|
3506
|
-
.sort((left, right) => right.count - left.count)
|
|
3507
|
-
const edges = ecosystemEdgesForClusters(clusters)
|
|
3508
|
-
const projectionAnchor = ecosystemFocusPoint()
|
|
3509
|
-
const projected = applyEcosystemDepthProjection(clusters, edges, projectionAnchor)
|
|
3510
|
-
state.renderClusters = projected.clusters
|
|
3511
|
-
state.renderClusterEdges = projected.edges
|
|
3512
|
-
state.renderNodes = []
|
|
3513
|
-
state.renderEdges = []
|
|
3514
|
-
return
|
|
3515
|
-
}
|
|
3516
2570
|
state.ecosystemViewActive = false
|
|
3517
2571
|
|
|
3518
2572
|
if (state.visibleNodes.length <= 2000) {
|
|
@@ -3758,7 +2812,6 @@ const render = now => {
|
|
|
3758
2812
|
ctx.translate(state.transform.x, state.transform.y)
|
|
3759
2813
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
3760
2814
|
const orderedClusters = [...state.renderClusters]
|
|
3761
|
-
.sort((left, right) => clusterDepth(right) - clusterDepth(left))
|
|
3762
2815
|
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
3763
2816
|
if (state.renderClusterEdges.length > 0) {
|
|
3764
2817
|
for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
|
|
@@ -3767,8 +2820,7 @@ const render = now => {
|
|
|
3767
2820
|
if (edgeOpacity <= 0.01) {
|
|
3768
2821
|
continue
|
|
3769
2822
|
}
|
|
3770
|
-
const
|
|
3771
|
-
const widthScale = 0.6 + depthScale * 0.9
|
|
2823
|
+
const widthScale = 1
|
|
3772
2824
|
ctx.beginPath()
|
|
3773
2825
|
ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
|
|
3774
2826
|
ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
|
|
@@ -3953,7 +3005,7 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
|
3953
3005
|
state.zoomTransition.screenX === screenX &&
|
|
3954
3006
|
state.zoomTransition.screenY === screenY
|
|
3955
3007
|
? { x: state.zoomTransition.worldX, y: state.zoomTransition.worldY }
|
|
3956
|
-
: resolveZoomAnchorWorldPoint(screenX, screenY
|
|
3008
|
+
: resolveZoomAnchorWorldPoint(screenX, screenY)
|
|
3957
3009
|
const worldX = worldPointAtCursor.x
|
|
3958
3010
|
const worldY = worldPointAtCursor.y
|
|
3959
3011
|
state.lastZoomFocus = {
|
|
@@ -3983,18 +3035,13 @@ const wheelZoomFactor = event => {
|
|
|
3983
3035
|
return 1
|
|
3984
3036
|
}
|
|
3985
3037
|
|
|
3986
|
-
const isMassiveEcosystemZoom =
|
|
3987
|
-
state.visibleNodes.length > massiveGraphNodeThreshold &&
|
|
3988
|
-
state.transform.scale <= massiveEcosystemClusterScaleThreshold
|
|
3989
|
-
const sensitivityMultiplier = isMassiveEcosystemZoom ? 0.48 : 1
|
|
3990
|
-
const capMultiplier = isMassiveEcosystemZoom ? 0.34 : 1
|
|
3991
3038
|
const isZoomOut = normalizedDelta > 0
|
|
3992
3039
|
const currentScale = state.transform.scale
|
|
3993
3040
|
const zoomOutDamping = isZoomOut
|
|
3994
3041
|
? (currentScale <= 0.03 ? 0.38 : currentScale <= 0.08 ? 0.52 : 0.68)
|
|
3995
3042
|
: 1
|
|
3996
|
-
const sensitivity = wheelZoomExponent * (isModifierZoom ? wheelZoomModifierBoost : 1) *
|
|
3997
|
-
const exponentCap = wheelZoomExponentCap *
|
|
3043
|
+
const sensitivity = wheelZoomExponent * (isModifierZoom ? wheelZoomModifierBoost : 1) * zoomOutDamping
|
|
3044
|
+
const exponentCap = wheelZoomExponentCap * (isZoomOut ? 0.74 : 1)
|
|
3998
3045
|
const exponent = Math.max(
|
|
3999
3046
|
-exponentCap,
|
|
4000
3047
|
Math.min(exponentCap, -normalizedDelta * sensitivity)
|
package/package.json
CHANGED