@andespindola/brainlink 0.1.0-beta.107 → 0.1.0-beta.109
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 -0
- package/dist/application/frontend/client-js.js +106 -42
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -601,6 +601,9 @@ The graph UI shows:
|
|
|
601
601
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
602
602
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
603
603
|
- 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
|
|
604
|
+
- hierarchical hot-path optimizations reduce per-frame allocations and repeated scans during layered cluster expansion and edge projection
|
|
605
|
+
- 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
|
|
606
|
+
- 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
|
|
604
607
|
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
605
608
|
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
606
609
|
- 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
|
|
@@ -35,6 +35,10 @@ const ecosystemDepthNear = 80
|
|
|
35
35
|
const ecosystemDepthFar = 2600
|
|
36
36
|
const ecosystemDepthPerspective = 620
|
|
37
37
|
const ecosystemDepthTiltY = 0.24
|
|
38
|
+
const ecosystemDepthYaw = 0.22
|
|
39
|
+
const ecosystemDepthPitch = 0.16
|
|
40
|
+
const ecosystemDepthRadialGain = 0.09
|
|
41
|
+
const ecosystemDepthOrbitalMaxOffset = 160
|
|
38
42
|
const ecosystemDepthMinScale = 0.24
|
|
39
43
|
const ecosystemDepthOpacityFloor = 0.2
|
|
40
44
|
const zoomRecoveryGuardMs = 4200
|
|
@@ -91,6 +95,8 @@ const state = {
|
|
|
91
95
|
ecosystemClustersBySize: new Map(),
|
|
92
96
|
ecosystemNodeClusterBySize: new Map(),
|
|
93
97
|
ecosystemLevelSizes: [],
|
|
98
|
+
ecosystemLevelIndexBySize: new Map(),
|
|
99
|
+
ecosystemHubNodeIds: new Set(),
|
|
94
100
|
ecosystemExpansionLevels: [],
|
|
95
101
|
ecosystemBaseSize: ecosystemLevelNodeCap,
|
|
96
102
|
ecosystemHubCluster: null,
|
|
@@ -617,6 +623,11 @@ const recomputeVisibility = () => {
|
|
|
617
623
|
state.ecosystemClustersBySize = ecosystemGraph.clustersBySize
|
|
618
624
|
state.ecosystemNodeClusterBySize = ecosystemGraph.nodeClusterBySize
|
|
619
625
|
state.ecosystemLevelSizes = ecosystemGraph.levelSizes
|
|
626
|
+
state.ecosystemLevelIndexBySize = ecosystemGraph.levelSizes.reduce((map, size, index) => {
|
|
627
|
+
map.set(size, index)
|
|
628
|
+
return map
|
|
629
|
+
}, new Map())
|
|
630
|
+
state.ecosystemHubNodeIds = new Set(ecosystemGraph.hubCluster?.nodeIds ?? [])
|
|
620
631
|
state.ecosystemExpansionLevels = ecosystemGraph.expansionLevels
|
|
621
632
|
state.ecosystemBaseSize = ecosystemGraph.baseSize
|
|
622
633
|
state.ecosystemHubCluster = ecosystemGraph.hubCluster
|
|
@@ -1105,10 +1116,18 @@ const expandFocusedClusters = (parentClusters, focusPoint, childSize, progress,
|
|
|
1105
1116
|
ecosystemFocusedParentLimit
|
|
1106
1117
|
))
|
|
1107
1118
|
const childClusters = state.ecosystemClustersBySize.get(childSize) ?? []
|
|
1108
|
-
const visibleChildClusters =
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
.
|
|
1119
|
+
const visibleChildClusters = []
|
|
1120
|
+
for (let index = 0; index < childClusters.length; index += 1) {
|
|
1121
|
+
const cluster = childClusters[index]
|
|
1122
|
+
if (!expandedParentIds.has(cluster.parentId)) {
|
|
1123
|
+
continue
|
|
1124
|
+
}
|
|
1125
|
+
const spreadCluster = spreadChildClusterFromParent(cluster, childSize, progress, spread)
|
|
1126
|
+
if (!isClusterInViewport(spreadCluster, viewport)) {
|
|
1127
|
+
continue
|
|
1128
|
+
}
|
|
1129
|
+
visibleChildClusters.push(spreadCluster)
|
|
1130
|
+
}
|
|
1112
1131
|
|
|
1113
1132
|
return {
|
|
1114
1133
|
expandedParentIds,
|
|
@@ -1137,11 +1156,21 @@ const selectHierarchicalEcosystemClusters = viewport => {
|
|
|
1137
1156
|
const visibleBaseClusters = filterEcosystemClustersByViewport(baseClusters, viewport)
|
|
1138
1157
|
const hubClusters = state.ecosystemHubCluster ? [state.ecosystemHubCluster] : []
|
|
1139
1158
|
const visibleClusters = [...visibleBaseClusters]
|
|
1159
|
+
const clustersBySize = new Map()
|
|
1160
|
+
for (let index = 0; index < visibleBaseClusters.length; index += 1) {
|
|
1161
|
+
const cluster = visibleBaseClusters[index]
|
|
1162
|
+
const levelClusters = clustersBySize.get(cluster.size)
|
|
1163
|
+
if (levelClusters) {
|
|
1164
|
+
levelClusters.push(cluster)
|
|
1165
|
+
} else {
|
|
1166
|
+
clustersBySize.set(cluster.size, [cluster])
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1140
1169
|
const focusPoint = ecosystemFocusPoint()
|
|
1141
1170
|
|
|
1142
1171
|
for (let index = 0; index < state.ecosystemExpansionLevels.length; index += 1) {
|
|
1143
1172
|
const level = state.ecosystemExpansionLevels[index]
|
|
1144
|
-
const parentClusters =
|
|
1173
|
+
const parentClusters = clustersBySize.get(level.parentSize) ?? []
|
|
1145
1174
|
if (parentClusters.length === 0) {
|
|
1146
1175
|
continue
|
|
1147
1176
|
}
|
|
@@ -1154,18 +1183,20 @@ const selectHierarchicalEcosystemClusters = viewport => {
|
|
|
1154
1183
|
const spread = semanticZoomSpread(progress, level.childSize)
|
|
1155
1184
|
const expansion = expandFocusedClusters(parentClusters, focusPoint, level.childSize, progress, spread, viewport)
|
|
1156
1185
|
visibleClusters.push(...expansion.childClusters)
|
|
1186
|
+
if (expansion.childClusters.length > 0) {
|
|
1187
|
+
const levelClusters = clustersBySize.get(level.childSize)
|
|
1188
|
+
if (levelClusters) {
|
|
1189
|
+
levelClusters.push(...expansion.childClusters)
|
|
1190
|
+
} else {
|
|
1191
|
+
clustersBySize.set(level.childSize, [...expansion.childClusters])
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1157
1194
|
}
|
|
1158
1195
|
|
|
1159
1196
|
return [...hubClusters, ...visibleClusters]
|
|
1160
1197
|
}
|
|
1161
1198
|
|
|
1162
|
-
const ecosystemLevelIndexBySize = () =>
|
|
1163
|
-
const indexBySize = new Map()
|
|
1164
|
-
for (let index = 0; index < state.ecosystemLevelSizes.length; index += 1) {
|
|
1165
|
-
indexBySize.set(state.ecosystemLevelSizes[index], index)
|
|
1166
|
-
}
|
|
1167
|
-
return indexBySize
|
|
1168
|
-
}
|
|
1199
|
+
const ecosystemLevelIndexBySize = () => state.ecosystemLevelIndexBySize
|
|
1169
1200
|
|
|
1170
1201
|
const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
|
|
1171
1202
|
if (cluster.isHub) {
|
|
@@ -1180,12 +1211,23 @@ const ecosystemDepthForCluster = (cluster, levelIndexMap) => {
|
|
|
1180
1211
|
|
|
1181
1212
|
const projectEcosystemPoint = (x, y, depth, anchor) => {
|
|
1182
1213
|
const safeDepth = Math.max(0, depth)
|
|
1183
|
-
const
|
|
1184
|
-
const
|
|
1214
|
+
const dx = x - anchor.x
|
|
1215
|
+
const dy = y - anchor.y
|
|
1216
|
+
const yawSin = Math.sin(ecosystemDepthYaw)
|
|
1217
|
+
const yawCos = Math.cos(ecosystemDepthYaw)
|
|
1218
|
+
const pitchSin = Math.sin(ecosystemDepthPitch)
|
|
1219
|
+
const pitchCos = Math.cos(ecosystemDepthPitch)
|
|
1220
|
+
const rotatedX = dx * yawCos + safeDepth * yawSin
|
|
1221
|
+
const rotatedZ = Math.max(0, safeDepth * yawCos - dx * yawSin)
|
|
1222
|
+
const rotatedY = dy * pitchCos - rotatedZ * pitchSin
|
|
1223
|
+
const projectedDepth = Math.max(0, rotatedZ + Math.max(0, dy * pitchSin))
|
|
1224
|
+
const factor = ecosystemDepthPerspective / (ecosystemDepthPerspective + projectedDepth)
|
|
1225
|
+
const verticalTilt = projectedDepth * ecosystemDepthTiltY
|
|
1185
1226
|
return {
|
|
1186
|
-
x: anchor.x +
|
|
1187
|
-
y: anchor.y +
|
|
1188
|
-
factor
|
|
1227
|
+
x: anchor.x + rotatedX * factor,
|
|
1228
|
+
y: anchor.y + rotatedY * factor - verticalTilt,
|
|
1229
|
+
factor,
|
|
1230
|
+
projectedDepth
|
|
1189
1231
|
}
|
|
1190
1232
|
}
|
|
1191
1233
|
|
|
@@ -1196,7 +1238,13 @@ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
|
|
|
1196
1238
|
|
|
1197
1239
|
for (let index = 0; index < clusters.length; index += 1) {
|
|
1198
1240
|
const cluster = clusters[index]
|
|
1199
|
-
const
|
|
1241
|
+
const baseDepth = ecosystemDepthForCluster(cluster, levelIndexMap)
|
|
1242
|
+
const radialDistance = Math.hypot(cluster.x - anchor.x, cluster.y - anchor.y)
|
|
1243
|
+
const radialOffset = cluster.isHub ? 0 : Math.min(320, radialDistance * ecosystemDepthRadialGain)
|
|
1244
|
+
const orbitalOffset = cluster.isHub
|
|
1245
|
+
? 0
|
|
1246
|
+
: Math.sin(Math.atan2(cluster.y - anchor.y, cluster.x - anchor.x) * 2.2) * ecosystemDepthOrbitalMaxOffset
|
|
1247
|
+
const depth = Math.max(0, baseDepth + radialOffset + orbitalOffset)
|
|
1200
1248
|
const projected = projectEcosystemPoint(cluster.x, cluster.y, depth, anchor)
|
|
1201
1249
|
const baseOpacity = Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1
|
|
1202
1250
|
const depthScale = ecosystemDepthMinScale + (1 - ecosystemDepthMinScale) * projected.factor
|
|
@@ -1209,27 +1257,27 @@ const applyEcosystemDepthProjection = (clusters, edges, anchor) => {
|
|
|
1209
1257
|
x: projected.x,
|
|
1210
1258
|
y: projected.y,
|
|
1211
1259
|
lodOpacity: baseOpacity * depthOpacity,
|
|
1212
|
-
depth,
|
|
1260
|
+
depth: projected.projectedDepth,
|
|
1213
1261
|
depthScale
|
|
1214
1262
|
}
|
|
1215
1263
|
projectedClusters.push(projectedCluster)
|
|
1216
1264
|
clusterById.set(projectedCluster.id, projectedCluster)
|
|
1217
1265
|
}
|
|
1218
1266
|
|
|
1219
|
-
const projectedEdges =
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1267
|
+
const projectedEdges = []
|
|
1268
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
1269
|
+
const edge = edges[index]
|
|
1270
|
+
const sourceCluster = clusterById.get(edge.sourceCluster.id)
|
|
1271
|
+
const targetCluster = clusterById.get(edge.targetCluster.id)
|
|
1272
|
+
if (!sourceCluster || !targetCluster) {
|
|
1273
|
+
continue
|
|
1274
|
+
}
|
|
1275
|
+
projectedEdges.push({
|
|
1276
|
+
...edge,
|
|
1277
|
+
sourceCluster,
|
|
1278
|
+
targetCluster
|
|
1231
1279
|
})
|
|
1232
|
-
|
|
1280
|
+
}
|
|
1233
1281
|
|
|
1234
1282
|
return {
|
|
1235
1283
|
clusters: projectedClusters,
|
|
@@ -1289,28 +1337,37 @@ const ecosystemEdgesForClusters = clusters => {
|
|
|
1289
1337
|
const clusterById = new Map(edgeClusters.map(cluster => [cluster.id, cluster]))
|
|
1290
1338
|
const clusterIds = new Set(clusterById.keys())
|
|
1291
1339
|
const levelsBySize = []
|
|
1340
|
+
const seenSizes = new Set()
|
|
1292
1341
|
for (let index = 0; index < edgeClusters.length; index += 1) {
|
|
1293
1342
|
const cluster = edgeClusters[index]
|
|
1294
1343
|
if (!cluster.size || cluster.isHub) continue
|
|
1295
|
-
if (
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
}
|
|
1344
|
+
if (seenSizes.has(cluster.size)) continue
|
|
1345
|
+
seenSizes.add(cluster.size)
|
|
1346
|
+
levelsBySize.push({
|
|
1347
|
+
size: cluster.size,
|
|
1348
|
+
lookup: state.ecosystemNodeClusterBySize.get(cluster.size) ?? new Map()
|
|
1349
|
+
})
|
|
1301
1350
|
}
|
|
1302
1351
|
levelsBySize.sort((left, right) => left.size - right.size)
|
|
1352
|
+
const resolvedNodeClusterById = new Map()
|
|
1303
1353
|
const resolveClusterForNode = nodeId => {
|
|
1304
|
-
if (
|
|
1354
|
+
if (resolvedNodeClusterById.has(nodeId)) {
|
|
1355
|
+
return resolvedNodeClusterById.get(nodeId)
|
|
1356
|
+
}
|
|
1357
|
+
if (state.ecosystemHubNodeIds.has(nodeId) && state.ecosystemHubCluster && clusterIds.has(state.ecosystemHubCluster.id)) {
|
|
1358
|
+
resolvedNodeClusterById.set(nodeId, state.ecosystemHubCluster)
|
|
1305
1359
|
return state.ecosystemHubCluster
|
|
1306
1360
|
}
|
|
1307
1361
|
for (let index = 0; index < levelsBySize.length; index += 1) {
|
|
1308
1362
|
const lookup = levelsBySize[index].lookup
|
|
1309
1363
|
const cluster = lookup.get(nodeId)
|
|
1310
1364
|
if (cluster && clusterIds.has(cluster.id)) {
|
|
1311
|
-
|
|
1365
|
+
const resolvedCluster = clusterById.get(cluster.id) ?? cluster
|
|
1366
|
+
resolvedNodeClusterById.set(nodeId, resolvedCluster)
|
|
1367
|
+
return resolvedCluster
|
|
1312
1368
|
}
|
|
1313
1369
|
}
|
|
1370
|
+
resolvedNodeClusterById.set(nodeId, null)
|
|
1314
1371
|
return null
|
|
1315
1372
|
}
|
|
1316
1373
|
|
|
@@ -2945,6 +3002,9 @@ const clusterRadiusPx = cluster => {
|
|
|
2945
3002
|
const clusterOpacity = cluster =>
|
|
2946
3003
|
Math.max(0, Math.min(1, Number.isFinite(cluster.lodOpacity) ? cluster.lodOpacity : 1))
|
|
2947
3004
|
|
|
3005
|
+
const clusterDepth = cluster => Number.isFinite(cluster.depth) ? cluster.depth : ecosystemDepthNear
|
|
3006
|
+
const clusterDepthScale = cluster => Number.isFinite(cluster.depthScale) ? cluster.depthScale : 1
|
|
3007
|
+
|
|
2948
3008
|
const worldViewportBounds = () => {
|
|
2949
3009
|
const width = Math.max(state.viewport.width, 320)
|
|
2950
3010
|
const height = Math.max(state.viewport.height, 320)
|
|
@@ -3364,6 +3424,8 @@ const render = now => {
|
|
|
3364
3424
|
ctx.save()
|
|
3365
3425
|
ctx.translate(state.transform.x, state.transform.y)
|
|
3366
3426
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
3427
|
+
const orderedClusters = [...state.renderClusters]
|
|
3428
|
+
.sort((left, right) => clusterDepth(right) - clusterDepth(left))
|
|
3367
3429
|
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
3368
3430
|
if (state.renderClusterEdges.length > 0) {
|
|
3369
3431
|
for (let index = 0; index < state.renderClusterEdges.length; index += 1) {
|
|
@@ -3372,15 +3434,17 @@ const render = now => {
|
|
|
3372
3434
|
if (edgeOpacity <= 0.01) {
|
|
3373
3435
|
continue
|
|
3374
3436
|
}
|
|
3437
|
+
const depthScale = Math.min(clusterDepthScale(edge.sourceCluster), clusterDepthScale(edge.targetCluster))
|
|
3438
|
+
const widthScale = 0.6 + depthScale * 0.9
|
|
3375
3439
|
ctx.beginPath()
|
|
3376
3440
|
ctx.moveTo(edge.sourceCluster.x, edge.sourceCluster.y)
|
|
3377
3441
|
ctx.lineTo(edge.targetCluster.x, edge.targetCluster.y)
|
|
3378
|
-
ctx.lineWidth = 1.2 / safeScale
|
|
3442
|
+
ctx.lineWidth = (1.2 * widthScale) / safeScale
|
|
3379
3443
|
ctx.strokeStyle = 'rgba(153, 165, 181, ' + (edge.inferred ? 0.14 : 0.22) * edgeOpacity + ')'
|
|
3380
3444
|
ctx.stroke()
|
|
3381
3445
|
}
|
|
3382
3446
|
}
|
|
3383
|
-
|
|
3447
|
+
orderedClusters.forEach(cluster => {
|
|
3384
3448
|
const isMacro = cluster.id === 'macro-galaxy'
|
|
3385
3449
|
const isEcosystem = String(cluster.id).startsWith('ecosystem-')
|
|
3386
3450
|
const isHub = Boolean(cluster.isHub)
|
package/package.json
CHANGED