@andespindola/brainlink 0.1.0-beta.123 → 0.1.0-beta.124
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
|
@@ -606,7 +606,7 @@ The graph UI shows:
|
|
|
606
606
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
607
607
|
- 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
|
|
608
608
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
609
|
-
- large graph LOD keeps a recursive graph-of-graphs mesh model: zoom-out shows one level capped at 1000 group nodes, zoom-in expands the framed node into its child graph capped at 1000 nodes, micro view renders only that focused subgraph, and zoom-out restores sibling groups
|
|
609
|
+
- large graph LOD keeps a recursive graph-of-graphs mesh model: zoom-out shows one level capped at 1000 group nodes, zoom-in expands the framed node into its child graph capped at 1000 nodes, micro view renders only that focused subgraph in a local frame anchored to the group node, and zoom-out restores sibling groups
|
|
610
610
|
|
|
611
611
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
612
612
|
|
|
@@ -11,7 +11,6 @@ const massiveOverviewScaleThreshold = 0.065
|
|
|
11
11
|
const massiveSegmentedScaleThreshold = 0.45
|
|
12
12
|
const massiveSegmentRepresentativeBudget = 760
|
|
13
13
|
const massiveAutoFitMacroScale = 0.018
|
|
14
|
-
const hierarchyGroupScaleThreshold = 0.62
|
|
15
14
|
const hierarchyExpansionStartScale = 0.18
|
|
16
15
|
const hierarchyMicroEnterCoverage = 0.72
|
|
17
16
|
const hierarchyMicroExitCoverage = 0.52
|
|
@@ -1213,6 +1212,11 @@ const groupRenderRadius = group => {
|
|
|
1213
1212
|
return 10 + Math.min(Math.log2(childCount + 1) * 4.2, 22)
|
|
1214
1213
|
}
|
|
1215
1214
|
|
|
1215
|
+
const childGraphRenderRadius = group => {
|
|
1216
|
+
const childCount = Math.max(group.nodeIds.length, group.childGroupIds.length, 1)
|
|
1217
|
+
return Math.max(420, Math.min(1800, Math.sqrt(childCount) * 24))
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1216
1220
|
const createGroupRenderNode = group => ({
|
|
1217
1221
|
id: groupRenderNodeId(group),
|
|
1218
1222
|
groupId: group.id,
|
|
@@ -1229,13 +1233,55 @@ const createGroupRenderNode = group => ({
|
|
|
1229
1233
|
radius: groupRenderRadius(group)
|
|
1230
1234
|
})
|
|
1231
1235
|
|
|
1232
|
-
const
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1236
|
+
const localGraphBounds = nodes => {
|
|
1237
|
+
if (nodes.length === 0) {
|
|
1238
|
+
return { centerX: 0, centerY: 0, radius: 1 }
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
const bounds = nodes.reduce(
|
|
1242
|
+
(current, node) => ({
|
|
1243
|
+
minX: Math.min(current.minX, node.x),
|
|
1244
|
+
maxX: Math.max(current.maxX, node.x),
|
|
1245
|
+
minY: Math.min(current.minY, node.y),
|
|
1246
|
+
maxY: Math.max(current.maxY, node.y)
|
|
1247
|
+
}),
|
|
1248
|
+
{
|
|
1249
|
+
minX: Number.POSITIVE_INFINITY,
|
|
1250
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
1251
|
+
minY: Number.POSITIVE_INFINITY,
|
|
1252
|
+
maxY: Number.NEGATIVE_INFINITY
|
|
1253
|
+
}
|
|
1254
|
+
)
|
|
1255
|
+
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
1256
|
+
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
1257
|
+
const radius = nodes.reduce(
|
|
1258
|
+
(largest, node) => Math.max(largest, Math.hypot(node.x - centerX, node.y - centerY)),
|
|
1259
|
+
1
|
|
1260
|
+
)
|
|
1261
|
+
|
|
1262
|
+
return { centerX, centerY, radius }
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
const projectNodeIntoGroupGraph = (node, group, bounds) => {
|
|
1266
|
+
const targetRadius = childGraphRenderRadius(group)
|
|
1267
|
+
const scale = targetRadius / Math.max(bounds.radius, 1)
|
|
1268
|
+
|
|
1269
|
+
return {
|
|
1270
|
+
x: group.x + (node.x - bounds.centerX) * scale,
|
|
1271
|
+
y: group.y + (node.y - bounds.centerY) * scale
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
const interpolateNodeFromGroup = (node, group, bounds, progress) => {
|
|
1276
|
+
const projected = projectNodeIntoGroupGraph(node, group, bounds)
|
|
1277
|
+
return {
|
|
1278
|
+
...node,
|
|
1279
|
+
x: group.x + (projected.x - group.x) * progress,
|
|
1280
|
+
y: group.y + (projected.y - group.y) * progress,
|
|
1281
|
+
vx: 0,
|
|
1282
|
+
vy: 0
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1239
1285
|
|
|
1240
1286
|
const parentHierarchyGroups = () =>
|
|
1241
1287
|
state.groups.filter(group => group.parentId === null)
|
|
@@ -1257,7 +1303,7 @@ const groupViewportCoverage = (group, viewport) => {
|
|
|
1257
1303
|
const centerX = (viewport.minX + viewport.maxX) / 2
|
|
1258
1304
|
const centerY = (viewport.minY + viewport.maxY) / 2
|
|
1259
1305
|
const centerDistance = Math.hypot(group.x - centerX, group.y - centerY)
|
|
1260
|
-
const fitCoverage = Math.min(1, group
|
|
1306
|
+
const fitCoverage = Math.min(1, childGraphRenderRadius(group) / Math.max(viewportRadius, 1))
|
|
1261
1307
|
const centerCoverage = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1))
|
|
1262
1308
|
|
|
1263
1309
|
return fitCoverage * 0.72 + centerCoverage * 0.28
|
|
@@ -1352,7 +1398,7 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
|
|
|
1352
1398
|
}
|
|
1353
1399
|
|
|
1354
1400
|
const computeHierarchyRenderVisibility = (viewport) => {
|
|
1355
|
-
if (state.groups.length === 0 || state.visibleNodes.length <= 1000
|
|
1401
|
+
if (state.groups.length === 0 || state.visibleNodes.length <= 1000) {
|
|
1356
1402
|
state.hierarchyFocusGroupId = null
|
|
1357
1403
|
return false
|
|
1358
1404
|
}
|
|
@@ -1376,11 +1422,13 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1376
1422
|
}
|
|
1377
1423
|
|
|
1378
1424
|
const focusIds = new Set(focus.nodeIds)
|
|
1425
|
+
const rawChildNodes = state.visibleNodes.filter(node => focusIds.has(node.id))
|
|
1426
|
+
const childBounds = localGraphBounds(rawChildNodes)
|
|
1379
1427
|
const childLimit = Math.max(160, Math.min(zoomedMassiveRenderNodeBudget, Math.floor(renderNodeBudget * progress * 2.4)))
|
|
1380
1428
|
const childNodes = selectStableSampleNodes(
|
|
1381
|
-
|
|
1429
|
+
rawChildNodes,
|
|
1382
1430
|
childLimit
|
|
1383
|
-
).map(node => interpolateNodeFromGroup(node, focus, progress))
|
|
1431
|
+
).map(node => interpolateNodeFromGroup(node, focus, childBounds, progress))
|
|
1384
1432
|
const childIds = new Set(childNodes.map(node => node.id))
|
|
1385
1433
|
const childById = new Map(childNodes.map(node => [node.id, node]))
|
|
1386
1434
|
const isMicroView = progress >= 0.72
|
|
@@ -16,10 +16,14 @@ const groupCoverage = (group, input) => {
|
|
|
16
16
|
const centerX = input.x + input.width / 2;
|
|
17
17
|
const centerY = input.y + input.height / 2;
|
|
18
18
|
const centerDistance = Math.hypot(group.x - centerX, group.y - centerY);
|
|
19
|
-
const fitCoverage = Math.min(1, group
|
|
19
|
+
const fitCoverage = Math.min(1, childGraphRenderRadius(group) / Math.max(viewportRadius, 1));
|
|
20
20
|
const centerCoverage = 1 - Math.min(1, centerDistance / Math.max(viewportRadius, 1));
|
|
21
21
|
return fitCoverage * 0.72 + centerCoverage * 0.28;
|
|
22
22
|
};
|
|
23
|
+
const childGraphRenderRadius = (group) => {
|
|
24
|
+
const childCount = Math.max(group.nodeIds.length, group.childGroupIds.length, 1);
|
|
25
|
+
return Math.max(420, Math.min(1800, Math.sqrt(childCount) * 24));
|
|
26
|
+
};
|
|
23
27
|
const groupNode = (group) => [
|
|
24
28
|
`group:${group.id}`,
|
|
25
29
|
group.title,
|
|
@@ -38,6 +42,34 @@ const realNode = (node) => [
|
|
|
38
42
|
node.segment,
|
|
39
43
|
'node'
|
|
40
44
|
];
|
|
45
|
+
const graphBounds = (nodes) => {
|
|
46
|
+
if (nodes.length === 0) {
|
|
47
|
+
return { centerX: 0, centerY: 0, radius: 1 };
|
|
48
|
+
}
|
|
49
|
+
const bounds = nodes.reduce((current, node) => ({
|
|
50
|
+
minX: Math.min(current.minX, node.x),
|
|
51
|
+
maxX: Math.max(current.maxX, node.x),
|
|
52
|
+
minY: Math.min(current.minY, node.y),
|
|
53
|
+
maxY: Math.max(current.maxY, node.y)
|
|
54
|
+
}), {
|
|
55
|
+
minX: Number.POSITIVE_INFINITY,
|
|
56
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
57
|
+
minY: Number.POSITIVE_INFINITY,
|
|
58
|
+
maxY: Number.NEGATIVE_INFINITY
|
|
59
|
+
});
|
|
60
|
+
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
61
|
+
const centerY = (bounds.minY + bounds.maxY) / 2;
|
|
62
|
+
const radius = nodes.reduce((largest, node) => Math.max(largest, Math.hypot(node.x - centerX, node.y - centerY)), 1);
|
|
63
|
+
return { centerX, centerY, radius };
|
|
64
|
+
};
|
|
65
|
+
const projectNodeIntoGroupGraph = (node, group, bounds) => {
|
|
66
|
+
const scale = childGraphRenderRadius(group) / Math.max(bounds.radius, 1);
|
|
67
|
+
return {
|
|
68
|
+
...node,
|
|
69
|
+
x: group.x + (node.x - bounds.centerX) * scale,
|
|
70
|
+
y: group.y + (node.y - bounds.centerY) * scale
|
|
71
|
+
};
|
|
72
|
+
};
|
|
41
73
|
const descendants = (group, groupById) => group.nodeIds.length > 0
|
|
42
74
|
? group.nodeIds
|
|
43
75
|
: group.childGroupIds.flatMap((childId) => {
|
|
@@ -120,9 +152,18 @@ export const getGraphView = async (vaultPath, input) => {
|
|
|
120
152
|
.sort((left, right) => right.coverage - left.coverage)[0];
|
|
121
153
|
if (focused && input.scale >= macroScale && focused.coverage >= microCoverage) {
|
|
122
154
|
const nodeIds = new Set(focused.group.nodeIds);
|
|
123
|
-
const
|
|
124
|
-
.filter((node) => nodeIds.has(node.id)
|
|
155
|
+
const projectedNodes = layout.nodes
|
|
156
|
+
.filter((node) => nodeIds.has(node.id));
|
|
157
|
+
const bounds = graphBounds(projectedNodes);
|
|
158
|
+
const nodesInViewport = projectedNodes
|
|
159
|
+
.map((node) => projectNodeIntoGroupGraph(node, focused.group, bounds))
|
|
160
|
+
.filter((node) => inViewport(node, input))
|
|
125
161
|
.slice(0, nodeLimit);
|
|
162
|
+
const nodes = nodesInViewport.length > 0
|
|
163
|
+
? nodesInViewport
|
|
164
|
+
: projectedNodes
|
|
165
|
+
.slice(0, nodeLimit)
|
|
166
|
+
.map((node) => projectNodeIntoGroupGraph(node, focused.group, bounds));
|
|
126
167
|
const visibleNodeIds = new Set(nodes.map((node) => node.id));
|
|
127
168
|
const viewNodes = nodes.map(realNode);
|
|
128
169
|
return {
|
package/docs/AGENT_USAGE.md
CHANGED
|
@@ -607,7 +607,7 @@ Without `--vault`, the graph UI serves `$HOME/.brainlink/vault`.
|
|
|
607
607
|
|
|
608
608
|
The frontend includes an agent selector that shows only the agent id. Selecting an agent calls the same read APIs with `agent=<agent-id>` and renders that namespace instead of merging every agent into one graph.
|
|
609
609
|
|
|
610
|
-
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on canvas zooms in at cursor position. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open on click in a modal (tags, outgoing links, backlinks and Markdown content). Vaults above 1000 notes also expose stable hierarchy mesh groups of up to 1000 direct nodes, with recursive parent groups when a level exceeds 1000 groups; zoom-out renders those groups as normal mesh graph nodes, zoom-in expands a group only after it is framed in the viewport, and micro view renders only the focused subgraph until zoom-out restores sibling groups.
|
|
610
|
+
Graph navigation controls include zoom in, zoom out, fit visible nodes and reset-to-fit-all nodes. Mouse wheel zoom (including `cmd+scroll` and `ctrl+scroll`) is anchored to the cursor. Keyboard shortcuts are `+` (zoom in), `-` (zoom out) and `0` (reset fit). Double-click on canvas zooms in at cursor position. Totals for notes, links and tags stay visible as floating metrics under the Brainlink title, and node details open on click in a modal (tags, outgoing links, backlinks and Markdown content). Vaults above 1000 notes also expose stable hierarchy mesh groups of up to 1000 direct nodes, with recursive parent groups when a level exceeds 1000 groups; zoom-out renders those groups as normal mesh graph nodes, zoom-in expands a group only after it is framed in the viewport, and micro view renders only the focused subgraph in a local frame anchored to the group node until zoom-out restores sibling groups.
|
|
611
611
|
During graph filtering, Brainlink keeps hub context nodes visible (`Memory Hub`/`MOC`/high-degree fallback) so filtered views still show relationship anchors.
|
|
612
612
|
|
|
613
613
|
The command reindexes by default, then serves:
|
package/package.json
CHANGED