@andespindola/brainlink 0.1.0-beta.75 → 0.1.0-beta.77
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 +1 -1
- package/dist/application/frontend/client-js.js +147 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -594,7 +594,7 @@ The graph UI shows:
|
|
|
594
594
|
- double-click on canvas zooms in at cursor position
|
|
595
595
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
596
596
|
- large-graph rendering safeguards (edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
597
|
-
- massive-graph LOD progression: very low zoom uses spatial overview sampling to preserve whole-vault shape
|
|
597
|
+
- massive-graph LOD progression: very low zoom uses spatial overview sampling plus hub-neighborhood edge previews to preserve whole-vault shape and orientation, then progressively reveals nodes and edges as zoom increases
|
|
598
598
|
|
|
599
599
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
600
600
|
|
|
@@ -579,6 +579,14 @@ const nodeBudgetForScale = (scale) => {
|
|
|
579
579
|
return renderNodeBudget
|
|
580
580
|
}
|
|
581
581
|
|
|
582
|
+
const massiveLowZoomNodeBudgetForScale = (scale) => {
|
|
583
|
+
if (scale < 0.004) return 780
|
|
584
|
+
if (scale < 0.01) return 860
|
|
585
|
+
if (scale < 0.02) return 900
|
|
586
|
+
if (scale < 0.035) return 900
|
|
587
|
+
return renderNodeBudget
|
|
588
|
+
}
|
|
589
|
+
|
|
582
590
|
const layerFocusForScale = (scale) => {
|
|
583
591
|
const normalized = Math.max(0, Math.min(1, (scale - 0.06) / 0.94))
|
|
584
592
|
const shellCenter = Math.max(0.08, 0.96 - normalized * 0.86)
|
|
@@ -1110,6 +1118,89 @@ const enrichSampleWithNeighbors = (nodes) => {
|
|
|
1110
1118
|
}
|
|
1111
1119
|
}
|
|
1112
1120
|
|
|
1121
|
+
const includeHubPreviewNeighborhood = (nodes, limit) => {
|
|
1122
|
+
const hub = state.primaryHub
|
|
1123
|
+
if (!hub) {
|
|
1124
|
+
return nodes
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const maxNodes = Math.max(1, Math.min(renderNodeBudget, limit))
|
|
1128
|
+
const merged = [...nodes]
|
|
1129
|
+
const ids = new Set(merged.map((node) => node.id))
|
|
1130
|
+
const protectedIds = new Set()
|
|
1131
|
+
|
|
1132
|
+
if (!ids.has(hub.id)) {
|
|
1133
|
+
if (merged.length < maxNodes) {
|
|
1134
|
+
merged.push(hub)
|
|
1135
|
+
ids.add(hub.id)
|
|
1136
|
+
} else {
|
|
1137
|
+
const replaceIndex = merged.findIndex((node) => node.id !== hub.id)
|
|
1138
|
+
if (replaceIndex >= 0) {
|
|
1139
|
+
ids.delete(merged[replaceIndex].id)
|
|
1140
|
+
merged[replaceIndex] = hub
|
|
1141
|
+
ids.add(hub.id)
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
protectedIds.add(hub.id)
|
|
1146
|
+
|
|
1147
|
+
const hubEdges = [...(state.visibleEdgeByNode.get(hub.id) ?? [])]
|
|
1148
|
+
.filter((edge) => edge.target && (edge.source === hub.id || edge.target === hub.id))
|
|
1149
|
+
.sort((left, right) => {
|
|
1150
|
+
const byWeight = edgeWeight(right) - edgeWeight(left)
|
|
1151
|
+
if (byWeight !== 0) return byWeight
|
|
1152
|
+
|
|
1153
|
+
const leftOtherId = left.source === hub.id ? left.target : left.source
|
|
1154
|
+
const rightOtherId = right.source === hub.id ? right.target : right.source
|
|
1155
|
+
const leftDegree = state.nodeDegrees.get(leftOtherId ?? '') ?? 0
|
|
1156
|
+
const rightDegree = state.nodeDegrees.get(rightOtherId ?? '') ?? 0
|
|
1157
|
+
if (leftDegree !== rightDegree) return rightDegree - leftDegree
|
|
1158
|
+
|
|
1159
|
+
return edgeIdentityKey(left).localeCompare(edgeIdentityKey(right))
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
for (let index = 0; index < hubEdges.length && merged.length < maxNodes; index += 1) {
|
|
1163
|
+
const edge = hubEdges[index]
|
|
1164
|
+
const otherId = edge.source === hub.id ? edge.target : edge.source
|
|
1165
|
+
if (!otherId || ids.has(otherId)) {
|
|
1166
|
+
continue
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
const otherNode = state.nodeById.get(otherId)
|
|
1170
|
+
if (!otherNode) {
|
|
1171
|
+
continue
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
if (merged.length < maxNodes) {
|
|
1175
|
+
ids.add(otherId)
|
|
1176
|
+
merged.push(otherNode)
|
|
1177
|
+
protectedIds.add(otherId)
|
|
1178
|
+
continue
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
const replaceIndex = (() => {
|
|
1182
|
+
for (let cursor = merged.length - 1; cursor >= 0; cursor -= 1) {
|
|
1183
|
+
const candidateId = merged[cursor]?.id
|
|
1184
|
+
if (candidateId && !protectedIds.has(candidateId)) {
|
|
1185
|
+
return cursor
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return -1
|
|
1189
|
+
})()
|
|
1190
|
+
if (replaceIndex >= 0) {
|
|
1191
|
+
const replacedId = merged[replaceIndex]?.id
|
|
1192
|
+
if (replacedId) {
|
|
1193
|
+
ids.delete(replacedId)
|
|
1194
|
+
}
|
|
1195
|
+
merged[replaceIndex] = otherNode
|
|
1196
|
+
ids.add(otherId)
|
|
1197
|
+
protectedIds.add(otherId)
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
return merged
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1113
1204
|
const ensureHubNodesInRenderedSet = (nodes) => {
|
|
1114
1205
|
if (nodes.length === 0) {
|
|
1115
1206
|
return nodes
|
|
@@ -1225,7 +1316,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
|
1225
1316
|
if (nodeCount <= 600) return { min: 0.12, max: 0.72 }
|
|
1226
1317
|
if (nodeCount <= 2000) return { min: 0.08, max: 0.52 }
|
|
1227
1318
|
if (nodeCount <= 6000) return { min: 0.06, max: 0.32 }
|
|
1228
|
-
return { min: 0.
|
|
1319
|
+
return { min: 0.0012, max: 0.24 }
|
|
1229
1320
|
}
|
|
1230
1321
|
|
|
1231
1322
|
const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
|
|
@@ -1824,16 +1915,26 @@ const computeRenderVisibility = () => {
|
|
|
1824
1915
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
1825
1916
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
1826
1917
|
if (state.transform.scale <= massiveOverviewClusterScaleThreshold) {
|
|
1918
|
+
const overviewLimit = Math.min(renderNodeBudget, massiveLowZoomNodeBudgetForScale(state.transform.scale))
|
|
1827
1919
|
const overviewClusters = filterOverviewClustersByViewport(viewport)
|
|
1828
1920
|
.sort((left, right) => right.count - left.count)
|
|
1829
|
-
.slice(0,
|
|
1921
|
+
.slice(0, overviewLimit)
|
|
1830
1922
|
if (overviewClusters.length > 0) {
|
|
1831
|
-
|
|
1832
|
-
state.renderNodes = representativeNodesFromClusters(
|
|
1923
|
+
const overviewNodes = representativeNodesFromClusters(
|
|
1833
1924
|
overviewClusters,
|
|
1834
|
-
|
|
1925
|
+
overviewLimit
|
|
1926
|
+
)
|
|
1927
|
+
const anchoredNodes = includeHubPreviewNeighborhood(
|
|
1928
|
+
overviewNodes,
|
|
1929
|
+
Math.min(renderNodeBudget, overviewLimit)
|
|
1835
1930
|
)
|
|
1836
|
-
|
|
1931
|
+
const enriched = enrichSampleWithNeighbors(anchoredNodes)
|
|
1932
|
+
const previewNodes = ensureHubNodesInRenderedSet(enriched.nodes)
|
|
1933
|
+
const previewIds = new Set(previewNodes.map((node) => node.id))
|
|
1934
|
+
const previewEdges = collectVisibleEdgesForNodes(previewIds)
|
|
1935
|
+
state.renderClusters = []
|
|
1936
|
+
state.renderNodes = previewNodes
|
|
1937
|
+
state.renderEdges = previewEdges
|
|
1837
1938
|
return
|
|
1838
1939
|
}
|
|
1839
1940
|
}
|
|
@@ -1863,9 +1964,15 @@ const computeRenderVisibility = () => {
|
|
|
1863
1964
|
sampledRaw,
|
|
1864
1965
|
Math.min(sampleLimit, renderNodeBudget)
|
|
1865
1966
|
)
|
|
1866
|
-
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
1867
|
-
let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
|
|
1868
1967
|
let sampledNodes = ensureHubNodesInRenderedSet(sampled)
|
|
1968
|
+
if (state.transform.scale < 0.035) {
|
|
1969
|
+
sampledNodes = includeHubPreviewNeighborhood(
|
|
1970
|
+
sampledNodes,
|
|
1971
|
+
Math.min(renderNodeBudget, sampleLimit + 160)
|
|
1972
|
+
)
|
|
1973
|
+
}
|
|
1974
|
+
const sampledIds = new Set(sampledNodes.map((node) => node.id))
|
|
1975
|
+
let sampledEdges = collectVisibleEdgesForNodes(sampledIds)
|
|
1869
1976
|
|
|
1870
1977
|
if (state.transform.scale >= 0.035 && sampledEdges.length === 0) {
|
|
1871
1978
|
const enriched = enrichSampleWithNeighbors(sampledNodes)
|
|
@@ -2035,13 +2142,15 @@ const render = now => {
|
|
|
2035
2142
|
state.offscreenFrameCount = 0
|
|
2036
2143
|
}
|
|
2037
2144
|
const minimumEdgeScale =
|
|
2038
|
-
state.
|
|
2039
|
-
? 0
|
|
2040
|
-
: state.renderNodes.length >
|
|
2041
|
-
? 0.
|
|
2042
|
-
: state.renderNodes.length >
|
|
2043
|
-
? 0.
|
|
2044
|
-
:
|
|
2145
|
+
state.nodes.length > massiveGraphNodeThreshold
|
|
2146
|
+
? 0
|
|
2147
|
+
: state.renderNodes.length > 1300
|
|
2148
|
+
? 0.12
|
|
2149
|
+
: state.renderNodes.length > 900
|
|
2150
|
+
? 0.085
|
|
2151
|
+
: state.renderNodes.length > 500
|
|
2152
|
+
? 0.05
|
|
2153
|
+
: 0
|
|
2045
2154
|
const drawEdges =
|
|
2046
2155
|
state.renderClusters.length === 0 &&
|
|
2047
2156
|
state.transform.scale >= minimumEdgeScale
|
|
@@ -2224,8 +2333,28 @@ const selectNodeById = id => {
|
|
|
2224
2333
|
}
|
|
2225
2334
|
|
|
2226
2335
|
const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
2336
|
+
const resolveZoomFactor = () => {
|
|
2337
|
+
if (state.nodes.length <= massiveGraphNodeThreshold) {
|
|
2338
|
+
return factor
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
const scale = state.transform.scale
|
|
2342
|
+
if (factor > 1) {
|
|
2343
|
+
if (scale < 0.006) return Math.max(factor, 1.48)
|
|
2344
|
+
if (scale < 0.02) return Math.max(factor, 1.34)
|
|
2345
|
+
if (scale < 0.08) return Math.max(factor, 1.22)
|
|
2346
|
+
return factor
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2349
|
+
if (scale < 0.006) return Math.min(factor, 0.68)
|
|
2350
|
+
if (scale < 0.02) return Math.min(factor, 0.78)
|
|
2351
|
+
if (scale < 0.08) return Math.min(factor, 0.86)
|
|
2352
|
+
return factor
|
|
2353
|
+
}
|
|
2354
|
+
|
|
2227
2355
|
state.lastManualZoomAt = performance.now()
|
|
2228
|
-
const
|
|
2356
|
+
const effectiveFactor = resolveZoomFactor()
|
|
2357
|
+
const nextScale = clampScale(state.transform.scale * effectiveFactor)
|
|
2229
2358
|
if (nextScale === state.transform.scale) {
|
|
2230
2359
|
return
|
|
2231
2360
|
}
|
|
@@ -2302,11 +2431,11 @@ const bindEvents = () => {
|
|
|
2302
2431
|
})
|
|
2303
2432
|
elements.zoomIn.addEventListener('click', () => {
|
|
2304
2433
|
const rect = canvas.getBoundingClientRect()
|
|
2305
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14)
|
|
2434
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 1.14, 'button')
|
|
2306
2435
|
})
|
|
2307
2436
|
elements.zoomOut.addEventListener('click', () => {
|
|
2308
2437
|
const rect = canvas.getBoundingClientRect()
|
|
2309
|
-
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88)
|
|
2438
|
+
zoomAtPoint(Math.max(rect.width, 320) / 2, Math.max(rect.height, 320) / 2, 0.88, 'button')
|
|
2310
2439
|
})
|
|
2311
2440
|
if (elements.fit) {
|
|
2312
2441
|
elements.fit.addEventListener('click', () => {
|
package/package.json
CHANGED