@andespindola/brainlink 0.1.0-beta.85 → 0.1.0-beta.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/application/frontend/client-js.js +128 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,11 +84,11 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
84
84
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
85
85
|
- Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
|
|
86
86
|
- WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
|
|
87
|
-
- Graph zoom-out renders
|
|
87
|
+
- Graph zoom-out renders hierarchical ecosystem subgraphs: macro clusters of up to 1000 notes expand near the user's focus into smaller graph meshes before individual notes are rendered.
|
|
88
88
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
89
89
|
- Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
|
|
90
90
|
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
|
|
91
|
-
- Zoomed-out graph LOD
|
|
91
|
+
- Zoomed-out graph LOD renders nested subgraphs and progressively expands only the focused cluster as zoom increases, including very large vaults.
|
|
92
92
|
- Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
|
|
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.
|
|
@@ -602,7 +602,7 @@ The graph UI shows:
|
|
|
602
602
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
603
603
|
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
604
604
|
- graph camera treats hub-centered navigation as structural only when the hub is dominant; diffuse stress graphs reset and zoom around the full graph mass
|
|
605
|
-
- graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes with aggregated real links, then progressively raises the focused node budget
|
|
605
|
+
- graph LOD progression: very low zoom uses connected ecosystem clusters of up to 1000 notes, zoom-in expands only focused clusters into 250-note and 60-note subgraphs with aggregated real links, then progressively raises the focused node budget so local areas keep nearby notes and links visible
|
|
606
606
|
|
|
607
607
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
608
608
|
|
|
@@ -22,8 +22,12 @@ const worldCoordinateLimit = 5_000_000
|
|
|
22
22
|
const transformCoordinateLimit = 20_000_000
|
|
23
23
|
const hoverHitTestIntervalMs = 64
|
|
24
24
|
const ecosystemGroupSize = 1000
|
|
25
|
+
const ecosystemGroupSizes = [1000, 250, 60]
|
|
25
26
|
const ecosystemClusterEdgeLimit = 520
|
|
26
|
-
const ecosystemClusterScaleThreshold = 0.
|
|
27
|
+
const ecosystemClusterScaleThreshold = 0.32
|
|
28
|
+
const ecosystemSubgraphScaleThreshold = 0.18
|
|
29
|
+
const ecosystemMicroScaleThreshold = 0.08
|
|
30
|
+
const ecosystemFocusedParentLimit = 3
|
|
27
31
|
const zoomRecoveryGuardMs = 4200
|
|
28
32
|
const zoomCapTargetViewportShare = 0.72
|
|
29
33
|
const meshEdgeScaleThreshold = 0.09
|
|
@@ -68,7 +72,7 @@ const state = {
|
|
|
68
72
|
visibleNodeSpatial: { cellSize: 220, minX: 0, minY: 0, maxX: 0, maxY: 0, buckets: new Map() },
|
|
69
73
|
visibleEdgeByNode: new Map(),
|
|
70
74
|
ecosystemClusters: [],
|
|
71
|
-
|
|
75
|
+
ecosystemClustersBySize: new Map(),
|
|
72
76
|
macroCenter: { x: 0, y: 0 },
|
|
73
77
|
macroRepresentative: null,
|
|
74
78
|
primaryHub: null,
|
|
@@ -566,10 +570,10 @@ const recomputeVisibility = () => {
|
|
|
566
570
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
567
571
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
568
572
|
const ecosystemGraph = nodes.length > 1
|
|
569
|
-
? buildEcosystemGraph(nodes
|
|
570
|
-
: { clusters: [],
|
|
573
|
+
? buildEcosystemGraph(nodes)
|
|
574
|
+
: { clusters: [], clustersBySize: new Map() }
|
|
571
575
|
state.ecosystemClusters = ecosystemGraph.clusters
|
|
572
|
-
state.
|
|
576
|
+
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
573
577
|
const primaryHub = rankedHubNodes()[0] ?? null
|
|
574
578
|
state.primaryHub = primaryHub
|
|
575
579
|
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
@@ -738,32 +742,138 @@ const buildEcosystemCluster = (nodes, index) => {
|
|
|
738
742
|
x: sum.x / count,
|
|
739
743
|
y: sum.y / count,
|
|
740
744
|
count,
|
|
745
|
+
nodeIds: nodes.map(node => node.id),
|
|
741
746
|
representative,
|
|
742
747
|
label: ecosystemKeyForNode(nodes[0] ?? representative ?? { path: '' })
|
|
743
748
|
}
|
|
744
749
|
}
|
|
745
750
|
|
|
746
|
-
const
|
|
747
|
-
if (nodes.length === 0) {
|
|
748
|
-
return { clusters: [], edges: [] }
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
|
|
751
|
+
const buildEcosystemLevel = (sortedNodes, size, parentLookup) => {
|
|
752
752
|
const clusters = []
|
|
753
753
|
const clusterByNodeId = new Map()
|
|
754
754
|
|
|
755
|
-
for (let offset = 0; offset < sortedNodes.length; offset +=
|
|
756
|
-
const clusterNodes = sortedNodes.slice(offset, offset +
|
|
757
|
-
const
|
|
755
|
+
for (let offset = 0; offset < sortedNodes.length; offset += size) {
|
|
756
|
+
const clusterNodes = sortedNodes.slice(offset, offset + size)
|
|
757
|
+
const parentCluster = parentLookup?.get(clusterNodes[0]?.id)
|
|
758
|
+
const cluster = {
|
|
759
|
+
...buildEcosystemCluster(clusterNodes, clusters.length),
|
|
760
|
+
id: 'ecosystem-' + size + '-' + clusters.length,
|
|
761
|
+
size,
|
|
762
|
+
parentId: parentCluster?.id ?? null
|
|
763
|
+
}
|
|
758
764
|
clusters.push(cluster)
|
|
759
765
|
for (let index = 0; index < clusterNodes.length; index += 1) {
|
|
760
766
|
clusterByNodeId.set(clusterNodes[index].id, cluster)
|
|
761
767
|
}
|
|
762
768
|
}
|
|
763
769
|
|
|
770
|
+
return { clusters, clusterByNodeId }
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
const buildEcosystemGraph = (nodes) => {
|
|
774
|
+
if (nodes.length === 0) {
|
|
775
|
+
return { clusters: [], clustersBySize: new Map() }
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const sortedNodes = [...nodes].sort(compareNodesForEcosystem)
|
|
779
|
+
const clustersBySize = new Map()
|
|
780
|
+
let parentLookup = null
|
|
781
|
+
|
|
782
|
+
for (let index = 0; index < ecosystemGroupSizes.length; index += 1) {
|
|
783
|
+
const size = ecosystemGroupSizes[index]
|
|
784
|
+
const level = buildEcosystemLevel(sortedNodes, size, parentLookup)
|
|
785
|
+
clustersBySize.set(size, level.clusters)
|
|
786
|
+
parentLookup = level.clusterByNodeId
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
clusters: clustersBySize.get(ecosystemGroupSize) ?? [],
|
|
791
|
+
clustersBySize
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
const isClusterInViewport = (cluster, viewport) =>
|
|
796
|
+
cluster.x >= viewport.minX &&
|
|
797
|
+
cluster.x <= viewport.maxX &&
|
|
798
|
+
cluster.y >= viewport.minY &&
|
|
799
|
+
cluster.y <= viewport.maxY
|
|
800
|
+
|
|
801
|
+
const filterEcosystemClustersByViewport = (clusters, viewport) => {
|
|
802
|
+
const visible = clusters.filter(cluster => isClusterInViewport(cluster, viewport))
|
|
803
|
+
return visible.length > 0 ? visible : [...clusters]
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const ecosystemFocusPoint = () => {
|
|
807
|
+
const now = performance.now()
|
|
808
|
+
if (now - state.lastZoomFocus.at <= 1800) {
|
|
809
|
+
return { x: state.lastZoomFocus.x, y: state.lastZoomFocus.y }
|
|
810
|
+
}
|
|
811
|
+
return viewportCenterWorldPoint()
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
const nearestEcosystemParentIds = (clusters, focusPoint, limit) =>
|
|
815
|
+
clusters
|
|
816
|
+
.map(cluster => ({
|
|
817
|
+
cluster,
|
|
818
|
+
distance: Math.hypot(cluster.x - focusPoint.x, cluster.y - focusPoint.y)
|
|
819
|
+
}))
|
|
820
|
+
.sort((left, right) => left.distance - right.distance)
|
|
821
|
+
.slice(0, limit)
|
|
822
|
+
.map(item => item.cluster.id)
|
|
823
|
+
|
|
824
|
+
const ecosystemPlanForScale = scale => {
|
|
825
|
+
if (scale <= ecosystemMicroScaleThreshold) {
|
|
826
|
+
return { baseSize: 1000, childSize: null }
|
|
827
|
+
}
|
|
828
|
+
if (scale <= ecosystemSubgraphScaleThreshold) {
|
|
829
|
+
return { baseSize: 1000, childSize: 250 }
|
|
830
|
+
}
|
|
831
|
+
return { baseSize: 250, childSize: 60 }
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
const selectHierarchicalEcosystemClusters = viewport => {
|
|
835
|
+
const plan = ecosystemPlanForScale(state.transform.scale)
|
|
836
|
+
const baseClusters = state.ecosystemClustersBySize.get(plan.baseSize) ?? state.ecosystemClusters
|
|
837
|
+
const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
|
|
838
|
+
|
|
839
|
+
if (!plan.childSize) {
|
|
840
|
+
return visibleBaseClusters
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const focusPoint = ecosystemFocusPoint()
|
|
844
|
+
const expandedParentIds = new Set(nearestEcosystemParentIds(
|
|
845
|
+
visibleBaseClusters,
|
|
846
|
+
focusPoint,
|
|
847
|
+
ecosystemFocusedParentLimit
|
|
848
|
+
))
|
|
849
|
+
const childClusters = state.ecosystemClustersBySize.get(plan.childSize) ?? []
|
|
850
|
+
const visibleChildClusters = childClusters.filter(cluster =>
|
|
851
|
+
expandedParentIds.has(cluster.parentId) &&
|
|
852
|
+
isClusterInViewport(cluster, viewport)
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
if (visibleChildClusters.length === 0) {
|
|
856
|
+
return visibleBaseClusters
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return [
|
|
860
|
+
...visibleBaseClusters.filter(cluster => !expandedParentIds.has(cluster.id)),
|
|
861
|
+
...visibleChildClusters
|
|
862
|
+
]
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const ecosystemEdgesForClusters = clusters => {
|
|
866
|
+
const clusterByNodeId = new Map()
|
|
867
|
+
for (let clusterIndex = 0; clusterIndex < clusters.length; clusterIndex += 1) {
|
|
868
|
+
const cluster = clusters[clusterIndex]
|
|
869
|
+
for (let nodeIndex = 0; nodeIndex < cluster.nodeIds.length; nodeIndex += 1) {
|
|
870
|
+
clusterByNodeId.set(cluster.nodeIds[nodeIndex], cluster)
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
764
874
|
const edgeByClusterPair = new Map()
|
|
765
|
-
for (let index = 0; index <
|
|
766
|
-
const edge =
|
|
875
|
+
for (let index = 0; index < state.visibleEdges.length; index += 1) {
|
|
876
|
+
const edge = state.visibleEdges[index]
|
|
767
877
|
const sourceCluster = clusterByNodeId.get(edge.source)
|
|
768
878
|
const targetCluster = clusterByNodeId.get(edge.target)
|
|
769
879
|
if (!sourceCluster || !targetCluster || sourceCluster.id === targetCluster.id) {
|
|
@@ -788,27 +898,9 @@ const buildEcosystemGraph = (nodes, edges) => {
|
|
|
788
898
|
})
|
|
789
899
|
}
|
|
790
900
|
|
|
791
|
-
|
|
901
|
+
return Array.from(edgeByClusterPair.values())
|
|
792
902
|
.sort((left, right) => right.weight - left.weight)
|
|
793
903
|
.slice(0, ecosystemClusterEdgeLimit)
|
|
794
|
-
|
|
795
|
-
return { clusters, edges: aggregatedEdges }
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
const filterEcosystemClustersByViewport = viewport =>
|
|
799
|
-
state.ecosystemClusters.filter((cluster) =>
|
|
800
|
-
cluster.x >= viewport.minX &&
|
|
801
|
-
cluster.x <= viewport.maxX &&
|
|
802
|
-
cluster.y >= viewport.minY &&
|
|
803
|
-
cluster.y <= viewport.maxY
|
|
804
|
-
)
|
|
805
|
-
|
|
806
|
-
const ecosystemEdgesForClusters = clusters => {
|
|
807
|
-
const clusterIds = new Set(clusters.map(cluster => cluster.id))
|
|
808
|
-
return state.ecosystemEdges.filter(edge =>
|
|
809
|
-
clusterIds.has(edge.sourceCluster.id) &&
|
|
810
|
-
clusterIds.has(edge.targetCluster.id)
|
|
811
|
-
)
|
|
812
904
|
}
|
|
813
905
|
|
|
814
906
|
const edgeBudgetForCurrentFrame = () => {
|
|
@@ -2407,11 +2499,8 @@ const computeRenderVisibility = () => {
|
|
|
2407
2499
|
}
|
|
2408
2500
|
|
|
2409
2501
|
if (state.transform.scale <= ecosystemClusterScaleThreshold && state.ecosystemClusters.length > 0) {
|
|
2410
|
-
const
|
|
2502
|
+
const clusters = selectHierarchicalEcosystemClusters(viewport)
|
|
2411
2503
|
.sort((left, right) => right.count - left.count)
|
|
2412
|
-
const clusters = viewportClusters.length > 0
|
|
2413
|
-
? viewportClusters
|
|
2414
|
-
: state.ecosystemClusters
|
|
2415
2504
|
state.renderClusters = clusters
|
|
2416
2505
|
state.renderClusterEdges = ecosystemEdgesForClusters(clusters)
|
|
2417
2506
|
state.renderNodes = []
|
package/package.json
CHANGED