@andespindola/brainlink 0.1.0-beta.113 → 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,21 +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
- - 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
600
601
  - keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
601
602
  - double-click on canvas zooms in at cursor position
602
603
  - floating graph totals (notes, links, tags) below the Brainlink title
603
604
  - graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
604
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
605
- - hierarchical hot-path optimizations reduce per-frame allocations and repeated scans during layered cluster expansion and edge projection
606
- - 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
607
- - 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
608
- - 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
609
606
  - WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
610
- - compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
611
- - 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
612
- - 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
613
608
 
614
609
  The server indexes before starting by default. Use `--no-index` to skip that step:
615
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
@@ -645,29 +609,15 @@ const recomputeVisibility = () => {
645
609
  y: macroHub ? macroHub.y : (bounds.minY + bounds.maxY) / 2
646
610
  }
647
611
  : { x: 0, y: 0 }
648
- const ecosystemGraph = nodes.length > 1
649
- ? buildEcosystemGraph(nodes, state.macroCenter, primaryHub)
650
- : {
651
- clusters: [],
652
- clustersBySize: new Map(),
653
- nodeClusterBySize: new Map(),
654
- levelSizes: [],
655
- expansionLevels: [],
656
- baseSize: ecosystemLevelNodeCap,
657
- hubCluster: null
658
- }
659
- state.ecosystemClusters = ecosystemGraph.clusters
660
- state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
661
- state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
662
- state.ecosystemLevelSizes = ecosystemGraph.levelSizes
663
- state.ecosystemLevelIndexBySize = ecosystemGraph.levelSizes.reduce((map, size, index) => {
664
- map.set(size, index)
665
- return map
666
- }, new Map())
667
- state.ecosystemHubNodeIds = new Set(ecosystemGraph.hubCluster?.nodeIds ?? [])
668
- state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
669
- state.ecosystemBaseSize = ecosystemGraph.baseSize
670
- 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
671
621
  state.macroRepresentative = resolveMacroRepresentative(nodes)
672
622
  markRenderDirty()
673
623
  }
@@ -773,271 +723,12 @@ const createVisibleEdgeLookup = edges => {
773
723
  return lookup
774
724
  }
775
725
 
776
- const ecosystemKeyForNode = node => {
777
- if (typeof node.segment === 'string' && node.segment.trim()) {
778
- return node.segment.trim()
779
- }
780
- if (typeof node.group === 'string' && node.group.trim()) {
781
- return node.group.trim()
782
- }
783
- const pathParts = String(node.path || '')
784
- .split('/')
785
- .filter(part => part.trim())
786
- .slice(0, 2)
787
- return pathParts.length > 0 ? pathParts.join('/') : 'root'
788
- }
789
-
790
- const compareNodesForEcosystem = (left, right) => {
791
- const keyComparison = ecosystemKeyForNode(left).localeCompare(ecosystemKeyForNode(right))
792
- if (keyComparison !== 0) return keyComparison
793
- const leftDegree = state.nodeDegrees.get(left.id) ?? 0
794
- const rightDegree = state.nodeDegrees.get(right.id) ?? 0
795
- if (leftDegree !== rightDegree) return rightDegree - leftDegree
796
- return String(left.title || left.id).localeCompare(String(right.title || right.id))
797
- }
798
-
799
- const selectEcosystemRepresentative = nodes => {
800
- let representative = nodes[0] ?? null
801
- let representativeScore = Number.NEGATIVE_INFINITY
802
-
803
- for (let index = 0; index < nodes.length; index += 1) {
804
- const node = nodes[index]
805
- const score = (state.nodeDegrees.get(node.id) ?? 0) + hubNodeScore(node) * 1000
806
- if (score > representativeScore) {
807
- representative = node
808
- representativeScore = score
809
- }
810
- }
811
-
812
- return representative
813
- }
814
-
815
- const ecosystemLayoutSpacingForSize = size => {
816
- if (size >= ecosystemLevelNodeCap) return 260
817
- if (size >= 320) return 110
818
- if (size >= 120) return 64
819
- if (size >= 48) return 34
820
- if (size >= 18) return 18
821
- if (size >= 8) return 11
822
- return 7
823
- }
824
-
825
- const buildIntermediateEcosystemSizes = (fromSize, toSize) => {
826
- if (fromSize <= toSize + 1) {
827
- return []
828
- }
829
- const intermediate = []
830
- let current = fromSize
831
- while (current > toSize + 1) {
832
- const stepped = Math.max(toSize + 1, Math.ceil(current / 3))
833
- if (stepped >= current) {
834
- break
835
- }
836
- intermediate.push(stepped)
837
- current = stepped
838
- }
839
- return intermediate
840
- }
841
-
842
- const buildEcosystemLevelSizes = nodeCount => {
843
- if (nodeCount <= 0) return []
844
- const primarySizes = []
845
- let currentSize = Math.max(1, Math.ceil(nodeCount / ecosystemLevelNodeCap))
846
- while (currentSize >= 1) {
847
- primarySizes.push(currentSize)
848
- if (currentSize === 1) {
849
- break
850
- }
851
- const nextSize = Math.max(1, Math.ceil(currentSize / ecosystemLevelNodeCap))
852
- if (nextSize === currentSize) {
853
- break
854
- }
855
- currentSize = nextSize
856
- }
857
- const expandedSizes = []
858
- for (let index = 0; index < primarySizes.length; index += 1) {
859
- const size = primarySizes[index]
860
- if (expandedSizes.length === 0 || expandedSizes[expandedSizes.length - 1] !== size) {
861
- expandedSizes.push(size)
862
- }
863
- const nextSize = primarySizes[index + 1]
864
- if (!Number.isFinite(nextSize)) {
865
- continue
866
- }
867
- const intermediate = buildIntermediateEcosystemSizes(size, nextSize)
868
- for (let intermediateIndex = 0; intermediateIndex < intermediate.length; intermediateIndex += 1) {
869
- const candidate = intermediate[intermediateIndex]
870
- if (expandedSizes[expandedSizes.length - 1] !== candidate) {
871
- expandedSizes.push(candidate)
872
- }
873
- }
874
- }
875
- if (expandedSizes[expandedSizes.length - 1] !== 1) {
876
- expandedSizes.push(1)
877
- }
878
- return expandedSizes
879
- }
880
-
881
- const buildEcosystemExpansionLevels = (levelSizes, nodeCount) => {
882
- if (levelSizes.length <= 1) {
883
- return []
884
- }
885
- const isMassive = nodeCount > massiveGraphNodeThreshold
886
- const maxScale = isMassive
887
- ? massiveEcosystemClusterScaleThreshold
888
- : ecosystemClusterScaleThreshold
889
- const startScale = isMassive ? 1.12 : 0.24
890
- const transitionCount = levelSizes.length - 1
891
- const usableScale = Math.max(0.08, maxScale - startScale)
892
- const step = usableScale / transitionCount
893
- const stride = isMassive ? 0.93 : 0.82
894
- const overlap = isMassive ? 1.22 : 1.62
895
- const levels = []
896
- for (let index = 0; index < transitionCount; index += 1) {
897
- const start = startScale + step * index * stride
898
- const end = Math.min(maxScale, start + step * overlap)
899
- levels.push({
900
- parentSize: levelSizes[index],
901
- childSize: levelSizes[index + 1],
902
- start,
903
- end
904
- })
905
- }
906
- return levels
907
- }
908
-
909
- const ecosystemCompactPoint = (index, total, center, spacing) => {
910
- if (total <= 1) {
911
- return { x: center.x, y: center.y }
912
- }
913
- const angle = index * 2.399963229728653
914
- const radius = spacing * Math.sqrt(index + 1)
915
- return {
916
- x: center.x + Math.cos(angle) * radius,
917
- y: center.y + Math.sin(angle) * radius
918
- }
919
- }
920
-
921
- const buildEcosystemCluster = (nodes, index, point) => {
922
- const count = Math.max(nodes.length, 1)
923
- const representative = selectEcosystemRepresentative(nodes)
924
-
925
- return {
926
- id: 'ecosystem-' + index,
927
- x: point.x,
928
- y: point.y,
929
- count,
930
- nodeIds: nodes.map(node => node.id),
931
- representative,
932
- label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
933
- }
934
- }
935
-
936
- const buildEcosystemHubCluster = (hub, center) => hub
937
- ? {
938
- id: 'ecosystem-hub',
939
- x: center.x,
940
- y: center.y,
941
- count: 1,
942
- size: 1,
943
- nodeIds: [hub.id],
944
- representative: hub,
945
- label: hub.title || 'Memory Hub',
946
- parentId: null,
947
- parentX: null,
948
- parentY: null,
949
- isHub: true
950
- }
951
- : null
952
-
953
- const buildEcosystemLevel = (sortedNodes, size, parentLookup, center) => {
954
- const clusters = []
955
- const clusterByNodeId = new Map()
956
- const parentChildIndex = new Map()
957
-
958
- for (let offset = 0; offset < sortedNodes.length; offset += size) {
959
- const clusterNodes = sortedNodes.slice(offset, offset + size)
960
- const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
961
- const siblingIndex = parentCluster
962
- ? (parentChildIndex.get(parentCluster.id) ?? 0)
963
- : clusters.length
964
- if (parentCluster) {
965
- parentChildIndex.set(parentCluster.id, siblingIndex + 1)
966
- }
967
- const point = parentCluster
968
- ? ecosystemCompactPoint(siblingIndex, Math.ceil((parentCluster.count || size) / size), parentCluster, ecosystemLayoutSpacingForSize(size))
969
- : ecosystemCompactPoint(clusters.length, Math.ceil(sortedNodes.length / size), center, ecosystemLayoutSpacingForSize(size))
970
- const cluster = {
971
- ...buildEcosystemCluster(clusterNodes, clusters.length, point),
972
- id: 'ecosystem-' + size + '-' + clusters.length,
973
- size,
974
- parentId: parentCluster?.id ?? null,
975
- parentX: parentCluster?.x ?? null,
976
- parentY: parentCluster?.y ?? null
977
- }
978
- clusters.push(cluster)
979
- for (let index = 0; index < clusterNodes.length; index += 1) {
980
- clusterByNodeId.set(clusterNodes[index].id, cluster)
981
- }
982
- }
983
-
984
- return { clusters, clusterByNodeId }
985
- }
986
-
987
- const buildEcosystemGraph = (nodes, center, hub) => {
988
- if (nodes.length === 0) {
989
- return {
990
- clusters: [],
991
- clustersBySize: new Map(),
992
- nodeClusterBySize: new Map(),
993
- levelSizes: [],
994
- expansionLevels: [],
995
- baseSize: ecosystemLevelNodeCap,
996
- hubCluster: null
997
- }
998
- }
999
-
1000
- const hubCluster = buildEcosystemHubCluster(hub, center)
1001
- const sortedNodes = nodes
1002
- .filter(node => node.id !== hub?.id)
1003
- .sort(compareNodesForEcosystem)
1004
- const levelSizes = buildEcosystemLevelSizes(sortedNodes.length)
1005
- const expansionLevels = buildEcosystemExpansionLevels(levelSizes, nodes.length)
1006
- const baseSize = levelSizes[0] ?? ecosystemLevelNodeCap
1007
- const clustersBySize = new Map()
1008
- const nodeClusterBySize = new Map()
1009
- let parentLookup = null
1010
-
1011
- for (let index = 0; index < levelSizes.length; index += 1) {
1012
- const size = levelSizes[index]
1013
- const level = buildEcosystemLevel(sortedNodes, size, parentLookup, center)
1014
- clustersBySize.set(size, level.clusters)
1015
- nodeClusterBySize.set(size, level.clusterByNodeId)
1016
- parentLookup = level.clusterByNodeId
1017
- }
1018
-
1019
- return {
1020
- clusters: clustersBySize.get(baseSize) ?? [],
1021
- clustersBySize,
1022
- nodeClusterBySize,
1023
- levelSizes,
1024
- expansionLevels,
1025
- baseSize,
1026
- hubCluster
1027
- }
1028
- }
1029
-
1030
726
  const isClusterInViewport = (cluster, viewport) =>
1031
727
  cluster.x >= viewport.minX &&
1032
728
  cluster.x <= viewport.maxX &&
1033
729
  cluster.y >= viewport.minY &&
1034
730
  cluster.y <= viewport.maxY
1035
731
 
1036
- const filterEcosystemClustersByViewport = (clusters, viewport) => {
1037
- const visible = clusters.filter(cluster => isClusterInViewport(cluster, viewport))
1038
- return visible.length > 0 ? visible : [...clusters]
1039
- }
1040
-
1041
732
  const ecosystemFocusPoint = () => {
1042
733
  const cursorPoint = cursorWorldPoint()
1043
734
  if (cursorPoint) {
@@ -1050,422 +741,6 @@ const ecosystemFocusPoint = () => {
1050
741
  return viewportCenterWorldPoint()
1051
742
  }
1052
743
 
1053
- const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
1054
- clusters
1055
- .map(cluster => ({
1056
- cluster,
1057
- distance: Math.max(
1058
- 0,
1059
- Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y) -
1060
- clusterRadiusPx(cluster) / Math.max(state.transform.scale, 0.0001)
1061
- )
1062
- }))
1063
- .sort((left, right) => left.distance - right.distance)
1064
- .slice(0, limit)
1065
- .map(item => item.cluster.id)
1066
-
1067
- const focusedParentCluster = (clusters, focusPoint) => {
1068
- if (clusters.length === 0) {
1069
- return null
1070
- }
1071
- let focused = clusters[0]
1072
- let nearestDistance = Number.POSITIVE_INFINITY
1073
- for (let index = 0; index < clusters.length; index += 1) {
1074
- const cluster = clusters[index]
1075
- const distance = Math.max(
1076
- 0,
1077
- Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y) -
1078
- clusterRadiusPx(cluster) / Math.max(state.transform.scale, 0.0001)
1079
- )
1080
- if (distance < nearestDistance) {
1081
- nearestDistance = distance
1082
- focused = cluster
1083
- }
1084
- }
1085
- return focused
1086
- }
1087
-
1088
- const nearestSiblingScreenDistancePx = (focusedCluster, clusters) => {
1089
- let nearestDistancePx = Number.POSITIVE_INFINITY
1090
- for (let index = 0; index < clusters.length; index += 1) {
1091
- const cluster = clusters[index]
1092
- if (cluster.id === focusedCluster.id) continue
1093
- const distancePx = Math.hypot(
1094
- (cluster.x - focusedCluster.x) * state.transform.scale,
1095
- (cluster.y - focusedCluster.y) * state.transform.scale
1096
- )
1097
- if (distancePx < nearestDistancePx) {
1098
- nearestDistancePx = distancePx
1099
- }
1100
- }
1101
- return nearestDistancePx
1102
- }
1103
-
1104
- const ecosystemFocusReadiness = (parentClusters, focusPoint, childSize) => {
1105
- if (parentClusters.length <= 1) {
1106
- return 1
1107
- }
1108
- const focusedCluster = focusedParentCluster(parentClusters, focusPoint)
1109
- if (!focusedCluster) {
1110
- return 0
1111
- }
1112
- const nearestDistancePx = nearestSiblingScreenDistancePx(focusedCluster, parentClusters)
1113
- const sizeHalfCap = Math.ceil(ecosystemLevelNodeCap / 2)
1114
- const sizeEighthCap = Math.ceil(ecosystemLevelNodeCap / 8)
1115
- const focusDistanceTargetPx = childSize >= sizeHalfCap
1116
- ? 680
1117
- : childSize >= sizeEighthCap
1118
- ? 520
1119
- : 380
1120
- const focusDistanceRangePx = childSize >= sizeHalfCap ? 260 : 220
1121
- return smoothStep((nearestDistancePx - focusDistanceTargetPx) / focusDistanceRangePx)
1122
- }
1123
- const smoothStep = value => {
1124
- const clamped = Math.max(0, Math.min(1, value))
1125
- return clamped * clamped * (3 - clamped * 2)
1126
- }
1127
-
1128
- const zoomProgress = (scale, start, end) =>
1129
- smoothStep((scale - start) / Math.max(end - start, 0.0001))
1130
-
1131
- const semanticZoomSpread = (progress, childSize) => {
1132
- const spreadExponent = childSize <= Math.ceil(ecosystemLevelNodeCap / 12) ? 5.6 : 4.2
1133
- const curve = Math.pow(progress, spreadExponent)
1134
- if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
1135
- return 0.12 + curve * 0.88
1136
- }
1137
- return curve
1138
- }
1139
-
1140
- const opacityForProgress = (progress, childSize) => {
1141
- const opacityExponent = childSize <= Math.ceil(ecosystemLevelNodeCap / 12) ? 2.8 : 2.1
1142
- const eased = Math.pow(progress, opacityExponent)
1143
- if (childSize >= Math.ceil(ecosystemLevelNodeCap / 2)) {
1144
- return 0.22 + eased * 0.78
1145
- }
1146
- return eased
1147
- }
1148
-
1149
- const expandFocusedClusters = (parentClusters, focusPoint, childSize, progress, spread, viewport) => {
1150
- const expandedParentIds = new Set(nearestEcosystemParentIds(
1151
- parentClusters,
1152
- focusPoint,
1153
- ecosystemFocusedParentLimit
1154
- ))
1155
- const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
1156
- const visibleChildClusters = []
1157
- for (let index = 0; index < childClusters.length; index += 1) {
1158
- const cluster = childClusters[index]
1159
- if (!expandedParentIds.has(cluster.parentId)) {
1160
- continue
1161
- }
1162
- const spreadCluster = spreadChildClusterFromParent(cluster, childSize, progress, spread)
1163
- if (!isClusterInViewport(spreadCluster, viewport)) {
1164
- continue
1165
- }
1166
- visibleChildClusters.push(spreadCluster)
1167
- }
1168
-
1169
- return {
1170
- expandedParentIds,
1171
- childClusters: visibleChildClusters
1172
- }
1173
- }
1174
-
1175
- const spreadChildClusterFromParent = (cluster, childSize, progress, spread) => {
1176
- if (!Number.isFinite(cluster.parentX) || !Number.isFinite(cluster.parentY)) {
1177
- return {
1178
- ...cluster,
1179
- lodOpacity: opacityForProgress(progress, childSize)
1180
- }
1181
- }
1182
-
1183
- return {
1184
- ...cluster,
1185
- x: cluster.parentX + (cluster.x - cluster.parentX) * spread,
1186
- y: cluster.parentY + (cluster.y - cluster.parentY) * spread,
1187
- lodOpacity: opacityForProgress(progress, childSize)
1188
- }
1189
- }
1190
-
1191
- const selectHierarchicalEcosystemClusters = viewport => {
1192
- const baseClusters = state.ecosystemClustersBySize.get(state.ecosystemBaseSize) ?? state.ecosystemClusters
1193
- const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
1194
- const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
1195
- const visibleClusters = [...visibleBaseClusters]
1196
- const clustersBySize = new Map()
1197
- for (let index = 0; index < visibleBaseClusters.length; index += 1) {
1198
- const cluster = visibleBaseClusters[index]
1199
- const levelClusters = clustersBySize.get(cluster.size)
1200
- if (levelClusters) {
1201
- levelClusters.push(cluster)
1202
- } else {
1203
- clustersBySize.set(cluster.size, [cluster])
1204
- }
1205
- }
1206
- const focusPoint = ecosystemFocusPoint()
1207
-
1208
- for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
1209
- const level = state.ecosystemExpansionLevels[index]
1210
- const parentClusters = clustersBySize.get(level.parentSize) ?? []
1211
- if (parentClusters.length === 0) {
1212
- continue
1213
- }
1214
- const zoomLevelProgress = zoomProgress(state.transform.scale, level.start, level.end)
1215
- const focusReadiness = ecosystemFocusReadiness(parentClusters, focusPoint, level.childSize)
1216
- const progress = zoomLevelProgress * focusReadiness
1217
- if (progress <= 0.002) {
1218
- continue
1219
- }
1220
- const spread = semanticZoomSpread(progress, level.childSize)
1221
- const expansion = expandFocusedClusters(parentClusters, focusPoint, level.childSize, progress, spread, viewport)
1222
- visibleClusters.push(...expansion.childClusters)
1223
- if (expansion.childClusters.length > 0) {
1224
- const levelClusters = clustersBySize.get(level.childSize)
1225
- if (levelClusters) {
1226
- levelClusters.push(...expansion.childClusters)
1227
- } else {
1228
- clustersBySize.set(level.childSize, [...expansion.childClusters])
1229
- }
1230
- }
1231
- }
1232
-
1233
- return [...hubClusters, ...visibleClusters]
1234
- }
1235
-
1236
- const ecosystemLevelIndexBySize = () => state.ecosystemLevelIndexBySize
1237
-
1238
- const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
1239
- if (cluster.isHub) {
1240
- return ecosystemDepthNear
1241
- }
1242
- const maxLevelIndex = Math.max(state.ecosystemLevelSizes.length - 1, 0)
1243
- const levelIndex = levelIndexMap.get(cluster.size) ?? 0
1244
- const reverseIndex = Math.max(0, maxLevelIndex - levelIndex)
1245
- const normalized = maxLevelIndex === 0 ? 0 : reverseIndex / maxLevelIndex
1246
- return ecosystemDepthNear + normalized * (ecosystemDepthFar - ecosystemDepthNear)
1247
- }
1248
-
1249
- const projectEcosystemPoint = (x, y, depth, anchor) => {
1250
- const safeDepth = Math.max(0, depth)
1251
- const dx = x - anchor.x
1252
- const dy = y - anchor.y
1253
- const yawSin = Math.sin(ecosystemDepthYaw)
1254
- const yawCos = Math.cos(ecosystemDepthYaw)
1255
- const pitchSin = Math.sin(ecosystemDepthPitch)
1256
- const pitchCos = Math.cos(ecosystemDepthPitch)
1257
- const rotatedX = dx * yawCos + safeDepth * yawSin
1258
- const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
1259
- const rotatedY = dy * pitchCos - rotatedZ * pitchSin
1260
- const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
1261
- const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + projectedDepth)
1262
- const verticalTilt = projectedDepth * ecosystemDepthTiltY
1263
- return {
1264
- x: anchor.x + rotatedX * factor,
1265
- y: anchor.y + rotatedY * factor - verticalTilt,
1266
- factor,
1267
- projectedDepth
1268
- }
1269
- }
1270
-
1271
- const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
1272
- const levelIndexMap = ecosystemLevelIndexBySize()
1273
- const projectedClusters = []
1274
- const clusterById = new Map()
1275
-
1276
- for (let index = 0; index < clusters.length; index += 1) {
1277
- const cluster = clusters[index]
1278
- const baseDepth = ecosystemDepthForCluster(cluster, levelIndexMap)
1279
- const radialDistance = Math.hypot(cluster.x - anchor.x, cluster.y - anchor.y)
1280
- const radialOffset = cluster.isHub ? 0 : Math.min(320, radialDistance * ecosystemDepthRadialGain)
1281
- const orbitalOffset = cluster.isHub
1282
- ? 0
1283
- : Math.sin(Math.atan2(cluster.y - anchor.y, cluster.x - anchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
1284
- const depth = Math.max(0, baseDepth + radialOffset + orbitalOffset)
1285
- const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, anchor)
1286
- const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
1287
- const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
1288
- const depthOpacity = Math.max(
1289
- ecosystemDepthOpacityFloor,
1290
- Math.min(1, depthScale * 1.08)
1291
- )
1292
- const projectedCluster = {
1293
- ...cluster,
1294
- x: projected.x,
1295
- y: projected.y,
1296
- lodOpacity: baseOpacity * depthOpacity,
1297
- depth: projected.projectedDepth,
1298
- depthScale
1299
- }
1300
- projectedClusters.push(projectedCluster)
1301
- clusterById.set(projectedCluster.id, projectedCluster)
1302
- }
1303
-
1304
- const projectedEdges = []
1305
- for (let index = 0; index < edges.length; index += 1) {
1306
- const edge = edges[index]
1307
- const sourceCluster = clusterById.get(edge.sourceCluster.id)
1308
- const targetCluster = clusterById.get(edge.targetCluster.id)
1309
- if (!sourceCluster || !targetCluster) {
1310
- continue
1311
- }
1312
- projectedEdges.push({
1313
- ...edge,
1314
- sourceCluster,
1315
- targetCluster
1316
- })
1317
- }
1318
-
1319
- return {
1320
- clusters: projectedClusters,
1321
- edges: projectedEdges
1322
- }
1323
- }
1324
-
1325
- const ecosystemSiblingEdgesForClusters = (clusters, existingEdges) => {
1326
- const byParent = new Map()
1327
- for (let index = 0; index < clusters.length; index += 1) {
1328
- const cluster = clusters[index]
1329
- if (cluster.isHub || !cluster.parentId) {
1330
- continue
1331
- }
1332
- const siblings = byParent.get(cluster.parentId)
1333
- if (siblings) {
1334
- siblings.push(cluster)
1335
- } else {
1336
- byParent.set(cluster.parentId, [cluster])
1337
- }
1338
- }
1339
-
1340
- const edges = []
1341
- for (const siblings of byParent.values()) {
1342
- const ordered = [...siblings]
1343
- .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)))
1344
- for (let index = 0; index < ordered.length && edges.length < ecosystemSiblingEdgeLimit; index += 1) {
1345
- const sourceCluster = ordered[index]
1346
- const targetCluster = ordered[(index + 1) % ordered.length]
1347
- if (!targetCluster || sourceCluster.id === targetCluster.id) {
1348
- continue
1349
- }
1350
- const orderedIds = sourceCluster.id < targetCluster.id
1351
- ? [sourceCluster.id, targetCluster.id]
1352
- : [targetCluster.id, sourceCluster.id]
1353
- const key = orderedIds.join(':')
1354
- if (existingEdges.has(key)) {
1355
- continue
1356
- }
1357
- const edge = {
1358
- id: key,
1359
- sourceCluster,
1360
- targetCluster,
1361
- weight: 0.7,
1362
- inferred: true
1363
- }
1364
- existingEdges.set(key, edge)
1365
- edges.push(edge)
1366
- }
1367
- }
1368
-
1369
- return edges
1370
- }
1371
-
1372
- const ecosystemEdgesForClusters = clusters => {
1373
- const edgeClusters = clusters.filter(cluster => cluster.isHub || clusterOpacity(cluster) > 0.018)
1374
- const clusterById = new Map(edgeClusters.map(cluster => [cluster.id, cluster]))
1375
- const clusterIds = new Set(clusterById.keys())
1376
- const levelsBySize = []
1377
- const seenSizes = new Set()
1378
- for (let index = 0; index < edgeClusters.length; index += 1) {
1379
- const cluster = edgeClusters[index]
1380
- if (!cluster.size || cluster.isHub) continue
1381
- if (seenSizes.has(cluster.size)) continue
1382
- seenSizes.add(cluster.size)
1383
- levelsBySize.push({
1384
- size: cluster.size,
1385
- lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
1386
- })
1387
- }
1388
- levelsBySize.sort((left, right) => left.size - right.size)
1389
- const resolvedNodeClusterById = new Map()
1390
- const resolveClusterForNode = nodeId => {
1391
- if (resolvedNodeClusterById.has(nodeId)) {
1392
- return resolvedNodeClusterById.get(nodeId)
1393
- }
1394
- if (state.ecosystemHubNodeIds.has(nodeId) && state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)) {
1395
- resolvedNodeClusterById.set(nodeId, state.ecosystemHubCluster)
1396
- return state.ecosystemHubCluster
1397
- }
1398
- for (let index = 0; index < levelsBySize.length; index += 1) {
1399
- const lookup = levelsBySize[index].lookup
1400
- const cluster = lookup.get(nodeId)
1401
- if (cluster && clusterIds.has(cluster.id)) {
1402
- const resolvedCluster = clusterById.get(cluster.id) ?? cluster
1403
- resolvedNodeClusterById.set(nodeId, resolvedCluster)
1404
- return resolvedCluster
1405
- }
1406
- }
1407
- resolvedNodeClusterById.set(nodeId, null)
1408
- return null
1409
- }
1410
-
1411
- const edgeByClusterPair = new Map()
1412
- for (let index = 0; index < state.visibleEdges.length; index += 1) {
1413
- const edge = state.visibleEdges[index]
1414
- const sourceCluster = resolveClusterForNode(edge.source)
1415
- const targetCluster = resolveClusterForNode(edge.target)
1416
- if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
1417
- continue
1418
- }
1419
-
1420
- const orderedIds = sourceCluster.id < targetCluster.id
1421
- ? [sourceCluster.id, targetCluster.id]
1422
- : [targetCluster.id, sourceCluster.id]
1423
- const key = orderedIds.join(':')
1424
- const current = edgeByClusterPair.get(key)
1425
- if (current) {
1426
- current.weight += edgeWeight(edge)
1427
- continue
1428
- }
1429
-
1430
- edgeByClusterPair.set(key, {
1431
- id: key,
1432
- sourceCluster,
1433
- targetCluster,
1434
- weight: edgeWeight(edge)
1435
- })
1436
- }
1437
-
1438
- ecosystemSiblingEdgesForClusters(edgeClusters, edgeByClusterPair)
1439
- const edges = Array.from(edgeByClusterPair.values())
1440
- .sort((left, right) => right.weight - left.weight)
1441
- .slice(0, ecosystemClusterEdgeLimit)
1442
- const hubCluster = state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)
1443
- ? state.ecosystemHubCluster
1444
- : null
1445
- if (!hubCluster) {
1446
- return edges
1447
- }
1448
-
1449
- const existingHubTargets = new Set(edges.flatMap(edge =>
1450
- edge.sourceCluster.id === hubCluster.id
1451
- ? [edge.targetCluster.id]
1452
- : edge.targetCluster.id === hubCluster.id
1453
- ? [edge.sourceCluster.id]
1454
- : []
1455
- ))
1456
- const syntheticHubEdges = edgeClusters
1457
- .filter(cluster => cluster.id !== hubCluster.id && !existingHubTargets.has(cluster.id))
1458
- .slice(0, ecosystemHubEdgeLimit)
1459
- .map(cluster => ({
1460
- id: hubCluster.id + ':' + cluster.id,
1461
- sourceCluster: hubCluster,
1462
- targetCluster: cluster,
1463
- weight: 1,
1464
- inferred: true
1465
- }))
1466
- return edges.concat(syntheticHubEdges)
1467
- }
1468
-
1469
744
  const edgeBudgetForCurrentFrame = () => {
1470
745
  const zoom = state.transform.scale
1471
746
  if (zoom < 0.12) return 380
@@ -1614,22 +889,16 @@ const cursorWorldPoint = () => {
1614
889
  return screenToWorldPoint(screenX, screenY)
1615
890
  }
1616
891
 
892
+ const resolveZoomAnchorWorldPoint = (screenX, screenY) => screenToWorldPoint(screenX, screenY)
893
+
1617
894
  const visibilityScaleBucket = (scale) => {
1618
895
  const safeScale = Math.max(zoomRange.min, scale)
1619
896
  return Math.round(safeScale * 180_000)
1620
897
  }
1621
898
 
1622
899
  const shouldRenderEcosystemClusterView = (nodeCount, scale) => {
1623
- const baseThreshold = nodeCount > massiveGraphNodeThreshold
1624
- ? massiveEcosystemClusterScaleThreshold
1625
- : ecosystemClusterScaleThreshold
1626
- const enterThreshold = baseThreshold * ecosystemClusterEnterHysteresis
1627
- const exitThreshold = baseThreshold * ecosystemClusterExitHysteresis
1628
- const shouldRender = state.ecosystemViewActive
1629
- ? scale <= exitThreshold
1630
- : scale <= enterThreshold
1631
- state.ecosystemViewActive = shouldRender
1632
- return shouldRender
900
+ state.ecosystemViewActive = false
901
+ return false
1633
902
  }
1634
903
 
1635
904
  const shouldRenderMacroGalaxyView = () => {
@@ -1826,20 +1095,11 @@ const edgeOpacityForScale = (edge, scale) => {
1826
1095
  }
1827
1096
 
1828
1097
  const edgeDepthOpacity = edge => {
1829
- if (!shouldProjectRenderNodesInDepth()) {
1830
- return 1
1831
- }
1832
- return Math.max(
1833
- graphDepthEdgeOpacityFloor,
1834
- Math.min(nodeRenderOpacity(edge.sourceNode), nodeRenderOpacity(edge.targetNode))
1835
- )
1098
+ return 1
1836
1099
  }
1837
1100
 
1838
1101
  const edgeDepthScale = edge => {
1839
- if (!shouldProjectRenderNodesInDepth()) {
1840
- return 1
1841
- }
1842
- return Math.max(0.62, Math.min(1.18, Math.min(nodeRenderScale(edge.sourceNode), nodeRenderScale(edge.targetNode))))
1102
+ return 1
1843
1103
  }
1844
1104
 
1845
1105
  const edgeStrokeFor = (edge, selectedEdge) => {
@@ -2022,10 +1282,6 @@ const drawGraphNodes = () => {
2022
1282
  }
2023
1283
  }
2024
1284
 
2025
- if (shouldProjectRenderNodesInDepth()) {
2026
- regularNodes.sort((left, right) => nodeRenderDepth(right) - nodeRenderDepth(left))
2027
- }
2028
-
2029
1285
  drawNodeBatch(regularNodes)
2030
1286
 
2031
1287
  if (state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) {
@@ -2102,7 +1358,7 @@ const drawGraphLabels = nodes => {
2102
1358
  }
2103
1359
 
2104
1360
  const drawAcceleratedGraph = (width, height, drawEdges) => {
2105
- if (!webGlRenderer || state.renderClusters.length > 0 || shouldProjectRenderNodesInDepth()) {
1361
+ if (!webGlRenderer || state.renderClusters.length > 0) {
2106
1362
  return false
2107
1363
  }
2108
1364
 
@@ -2486,12 +1742,7 @@ const zoomCapByHubDistance = (distance) => {
2486
1742
 
2487
1743
  const currentZoomMax = () => {
2488
1744
  const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
2489
- const hubDistanceCap = isDominantHub(state.primaryHub, nodeCount)
2490
- ? zoomCapByHubDistance(state.hubNeighborDistance)
2491
- : zoomRange.max
2492
- const minimumUsefulCap = nodeCount > massiveGraphNodeThreshold ? 1.9 : nodeCount > largeGraphNodeThreshold ? 1.35 : 0.8
2493
- const capped = Math.min(zoomCapByNodeCount(nodeCount), Math.max(minimumUsefulCap, hubDistanceCap))
2494
- return Math.max(zoomRange.min * 2, capped)
1745
+ return Math.max(zoomRange.min * 2, zoomCapByNodeCount(nodeCount))
2495
1746
  }
2496
1747
 
2497
1748
  const zoomFloorByNodeCount = (nodeCount) => {
@@ -2724,7 +1975,7 @@ const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: t
2724
1975
  markRenderDirty()
2725
1976
  }
2726
1977
 
2727
- const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
1978
+ const resetView = () => fitView({ useFiltered: false, macro: false, preferHubCenter: false })
2728
1979
 
2729
1980
  const focusPrimaryHub = () => {
2730
1981
  const hub = state.primaryHub
@@ -3147,103 +2398,13 @@ const clusterRadiusPx = cluster => {
3147
2398
  const clusterOpacity = cluster =>
3148
2399
  Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
3149
2400
 
3150
- const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
3151
- const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
3152
-
3153
- const shouldProjectRenderNodesInDepth = () => {
3154
- const withinNodeCountWindow =
3155
- state.renderClusters.length === 0 &&
3156
- state.renderNodes.length >= graphDepthProjectionNodeThreshold &&
3157
- state.renderNodes.length <= graphDepthProjectionNodeCap &&
3158
- !state.pointer.down
3159
-
3160
- if (!withinNodeCountWindow) {
3161
- state.depthProjectionActive = false
3162
- return false
3163
- }
3164
-
3165
- const scale = state.transform.scale
3166
- const shouldProject = state.depthProjectionActive
3167
- ? scale >= graphDepthProjectionExitMinScale && scale <= graphDepthProjectionExitMaxScale
3168
- : scale >= graphDepthProjectionEnterMinScale && scale <= graphDepthProjectionEnterMaxScale
3169
- state.depthProjectionActive = shouldProject
3170
- return shouldProject
3171
- }
3172
-
3173
- const nodeProjectionAnchor = () => {
3174
- const hub = state.primaryHub
3175
- if (hub) {
3176
- return { x: hub.x, y: hub.y }
3177
- }
3178
- if (state.macroRepresentative) {
3179
- return { x: state.macroRepresentative.x, y: state.macroRepresentative.y }
3180
- }
3181
- return ecosystemFocusPoint()
3182
- }
3183
-
3184
- const projectGraphNodePoint = (x, y, depth, anchor) => {
3185
- const safeDepth = Math.max(0, depth)
3186
- const dx = x - anchor.x
3187
- const dy = y - anchor.y
3188
- const yawSin = Math.sin(graphDepthYaw)
3189
- const yawCos = Math.cos(graphDepthYaw)
3190
- const pitchSin = Math.sin(graphDepthPitch)
3191
- const pitchCos = Math.cos(graphDepthPitch)
3192
- const rotatedX = dx * yawCos + safeDepth * yawSin
3193
- const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
3194
- const rotatedY = dy * pitchCos - rotatedZ * pitchSin
3195
- const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
3196
- const factor = graphDepthPerspective / (graphDepthPerspective + projectedDepth)
3197
- return {
3198
- x: anchor.x + rotatedX * factor,
3199
- y: anchor.y + rotatedY * factor,
3200
- depth: projectedDepth,
3201
- scale: graphDepthMinScale + (1 - graphDepthMinScale) * factor
3202
- }
3203
- }
3204
-
3205
2401
  const refreshRenderNodeDepthProjection = () => {
3206
- if (!shouldProjectRenderNodesInDepth()) {
3207
- state.renderNodeDepthProjectionById = new Map()
3208
- return
3209
- }
3210
-
3211
- const anchor = nodeProjectionAnchor()
3212
- let maxDistance = 1
3213
- for (let index = 0; index < state.renderNodes.length; index += 1) {
3214
- const node = state.renderNodes[index]
3215
- const distance = Math.hypot(node.x - anchor.x, node.y - anchor.y)
3216
- if (distance > maxDistance) {
3217
- maxDistance = distance
3218
- }
3219
- }
3220
-
3221
- const projectionById = new Map()
3222
- for (let index = 0; index < state.renderNodes.length; index += 1) {
3223
- const node = state.renderNodes[index]
3224
- const radialDistance = Math.hypot(node.x - anchor.x, node.y - anchor.y)
3225
- const radialRatio = radialDistance / maxDistance
3226
- const degree = state.nodeDegrees.get(node.id) ?? 0
3227
- const degreeLift = Math.min(60, degree * 2.2)
3228
- const radialDepth = radialRatio * graphDepthFar * graphDepthRadialGain
3229
- const depth = Math.max(0, graphDepthNear + radialDepth - degreeLift)
3230
- const projected = projectGraphNodePoint(node.x, node.y, depth, anchor)
3231
- const opacity = Math.max(graphDepthOpacityFloor, Math.min(1, projected.scale * 1.08))
3232
- projectionById.set(node.id, {
3233
- x: projected.x,
3234
- y: projected.y,
3235
- depth: projected.depth,
3236
- scale: projected.scale,
3237
- opacity
3238
- })
3239
- }
3240
- state.renderNodeDepthProjectionById = projectionById
2402
+ state.renderNodeDepthProjectionById = new Map()
3241
2403
  }
3242
2404
 
3243
2405
  const projectedNode = node => state.renderNodeDepthProjectionById.get(node.id) ?? null
3244
2406
  const nodeRenderX = node => projectedNode(node)?.x ?? node.x
3245
2407
  const nodeRenderY = node => projectedNode(node)?.y ?? node.y
3246
- const nodeRenderDepth = node => projectedNode(node)?.depth ?? graphDepthNear
3247
2408
  const nodeRenderScale = node => projectedNode(node)?.scale ?? 1
3248
2409
  const nodeRenderOpacity = node => projectedNode(node)?.opacity ?? 1
3249
2410
  const worldViewportBounds = () => {
@@ -3406,22 +2567,6 @@ const computeRenderVisibility = () => {
3406
2567
  return
3407
2568
  }
3408
2569
 
3409
- if (
3410
- state.ecosystemExpansionLevels.length > 0 &&
3411
- shouldRenderEcosystemClusterView(state.visibleNodes.length, state.transform.scale) &&
3412
- state.ecosystemClusters.length > 0
3413
- ) {
3414
- const clusters = selectHierarchicalEcosystemClusters(viewport)
3415
- .sort((left, right) => right.count - left.count)
3416
- const edges = ecosystemEdgesForClusters(clusters)
3417
- const projectionAnchor = ecosystemFocusPoint()
3418
- const projected = applyEcosystemDepthProjection(clusters, edges, projectionAnchor)
3419
- state.renderClusters = projected.clusters
3420
- state.renderClusterEdges = projected.edges
3421
- state.renderNodes = []
3422
- state.renderEdges = []
3423
- return
3424
- }
3425
2570
  state.ecosystemViewActive = false
3426
2571
 
3427
2572
  if (state.visibleNodes.length <= 2000) {
@@ -3667,7 +2812,6 @@ const render = now => {
3667
2812
  ctx.translate(state.transform.x, state.transform.y)
3668
2813
  ctx.scale(state.transform.scale, state.transform.scale)
3669
2814
  const orderedClusters = [...state.renderClusters]
3670
- .sort((left, right) => clusterDepth(right) - clusterDepth(left))
3671
2815
  const safeScale = Math.max(state.transform.scale, 0.0001)
3672
2816
  if (state.renderClusterEdges.length > 0) {
3673
2817
  for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
@@ -3676,8 +2820,7 @@ const render = now => {
3676
2820
  if (edgeOpacity <= 0.01) {
3677
2821
  continue
3678
2822
  }
3679
- const depthScale = Math.min(clusterDepthScale(edge.sourceCluster), clusterDepthScale(edge.targetCluster))
3680
- const widthScale = 0.6 + depthScale * 0.9
2823
+ const widthScale = 1
3681
2824
  ctx.beginPath()
3682
2825
  ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
3683
2826
  ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
@@ -3862,7 +3005,7 @@ const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
3862
3005
  state.zoomTransition.screenX === screenX &&
3863
3006
  state.zoomTransition.screenY === screenY
3864
3007
  ? { x: state.zoomTransition.worldX, y: state.zoomTransition.worldY }
3865
- : screenToWorldPoint(screenX, screenY)
3008
+ : resolveZoomAnchorWorldPoint(screenX, screenY)
3866
3009
  const worldX = worldPointAtCursor.x
3867
3010
  const worldY = worldPointAtCursor.y
3868
3011
  state.lastZoomFocus = {
@@ -3892,18 +3035,13 @@ const wheelZoomFactor = event => {
3892
3035
  return 1
3893
3036
  }
3894
3037
 
3895
- const isMassiveEcosystemZoom =
3896
- state.visibleNodes.length > massiveGraphNodeThreshold &&
3897
- state.transform.scale <= massiveEcosystemClusterScaleThreshold
3898
- const sensitivityMultiplier = isMassiveEcosystemZoom ? 0.48 : 1
3899
- const capMultiplier = isMassiveEcosystemZoom ? 0.34 : 1
3900
3038
  const isZoomOut = normalizedDelta > 0
3901
3039
  const currentScale = state.transform.scale
3902
3040
  const zoomOutDamping = isZoomOut
3903
3041
  ? (currentScale <= 0.03 ? 0.38 : currentScale <= 0.08 ? 0.52 : 0.68)
3904
3042
  : 1
3905
- const sensitivity = wheelZoomExponent * (isModifierZoom ? wheelZoomModifierBoost : 1) * sensitivityMultiplier * zoomOutDamping
3906
- const exponentCap = wheelZoomExponentCap * capMultiplier * (isZoomOut ? 0.74 : 1)
3043
+ const sensitivity = wheelZoomExponent * (isModifierZoom ? wheelZoomModifierBoost : 1) * zoomOutDamping
3044
+ const exponentCap = wheelZoomExponentCap * (isZoomOut ? 0.74 : 1)
3907
3045
  const exponent = Math.max(
3908
3046
  -exponentCap,
3909
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.113",
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",