@andespindola/brainlink 0.1.0-beta.79 → 0.1.0-beta.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/application/frontend/client-js.js +151 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -82,6 +82,7 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
82
82
|
- Local HTTP API.
|
|
83
83
|
- Realtime graph UI with agent selector and colored knowledge groups.
|
|
84
84
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
85
|
+
- Large graph canvas rendering batches regular nodes and edges to reduce per-frame draw calls while keeping selected and hovered items highlighted.
|
|
85
86
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
86
87
|
- 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).
|
|
87
88
|
- Zoomed-out graph LOD clusters dense regions and progressively expands the focused viewport as zoom increases, including very large vaults.
|
|
@@ -594,7 +595,7 @@ The graph UI shows:
|
|
|
594
595
|
- keyboard shortcuts: `+` zoom in, `-` zoom out, `0` reset fit
|
|
595
596
|
- double-click on canvas zooms in at cursor position
|
|
596
597
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
597
|
-
- large-graph rendering safeguards (edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
598
|
+
- large-graph rendering safeguards (batched canvas drawing, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
598
599
|
- 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 raises the focused node budget as zoom increases so dense local areas keep nearby notes and links visible
|
|
599
600
|
|
|
600
601
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
@@ -914,7 +914,58 @@ const drawGraphEdge = (edge) => {
|
|
|
914
914
|
ctx.stroke()
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
const drawEdgeBatch = (edges, options) => {
|
|
918
|
+
if (edges.length === 0) {
|
|
919
|
+
return
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
ctx.beginPath()
|
|
923
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
924
|
+
const edge = edges[index]
|
|
925
|
+
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
926
|
+
ctx.lineTo(edge.targetNode.x, edge.targetNode.y)
|
|
927
|
+
}
|
|
928
|
+
ctx.strokeStyle = options.strokeStyle
|
|
929
|
+
ctx.lineWidth = options.lineWidth
|
|
930
|
+
ctx.stroke()
|
|
931
|
+
}
|
|
932
|
+
|
|
917
933
|
const drawGraphEdges = () => {
|
|
934
|
+
if (state.nodes.length > largeGraphNodeThreshold) {
|
|
935
|
+
const regularEdges = []
|
|
936
|
+
const inferredEdges = []
|
|
937
|
+
const selectedEdges = []
|
|
938
|
+
|
|
939
|
+
for (let index = 0; index < state.renderEdges.length; index += 1) {
|
|
940
|
+
const edge = state.renderEdges[index]
|
|
941
|
+
const isSelected = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
942
|
+
if (isSelected) {
|
|
943
|
+
selectedEdges.push(edge)
|
|
944
|
+
} else if (edge.inferred) {
|
|
945
|
+
inferredEdges.push(edge)
|
|
946
|
+
} else {
|
|
947
|
+
regularEdges.push(edge)
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
const scale = state.transform.scale
|
|
952
|
+
const regularOpacity = edgeOpacityForScale({ inferred: false }, scale)
|
|
953
|
+
const inferredOpacity = edgeOpacityForScale({ inferred: true }, scale)
|
|
954
|
+
drawEdgeBatch(regularEdges, {
|
|
955
|
+
strokeStyle: 'rgba(153, 165, 181, ' + regularOpacity + ')',
|
|
956
|
+
lineWidth: 1.05
|
|
957
|
+
})
|
|
958
|
+
drawEdgeBatch(inferredEdges, {
|
|
959
|
+
strokeStyle: 'rgba(203, 213, 225, ' + inferredOpacity + ')',
|
|
960
|
+
lineWidth: 0.84
|
|
961
|
+
})
|
|
962
|
+
|
|
963
|
+
for (let index = 0; index < selectedEdges.length; index += 1) {
|
|
964
|
+
drawGraphEdge(selectedEdges[index])
|
|
965
|
+
}
|
|
966
|
+
return
|
|
967
|
+
}
|
|
968
|
+
|
|
918
969
|
const selectedEdges = []
|
|
919
970
|
const regularEdges = []
|
|
920
971
|
for (let index = 0; index < state.renderEdges.length; index += 1) {
|
|
@@ -935,6 +986,105 @@ const drawGraphEdges = () => {
|
|
|
935
986
|
}
|
|
936
987
|
}
|
|
937
988
|
|
|
989
|
+
const shouldDrawNodeLabels = (node, isSelected, isHovered) =>
|
|
990
|
+
isSelected ||
|
|
991
|
+
isHovered ||
|
|
992
|
+
(state.nodes.length > largeGraphNodeThreshold && state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) ||
|
|
993
|
+
(state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
|
|
994
|
+
|
|
995
|
+
const drawSingleNode = (node, options = { drawLabel: true }) => {
|
|
996
|
+
const radius = nodeRadius(node)
|
|
997
|
+
const isSelected = state.selected?.id === node.id
|
|
998
|
+
const isHovered = state.hovered?.id === node.id
|
|
999
|
+
ctx.beginPath()
|
|
1000
|
+
ctx.arc(node.x, node.y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
|
|
1001
|
+
ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
|
|
1002
|
+
ctx.fill()
|
|
1003
|
+
ctx.beginPath()
|
|
1004
|
+
ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
|
|
1005
|
+
ctx.fillStyle = isSelected ? graphTheme.nodeSelected : isHovered ? graphTheme.nodeHover : graphTheme.node
|
|
1006
|
+
ctx.fill()
|
|
1007
|
+
ctx.lineWidth = isSelected ? 2.6 : 1.5
|
|
1008
|
+
ctx.strokeStyle = isSelected ? graphTheme.nodeStrokeActive : graphTheme.nodeStroke
|
|
1009
|
+
ctx.stroke()
|
|
1010
|
+
|
|
1011
|
+
if (options.drawLabel && shouldDrawNodeLabels(node, isSelected, isHovered)) {
|
|
1012
|
+
ctx.fillStyle = graphTheme.label
|
|
1013
|
+
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
1014
|
+
ctx.textAlign = 'center'
|
|
1015
|
+
ctx.textBaseline = 'top'
|
|
1016
|
+
ctx.fillText(node.title.slice(0, 34), node.x, node.y + radius + 8)
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const drawNodeBatch = (nodes) => {
|
|
1021
|
+
if (nodes.length === 0) {
|
|
1022
|
+
return
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const drawHalos = state.renderNodes.length <= 1200 || state.transform.scale >= 0.45
|
|
1026
|
+
if (drawHalos) {
|
|
1027
|
+
ctx.beginPath()
|
|
1028
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1029
|
+
const node = nodes[index]
|
|
1030
|
+
ctx.moveTo(node.x + nodeRadius(node) + 3, node.y)
|
|
1031
|
+
ctx.arc(node.x, node.y, nodeRadius(node) + 3, 0, Math.PI * 2)
|
|
1032
|
+
}
|
|
1033
|
+
ctx.fillStyle = graphTheme.nodeHalo
|
|
1034
|
+
ctx.fill()
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
ctx.beginPath()
|
|
1038
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1039
|
+
const node = nodes[index]
|
|
1040
|
+
const radius = nodeRadius(node)
|
|
1041
|
+
ctx.moveTo(node.x + radius, node.y)
|
|
1042
|
+
ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
|
|
1043
|
+
}
|
|
1044
|
+
ctx.fillStyle = graphTheme.node
|
|
1045
|
+
ctx.fill()
|
|
1046
|
+
ctx.lineWidth = 1.25
|
|
1047
|
+
ctx.strokeStyle = graphTheme.nodeStroke
|
|
1048
|
+
ctx.stroke()
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
const drawGraphNodes = () => {
|
|
1052
|
+
if (state.nodes.length <= largeGraphNodeThreshold) {
|
|
1053
|
+
state.renderNodes.forEach(node => drawSingleNode(node))
|
|
1054
|
+
return
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
const regularNodes = []
|
|
1058
|
+
const priorityNodes = []
|
|
1059
|
+
|
|
1060
|
+
for (let index = 0; index < state.renderNodes.length; index += 1) {
|
|
1061
|
+
const node = state.renderNodes[index]
|
|
1062
|
+
const isPriority =
|
|
1063
|
+
state.selected?.id === node.id ||
|
|
1064
|
+
state.hovered?.id === node.id
|
|
1065
|
+
if (isPriority) {
|
|
1066
|
+
priorityNodes.push(node)
|
|
1067
|
+
} else {
|
|
1068
|
+
regularNodes.push(node)
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
drawNodeBatch(regularNodes)
|
|
1073
|
+
|
|
1074
|
+
if (state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) {
|
|
1075
|
+
ctx.fillStyle = graphTheme.label
|
|
1076
|
+
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
1077
|
+
ctx.textAlign = 'center'
|
|
1078
|
+
ctx.textBaseline = 'top'
|
|
1079
|
+
for (let index = 0; index < regularNodes.length; index += 1) {
|
|
1080
|
+
const node = regularNodes[index]
|
|
1081
|
+
ctx.fillText(node.title.slice(0, 34), node.x, node.y + nodeRadius(node) + 8)
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
priorityNodes.forEach(node => drawSingleNode(node))
|
|
1086
|
+
}
|
|
1087
|
+
|
|
938
1088
|
const edgePairKey = (source, target) =>
|
|
939
1089
|
source < target ? source + '|' + target : target + '|' + source
|
|
940
1090
|
|
|
@@ -2203,35 +2353,7 @@ const render = now => {
|
|
|
2203
2353
|
// Keep cluster markers minimal and faster to draw on large graphs.
|
|
2204
2354
|
})
|
|
2205
2355
|
} else {
|
|
2206
|
-
|
|
2207
|
-
const radius = nodeRadius(node)
|
|
2208
|
-
const isSelected = state.selected?.id === node.id
|
|
2209
|
-
const isHovered = state.hovered?.id === node.id
|
|
2210
|
-
ctx.beginPath()
|
|
2211
|
-
ctx.arc(node.x, node.y, radius + (isSelected ? 7 : isHovered ? 4 : 0), 0, Math.PI * 2)
|
|
2212
|
-
ctx.fillStyle = isSelected || isHovered ? graphTheme.nodeHaloActive : graphTheme.nodeHalo
|
|
2213
|
-
ctx.fill()
|
|
2214
|
-
ctx.beginPath()
|
|
2215
|
-
ctx.arc(node.x, node.y, radius, 0, Math.PI * 2)
|
|
2216
|
-
ctx.fillStyle = isSelected ? graphTheme.nodeSelected : isHovered ? graphTheme.nodeHover : graphTheme.node
|
|
2217
|
-
ctx.fill()
|
|
2218
|
-
ctx.lineWidth = isSelected ? 2.6 : 1.5
|
|
2219
|
-
ctx.strokeStyle = isSelected ? graphTheme.nodeStrokeActive : graphTheme.nodeStroke
|
|
2220
|
-
ctx.stroke()
|
|
2221
|
-
|
|
2222
|
-
const shouldDrawLabels =
|
|
2223
|
-
isSelected ||
|
|
2224
|
-
isHovered ||
|
|
2225
|
-
(state.nodes.length > largeGraphNodeThreshold && state.transform.scale >= 0.62 && state.renderNodes.length <= 1200) ||
|
|
2226
|
-
(state.nodes.length <= largeGraphNodeThreshold && (state.transform.scale > 1.18 || state.nodes.length <= 25))
|
|
2227
|
-
if (shouldDrawLabels) {
|
|
2228
|
-
ctx.fillStyle = graphTheme.label
|
|
2229
|
-
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
2230
|
-
ctx.textAlign = 'center'
|
|
2231
|
-
ctx.textBaseline = 'top'
|
|
2232
|
-
ctx.fillText(node.title.slice(0, 34), node.x, node.y + radius + 8)
|
|
2233
|
-
}
|
|
2234
|
-
})
|
|
2356
|
+
drawGraphNodes()
|
|
2235
2357
|
}
|
|
2236
2358
|
|
|
2237
2359
|
ctx.restore()
|
package/package.json
CHANGED