@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 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 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.
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 macro mass and zoom-in progressively expands toward local detail.
91
- - Zoomed-out graph LOD renders nested subgraphs and progressively expands only the focused cluster as zoom increases, including very large vaults.
92
- - Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
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 LOD jumps while keeping cursor-anchored focus
599
- - bloom-like navigation profile: wheel zoom now biases focus toward hovered/selected/hub nodes and applies subtle camera-anchor parallax so depth remains stable while zooming and panning
600
- - zoom-out floor for large and massive graphs, plus reset macro floor tied to hub-neighbor distance and first-level cluster spacing, with a tighter initial camera so the first graph appears as a closer cell instead of a distant mesh
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
- - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
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
- const ecosystemGraph = nodes.length > 1
654
- ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
655
- : {
656
- clusters: [],
657
- clustersBySize: new Map(),
658
- nodeClusterBySize: new Map(),
659
- levelSizes: [],
660
- expansionLevels: [],
661
- baseSize: ecosystemLevelNodeCap,
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 bloomZoomFocusStrength = (scale) => {
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
- const baseThreshold = nodeCount > massiveGraphNodeThreshold
1715
- ? massiveEcosystemClusterScaleThreshold
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
- if (!shouldProjectRenderNodesInDepth()) {
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
- if (!shouldProjectRenderNodesInDepth()) {
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 || shouldProjectRenderNodesInDepth()) {
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
- const hubDistanceCap = isDominantHub(state.primaryHub, nodeCount)
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: true, preferHubCenter: true })
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
- if (!shouldProjectRenderNodesInDepth()) {
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 depthScale = Math.min(clusterDepthScale(edge.sourceCluster), clusterDepthScale(edge.targetCluster))
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, source)
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) * sensitivityMultiplier * zoomOutDamping
3997
- const exponentCap = wheelZoomExponentCap * capMultiplier * (isZoomOut ? 0.74 : 1)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "0.1.0-beta.114",
3
+ "version": "0.1.0-beta.115",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",