@andespindola/brainlink 0.1.0-beta.87 → 0.1.0-beta.89
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 +2 -2
- package/dist/application/frontend/client-js.js +198 -70
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,7 +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 hierarchical ecosystem subgraphs only above 1000 notes:
|
|
87
|
+
- Graph zoom-out renders hierarchical ecosystem subgraphs only above 1000 notes: the memory hub stays centered, 1000-note groups stay as compact sand-like points, and focused groups gradually expand into smaller graph meshes before individual notes are rendered.
|
|
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 macro mass and zoom-in progressively expands toward local detail.
|
|
@@ -602,7 +602,7 @@ The graph UI shows:
|
|
|
602
602
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
603
603
|
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
604
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: graphs up to 1000 notes render directly; larger graphs use
|
|
605
|
+
- graph LOD progression: graphs up to 1000 notes render directly; larger graphs use a compact memory-hub-centered mesh of connected 1000-note points, zoom-in spreads only focused clusters into 250-note and 60-note subgraphs with aggregated real links, keeps parent points during expansion to avoid visual jumps, then progressively raises the focused node budget so local areas keep nearby notes and links visible
|
|
606
606
|
|
|
607
607
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
608
608
|
|
|
@@ -25,6 +25,7 @@ const ecosystemGroupSize = 1000
|
|
|
25
25
|
const ecosystemActivationNodeThreshold = 1000
|
|
26
26
|
const ecosystemGroupSizes = [1000, 250, 60]
|
|
27
27
|
const ecosystemClusterEdgeLimit = 520
|
|
28
|
+
const ecosystemHubEdgeLimit = 120
|
|
28
29
|
const ecosystemClusterScaleThreshold = 0.32
|
|
29
30
|
const ecosystemSubgraphScaleThreshold = 0.18
|
|
30
31
|
const ecosystemMicroScaleThreshold = 0.08
|
|
@@ -37,9 +38,9 @@ const meshEdgeMaxBudget = 1400
|
|
|
37
38
|
const layeredCoreScaleThreshold = 0.55
|
|
38
39
|
const dragNeighborhoodMaxAffected = 180
|
|
39
40
|
const dragSettleRounds = 3
|
|
40
|
-
const wheelZoomExponent = 0.
|
|
41
|
-
const wheelZoomExponentCap = 0.
|
|
42
|
-
const wheelZoomModifierBoost = 1.
|
|
41
|
+
const wheelZoomExponent = 0.0009
|
|
42
|
+
const wheelZoomExponentCap = 0.035
|
|
43
|
+
const wheelZoomModifierBoost = 1.08
|
|
43
44
|
const state = {
|
|
44
45
|
graph: { nodes: [], edges: [] },
|
|
45
46
|
nodes: [],
|
|
@@ -74,6 +75,8 @@ const state = {
|
|
|
74
75
|
visibleEdgeByNode: new Map(),
|
|
75
76
|
ecosystemClusters: [],
|
|
76
77
|
ecosystemClustersBySize: new Map(),
|
|
78
|
+
ecosystemNodeClusterBySize: new Map(),
|
|
79
|
+
ecosystemHubCluster: null,
|
|
77
80
|
macroCenter: { x: 0, y: 0 },
|
|
78
81
|
macroRepresentative: null,
|
|
79
82
|
primaryHub: null,
|
|
@@ -570,11 +573,6 @@ const recomputeVisibility = () => {
|
|
|
570
573
|
state.visibleEdges = limitedEdges
|
|
571
574
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
572
575
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
573
|
-
const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
|
|
574
|
-
? buildEcosystemGraph(nodes)
|
|
575
|
-
: { clusters: [], clustersBySize: new Map() }
|
|
576
|
-
state.ecosystemClusters = ecosystemGraph.clusters
|
|
577
|
-
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
578
576
|
const primaryHub = rankedHubNodes()[0] ?? null
|
|
579
577
|
state.primaryHub = primaryHub
|
|
580
578
|
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
@@ -586,6 +584,13 @@ const recomputeVisibility = () => {
|
|
|
586
584
|
y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
|
|
587
585
|
}
|
|
588
586
|
: { x: 0, y: 0 }
|
|
587
|
+
const ecosystemGraph = nodes.length > ecosystemActivationNodeThreshold
|
|
588
|
+
? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
|
|
589
|
+
: { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
|
|
590
|
+
state.ecosystemClusters = ecosystemGraph.clusters
|
|
591
|
+
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
592
|
+
state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
|
|
593
|
+
state.ecosystemHubCluster = ecosystemGraph.hubCluster
|
|
589
594
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
590
595
|
markRenderDirty()
|
|
591
596
|
}
|
|
@@ -730,18 +735,32 @@ const selectEcosystemRepresentative = nodes => {
|
|
|
730
735
|
return representative
|
|
731
736
|
}
|
|
732
737
|
|
|
733
|
-
const
|
|
738
|
+
const ecosystemLayoutSpacingForSize = size => {
|
|
739
|
+
if (size >= 1000) return 360
|
|
740
|
+
if (size >= 250) return 92
|
|
741
|
+
return 28
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const ecosystemCompactPoint = (index, total, center, spacing) => {
|
|
745
|
+
if (total <= 1) {
|
|
746
|
+
return { x: center.x, y: center.y }
|
|
747
|
+
}
|
|
748
|
+
const angle = index * 2.399963229728653
|
|
749
|
+
const radius = spacing * Math.sqrt(index + 1)
|
|
750
|
+
return {
|
|
751
|
+
x: center.x + Math.cos(angle) * radius,
|
|
752
|
+
y: center.y + Math.sin(angle) * radius
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const buildEcosystemCluster = (nodes, index, point) => {
|
|
734
757
|
const count = Math.max(nodes.length, 1)
|
|
735
|
-
const sum = nodes.reduce((accumulator, node) => ({
|
|
736
|
-
x: accumulator.x + node.x,
|
|
737
|
-
y: accumulator.y + node.y
|
|
738
|
-
}), { x: 0, y: 0 })
|
|
739
758
|
const representative = selectEcosystemRepresentative(nodes)
|
|
740
759
|
|
|
741
760
|
return {
|
|
742
761
|
id: 'ecosystem-' + index,
|
|
743
|
-
x:
|
|
744
|
-
y:
|
|
762
|
+
x: point.x,
|
|
763
|
+
y: point.y,
|
|
745
764
|
count,
|
|
746
765
|
nodeIds: nodes.map(node => node.id),
|
|
747
766
|
representative,
|
|
@@ -749,15 +768,42 @@ const buildEcosystemCluster = (nodes, index) => {
|
|
|
749
768
|
}
|
|
750
769
|
}
|
|
751
770
|
|
|
752
|
-
const
|
|
771
|
+
const buildEcosystemHubCluster = (hub, center) => hub
|
|
772
|
+
? {
|
|
773
|
+
id: 'ecosystem-hub',
|
|
774
|
+
x: center.x,
|
|
775
|
+
y: center.y,
|
|
776
|
+
count: 1,
|
|
777
|
+
size: 1,
|
|
778
|
+
nodeIds: [hub.id],
|
|
779
|
+
representative: hub,
|
|
780
|
+
label: hub.title || 'Memory Hub',
|
|
781
|
+
parentId: null,
|
|
782
|
+
parentX: null,
|
|
783
|
+
parentY: null,
|
|
784
|
+
isHub: true
|
|
785
|
+
}
|
|
786
|
+
: null
|
|
787
|
+
|
|
788
|
+
const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
|
|
753
789
|
const clusters = []
|
|
754
790
|
const clusterByNodeId = new Map()
|
|
791
|
+
const parentChildIndex = new Map()
|
|
755
792
|
|
|
756
793
|
for (let offset = 0; offset < sortedNodes.length; offset += size) {
|
|
757
794
|
const clusterNodes = sortedNodes.slice(offset, offset + size)
|
|
758
795
|
const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
|
|
796
|
+
const siblingIndex = parentCluster
|
|
797
|
+
? (parentChildIndex.get(parentCluster.id) ?? 0)
|
|
798
|
+
: clusters.length
|
|
799
|
+
if (parentCluster) {
|
|
800
|
+
parentChildIndex.set(parentCluster.id, siblingIndex + 1)
|
|
801
|
+
}
|
|
802
|
+
const point = parentCluster
|
|
803
|
+
? ecosystemCompactPoint(siblingIndex, Math.ceil((parentCluster.count || size) / size), parentCluster, ecosystemLayoutSpacingForSize(size))
|
|
804
|
+
: ecosystemCompactPoint(clusters.length, Math.ceil(sortedNodes.length / size), center, ecosystemLayoutSpacingForSize(size))
|
|
759
805
|
const cluster = {
|
|
760
|
-
...buildEcosystemCluster(clusterNodes, clusters.length),
|
|
806
|
+
...buildEcosystemCluster(clusterNodes, clusters.length, point),
|
|
761
807
|
id: 'ecosystem-' + size + '-' + clusters.length,
|
|
762
808
|
size,
|
|
763
809
|
parentId: parentCluster?.id ?? null,
|
|
@@ -773,25 +819,32 @@ const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
|
|
|
773
819
|
return { clusters, clusterByNodeId }
|
|
774
820
|
}
|
|
775
821
|
|
|
776
|
-
const buildEcosystemGraph = (nodes) => {
|
|
822
|
+
const buildEcosystemGraph = (nodes, center, hub) => {
|
|
777
823
|
if (nodes.length === 0) {
|
|
778
|
-
return { clusters: [], clustersBySize: new Map() }
|
|
824
|
+
return { clusters: [], clustersBySize: new Map(), nodeClusterBySize: new Map(), hubCluster: null }
|
|
779
825
|
}
|
|
780
826
|
|
|
781
|
-
const
|
|
827
|
+
const hubCluster = buildEcosystemHubCluster(hub, center)
|
|
828
|
+
const sortedNodes = nodes
|
|
829
|
+
.filter(node => node.id !== hub?.id)
|
|
830
|
+
.sort(compareNodesForEcosystem)
|
|
782
831
|
const clustersBySize = new Map()
|
|
832
|
+
const nodeClusterBySize = new Map()
|
|
783
833
|
let parentLookup = null
|
|
784
834
|
|
|
785
835
|
for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
|
|
786
836
|
const size = ecosystemGroupSizes[index]
|
|
787
|
-
const level = buildEcosystemLevel(sortedNodes, size, parentLookup)
|
|
837
|
+
const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
|
|
788
838
|
clustersBySize.set(size, level.clusters)
|
|
839
|
+
nodeClusterBySize.set(size, level.clusterByNodeId)
|
|
789
840
|
parentLookup = level.clusterByNodeId
|
|
790
841
|
}
|
|
791
842
|
|
|
792
843
|
return {
|
|
793
844
|
clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
|
|
794
|
-
clustersBySize
|
|
845
|
+
clustersBySize,
|
|
846
|
+
nodeClusterBySize,
|
|
847
|
+
hubCluster
|
|
795
848
|
}
|
|
796
849
|
}
|
|
797
850
|
|
|
@@ -832,21 +885,24 @@ const smoothStep = value => {
|
|
|
832
885
|
const zoomProgress = (scale, start, end) =>
|
|
833
886
|
smoothStep((scale - start) / Math.max(end - start, 0.0001))
|
|
834
887
|
|
|
835
|
-
const
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
888
|
+
const shouldReplaceParentCluster = spread => spread >= 0.96
|
|
889
|
+
|
|
890
|
+
const expandFocusedClusters = (parentClusters, childSize, spread, viewport) => {
|
|
891
|
+
const focusPoint = ecosystemFocusPoint()
|
|
892
|
+
const expandedParentIds = new Set(nearestEcosystemParentIds(
|
|
893
|
+
parentClusters,
|
|
894
|
+
focusPoint,
|
|
895
|
+
ecosystemFocusedParentLimit
|
|
896
|
+
))
|
|
897
|
+
const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
|
|
898
|
+
const visibleChildClusters = childClusters
|
|
899
|
+
.filter(cluster => expandedParentIds.has(cluster.parentId))
|
|
900
|
+
.map(cluster => spreadChildClusterFromParent(cluster, spread))
|
|
901
|
+
.filter(cluster => isClusterInViewport(cluster, viewport))
|
|
902
|
+
|
|
846
903
|
return {
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
spread: zoomProgress(scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold)
|
|
904
|
+
expandedParentIds,
|
|
905
|
+
childClusters: visibleChildClusters
|
|
850
906
|
}
|
|
851
907
|
}
|
|
852
908
|
|
|
@@ -863,50 +919,93 @@ const spreadChildClusterFromParent = (cluster, spread) => {
|
|
|
863
919
|
}
|
|
864
920
|
|
|
865
921
|
const selectHierarchicalEcosystemClusters = viewport => {
|
|
866
|
-
const
|
|
867
|
-
const baseClusters = state.ecosystemClustersBySize.get(plan.baseSize) ?? state.ecosystemClusters
|
|
922
|
+
const baseClusters = state.ecosystemClustersBySize.get(ecosystemGroupSize) ?? state.ecosystemClusters
|
|
868
923
|
const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
|
|
924
|
+
const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
|
|
869
925
|
|
|
870
|
-
if (
|
|
871
|
-
return visibleBaseClusters
|
|
926
|
+
if (state.transform.scale <= ecosystemMicroScaleThreshold) {
|
|
927
|
+
return [...hubClusters, ...visibleBaseClusters]
|
|
872
928
|
}
|
|
873
929
|
|
|
874
|
-
const
|
|
875
|
-
const expandedParentIds = new Set(nearestEcosystemParentIds(
|
|
930
|
+
const midExpansion = expandFocusedClusters(
|
|
876
931
|
visibleBaseClusters,
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
932
|
+
250,
|
|
933
|
+
state.transform.scale <= ecosystemSubgraphScaleThreshold
|
|
934
|
+
? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
|
|
935
|
+
: 1,
|
|
936
|
+
viewport
|
|
937
|
+
)
|
|
938
|
+
if (midExpansion.childClusters.length === 0) {
|
|
939
|
+
return [...hubClusters, ...visibleBaseClusters]
|
|
940
|
+
}
|
|
885
941
|
|
|
886
|
-
|
|
887
|
-
|
|
942
|
+
const midSpread = state.transform.scale <= ecosystemSubgraphScaleThreshold
|
|
943
|
+
? zoomProgress(state.transform.scale, ecosystemMicroScaleThreshold, ecosystemSubgraphScaleThreshold)
|
|
944
|
+
: 1
|
|
945
|
+
const remainingBaseClusters = shouldReplaceParentCluster(midSpread)
|
|
946
|
+
? visibleBaseClusters.filter(cluster => !midExpansion.expandedParentIds.has(cluster.id))
|
|
947
|
+
: visibleBaseClusters
|
|
948
|
+
if (state.transform.scale <= ecosystemSubgraphScaleThreshold) {
|
|
949
|
+
return [
|
|
950
|
+
...hubClusters,
|
|
951
|
+
...remainingBaseClusters,
|
|
952
|
+
...midExpansion.childClusters
|
|
953
|
+
]
|
|
888
954
|
}
|
|
889
955
|
|
|
956
|
+
const microExpansion = expandFocusedClusters(
|
|
957
|
+
midExpansion.childClusters,
|
|
958
|
+
60,
|
|
959
|
+
zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold),
|
|
960
|
+
viewport
|
|
961
|
+
)
|
|
962
|
+
if (microExpansion.childClusters.length === 0) {
|
|
963
|
+
return [
|
|
964
|
+
...hubClusters,
|
|
965
|
+
...remainingBaseClusters,
|
|
966
|
+
...midExpansion.childClusters
|
|
967
|
+
]
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
const microSpread = zoomProgress(state.transform.scale, ecosystemSubgraphScaleThreshold, ecosystemClusterScaleThreshold)
|
|
971
|
+
const visibleMidClusters = shouldReplaceParentCluster(microSpread)
|
|
972
|
+
? midExpansion.childClusters.filter(cluster => !microExpansion.expandedParentIds.has(cluster.id))
|
|
973
|
+
: midExpansion.childClusters
|
|
890
974
|
return [
|
|
891
|
-
...
|
|
892
|
-
...
|
|
975
|
+
...hubClusters,
|
|
976
|
+
...remainingBaseClusters,
|
|
977
|
+
...visibleMidClusters,
|
|
978
|
+
...microExpansion.childClusters
|
|
893
979
|
]
|
|
894
980
|
}
|
|
895
981
|
|
|
896
982
|
const ecosystemEdgesForClusters = clusters => {
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
983
|
+
const clusterById = new Map(clusters.map(cluster => [cluster.id, cluster]))
|
|
984
|
+
const clusterIds = new Set(clusterById.keys())
|
|
985
|
+
const levelBySize = new Map()
|
|
986
|
+
for (let index = 0; index < clusters.length; index += 1) {
|
|
987
|
+
const cluster = clusters[index]
|
|
988
|
+
if (!cluster.size || cluster.isHub) continue
|
|
989
|
+
levelBySize.set(cluster.size, state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map())
|
|
990
|
+
}
|
|
991
|
+
const resolveClusterForNode = nodeId => {
|
|
992
|
+
if (state.ecosystemHubCluster?.nodeIds.includes(nodeId) && clusterIds.has(state.ecosystemHubCluster.id)) {
|
|
993
|
+
return state.ecosystemHubCluster
|
|
994
|
+
}
|
|
995
|
+
for (const [size, lookup] of levelBySize) {
|
|
996
|
+
const cluster = lookup.get(nodeId)
|
|
997
|
+
if (cluster && clusterIds.has(cluster.id)) {
|
|
998
|
+
return clusterById.get(cluster.id) ?? cluster
|
|
999
|
+
}
|
|
902
1000
|
}
|
|
1001
|
+
return null
|
|
903
1002
|
}
|
|
904
1003
|
|
|
905
1004
|
const edgeByClusterPair = new Map()
|
|
906
1005
|
for (let index = 0; index < state.visibleEdges.length; index += 1) {
|
|
907
1006
|
const edge = state.visibleEdges[index]
|
|
908
|
-
const sourceCluster =
|
|
909
|
-
const targetCluster =
|
|
1007
|
+
const sourceCluster = resolveClusterForNode(edge.source)
|
|
1008
|
+
const targetCluster = resolveClusterForNode(edge.target)
|
|
910
1009
|
if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
|
|
911
1010
|
continue
|
|
912
1011
|
}
|
|
@@ -929,9 +1028,34 @@ const ecosystemEdgesForClusters = clusters => {
|
|
|
929
1028
|
})
|
|
930
1029
|
}
|
|
931
1030
|
|
|
932
|
-
|
|
1031
|
+
const edges = Array.from(edgeByClusterPair.values())
|
|
933
1032
|
.sort((left, right) => right.weight - left.weight)
|
|
934
1033
|
.slice(0, ecosystemClusterEdgeLimit)
|
|
1034
|
+
const hubCluster = state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)
|
|
1035
|
+
? state.ecosystemHubCluster
|
|
1036
|
+
: null
|
|
1037
|
+
if (!hubCluster) {
|
|
1038
|
+
return edges
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
const existingHubTargets = new Set(edges.flatMap(edge =>
|
|
1042
|
+
edge.sourceCluster.id === hubCluster.id
|
|
1043
|
+
? [edge.targetCluster.id]
|
|
1044
|
+
: edge.targetCluster.id === hubCluster.id
|
|
1045
|
+
? [edge.sourceCluster.id]
|
|
1046
|
+
: []
|
|
1047
|
+
))
|
|
1048
|
+
const syntheticHubEdges = clusters
|
|
1049
|
+
.filter(cluster => cluster.id !== hubCluster.id && !existingHubTargets.has(cluster.id))
|
|
1050
|
+
.slice(0, ecosystemHubEdgeLimit)
|
|
1051
|
+
.map(cluster => ({
|
|
1052
|
+
id: hubCluster.id + ':' + cluster.id,
|
|
1053
|
+
sourceCluster: hubCluster,
|
|
1054
|
+
targetCluster: cluster,
|
|
1055
|
+
weight: 1,
|
|
1056
|
+
inferred: true
|
|
1057
|
+
}))
|
|
1058
|
+
return edges.concat(syntheticHubEdges)
|
|
935
1059
|
}
|
|
936
1060
|
|
|
937
1061
|
const edgeBudgetForCurrentFrame = () => {
|
|
@@ -2372,6 +2496,9 @@ const clusterRadiusPx = cluster => {
|
|
|
2372
2496
|
if (cluster.id === 'macro-galaxy') {
|
|
2373
2497
|
return 10
|
|
2374
2498
|
}
|
|
2499
|
+
if (cluster.isHub) {
|
|
2500
|
+
return 5.2
|
|
2501
|
+
}
|
|
2375
2502
|
if (String(cluster.id).startsWith('ecosystem-')) {
|
|
2376
2503
|
const base = cluster.size >= 1000 ? 2.4 : cluster.size >= 250 ? 2.1 : 1.8
|
|
2377
2504
|
return Math.max(1.8, Math.min(4.2, base + Math.log10(cluster.count + 1) * 0.28))
|
|
@@ -2807,10 +2934,11 @@ const render = now => {
|
|
|
2807
2934
|
state.renderClusters.forEach(cluster => {
|
|
2808
2935
|
const isMacro = cluster.id === 'macro-galaxy'
|
|
2809
2936
|
const isEcosystem = String(cluster.id).startsWith('ecosystem-')
|
|
2937
|
+
const isHub = Boolean(cluster.isHub)
|
|
2810
2938
|
const radiusPx = clusterRadiusPx(cluster)
|
|
2811
2939
|
const radius = radiusPx / safeScale
|
|
2812
|
-
const haloRadius = (radiusPx + (isMacro ? 8 : isEcosystem ? 1.1 : 4)) / safeScale
|
|
2813
|
-
if (!isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
|
|
2940
|
+
const haloRadius = (radiusPx + (isMacro ? 8 : isHub ? 4 : isEcosystem ? 1.1 : 4)) / safeScale
|
|
2941
|
+
if (isHub || !isEcosystem || state.transform.scale >= ecosystemSubgraphScaleThreshold) {
|
|
2814
2942
|
ctx.beginPath()
|
|
2815
2943
|
ctx.arc(cluster.x, cluster.y, haloRadius, 0, Math.PI * 2)
|
|
2816
2944
|
ctx.fillStyle = isMacro ? 'rgba(243, 247, 251, 0.28)' : graphTheme.nodeHalo
|
|
@@ -2820,7 +2948,7 @@ const render = now => {
|
|
|
2820
2948
|
ctx.arc(cluster.x, cluster.y, radius, 0, Math.PI * 2)
|
|
2821
2949
|
ctx.fillStyle = isMacro ? '#f3f7fb' : graphTheme.node
|
|
2822
2950
|
ctx.fill()
|
|
2823
|
-
ctx.lineWidth = (isEcosystem ? 0.7 : 1.4) / safeScale
|
|
2951
|
+
ctx.lineWidth = (isEcosystem && !isHub ? 0.7 : 1.4) / safeScale
|
|
2824
2952
|
ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
|
|
2825
2953
|
ctx.stroke()
|
|
2826
2954
|
if (isMacro && cluster.representative?.title) {
|
|
@@ -3035,11 +3163,11 @@ const bindEvents = () => {
|
|
|
3035
3163
|
})
|
|
3036
3164
|
elements.zoomIn.addEventListener('click', () => {
|
|
3037
3165
|
const rect = canvas.getBoundingClientRect()
|
|
3038
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.
|
|
3166
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.055, 'button')
|
|
3039
3167
|
})
|
|
3040
3168
|
elements.zoomOut.addEventListener('click', () => {
|
|
3041
3169
|
const rect = canvas.getBoundingClientRect()
|
|
3042
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.
|
|
3170
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.948, 'button')
|
|
3043
3171
|
})
|
|
3044
3172
|
if (elements.fit) {
|
|
3045
3173
|
elements.fit.addEventListener('click', () => {
|
|
@@ -3070,7 +3198,7 @@ const bindEvents = () => {
|
|
|
3070
3198
|
const rect = canvas.getBoundingClientRect()
|
|
3071
3199
|
const cursorX = event.clientX - rect.left
|
|
3072
3200
|
const cursorY = event.clientY - rect.top
|
|
3073
|
-
zoomAtPoint(cursorX, cursorY, 1.
|
|
3201
|
+
zoomAtPoint(cursorX, cursorY, 1.055)
|
|
3074
3202
|
})
|
|
3075
3203
|
canvas.addEventListener('pointerdown', event => {
|
|
3076
3204
|
const point = worldPoint(event)
|
|
@@ -3144,14 +3272,14 @@ const bindEvents = () => {
|
|
|
3144
3272
|
if (event.key === '+' || event.key === '=') {
|
|
3145
3273
|
event.preventDefault()
|
|
3146
3274
|
const rect = canvas.getBoundingClientRect()
|
|
3147
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.
|
|
3275
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.05)
|
|
3148
3276
|
return
|
|
3149
3277
|
}
|
|
3150
3278
|
|
|
3151
3279
|
if (event.key === '-' || event.key === '_') {
|
|
3152
3280
|
event.preventDefault()
|
|
3153
3281
|
const rect = canvas.getBoundingClientRect()
|
|
3154
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.
|
|
3282
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.952)
|
|
3155
3283
|
return
|
|
3156
3284
|
}
|
|
3157
3285
|
|
package/package.json
CHANGED