@andespindola/brainlink 0.1.0-beta.135 → 0.1.0-beta.137
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.
|
@@ -30,7 +30,7 @@ const worldCoordinateLimit = 5_000_000
|
|
|
30
30
|
const transformCoordinateLimit = 20_000_000
|
|
31
31
|
const hoverHitTestIntervalMs = 64
|
|
32
32
|
const zoomRecoveryGuardMs = 4200
|
|
33
|
-
const
|
|
33
|
+
const hierarchyAbsoluteEdgeSafetyCap = 24_000
|
|
34
34
|
const dragNeighborhoodMaxAffected = 180
|
|
35
35
|
const dragSettleRounds = 3
|
|
36
36
|
const wheelZoomExponent = 0.0009
|
|
@@ -89,6 +89,7 @@ const state = {
|
|
|
89
89
|
lastHoverHitAt: 0,
|
|
90
90
|
lastManualZoomAt: 0,
|
|
91
91
|
lastZoomFocus: { x: 0, y: 0, at: 0 },
|
|
92
|
+
leafFocusRootNodeId: null,
|
|
92
93
|
hierarchyFocusGroupId: null,
|
|
93
94
|
hierarchyFocusStack: [],
|
|
94
95
|
hierarchyRevealFocusGroupId: null,
|
|
@@ -1337,6 +1338,68 @@ const arrangeChildGraphNodes = (nodes, group, origin = group) => {
|
|
|
1337
1338
|
return arranged
|
|
1338
1339
|
}
|
|
1339
1340
|
|
|
1341
|
+
const projectNodesIntoChildGraph = (nodes, focusRenderNode, group) => {
|
|
1342
|
+
if (nodes.length <= 1) {
|
|
1343
|
+
return nodes.map(node => ({ ...node, x: focusRenderNode.x, y: focusRenderNode.y }))
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
let minX = Number.POSITIVE_INFINITY
|
|
1347
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
1348
|
+
let minY = Number.POSITIVE_INFINITY
|
|
1349
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
1350
|
+
|
|
1351
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1352
|
+
const node = nodes[index]
|
|
1353
|
+
minX = Math.min(minX, node.x)
|
|
1354
|
+
maxX = Math.max(maxX, node.x)
|
|
1355
|
+
minY = Math.min(minY, node.y)
|
|
1356
|
+
maxY = Math.max(maxY, node.y)
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
const centerX = (minX + maxX) / 2
|
|
1360
|
+
const centerY = (minY + maxY) / 2
|
|
1361
|
+
const spanX = Math.max(1, maxX - minX)
|
|
1362
|
+
const spanY = Math.max(1, maxY - minY)
|
|
1363
|
+
const targetDiameter = Math.max(1, childGraphRenderRadius(group) * 2 * 0.9)
|
|
1364
|
+
const scale = targetDiameter / Math.max(spanX, spanY)
|
|
1365
|
+
|
|
1366
|
+
return nodes.map(node => ({
|
|
1367
|
+
...node,
|
|
1368
|
+
x: focusRenderNode.x + (node.x - centerX) * scale,
|
|
1369
|
+
y: focusRenderNode.y + (node.y - centerY) * scale
|
|
1370
|
+
}))
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
const recursiveLeafSubgraphNodes = (rootNodeId, maxNodes = renderNodeBudget) => {
|
|
1374
|
+
const root = state.nodeById.get(rootNodeId)
|
|
1375
|
+
if (!root) {
|
|
1376
|
+
return []
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
const visited = new Set([rootNodeId])
|
|
1380
|
+
const queue = [rootNodeId]
|
|
1381
|
+
|
|
1382
|
+
while (queue.length > 0 && visited.size < maxNodes) {
|
|
1383
|
+
const currentId = queue.shift()
|
|
1384
|
+
const edges = [...(state.visibleEdgeByNode.get(currentId) ?? [])]
|
|
1385
|
+
.filter(edge => edge.target)
|
|
1386
|
+
.sort((left, right) => edgeWeight(right) - edgeWeight(left))
|
|
1387
|
+
for (let edgeIndex = 0; edgeIndex < edges.length && visited.size < maxNodes; edgeIndex += 1) {
|
|
1388
|
+
const edge = edges[edgeIndex]
|
|
1389
|
+
const nextId = edge.source === currentId ? edge.target : edge.source
|
|
1390
|
+
if (!nextId || visited.has(nextId) || !state.nodeById.has(nextId)) {
|
|
1391
|
+
continue
|
|
1392
|
+
}
|
|
1393
|
+
visited.add(nextId)
|
|
1394
|
+
queue.push(nextId)
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
return [...visited]
|
|
1399
|
+
.map(id => state.nodeById.get(id))
|
|
1400
|
+
.filter(Boolean)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1340
1403
|
const arrangeChildGroupNodes = (groups, parentGroup, origin) => {
|
|
1341
1404
|
if (groups.length <= 1) {
|
|
1342
1405
|
return groups.map(group => ({
|
|
@@ -1502,6 +1565,7 @@ const updateHierarchyFocusGroup = (groups, _viewport) => {
|
|
|
1502
1565
|
state.selected = null
|
|
1503
1566
|
}
|
|
1504
1567
|
}
|
|
1568
|
+
state.leafFocusRootNodeId = null
|
|
1505
1569
|
state.hierarchyFocusGroupId = null
|
|
1506
1570
|
return null
|
|
1507
1571
|
}
|
|
@@ -1542,7 +1606,7 @@ const hierarchyViewportProgress = (group, _viewport) => {
|
|
|
1542
1606
|
return Math.pow(Math.max(0, Math.min(1, transitionScaleProgress)), 2.8)
|
|
1543
1607
|
}
|
|
1544
1608
|
|
|
1545
|
-
const groupEdgesForRenderedGroups = (groupNodes) => {
|
|
1609
|
+
const groupEdgesForRenderedGroups = (groupNodes, options = { preferComplete: false }) => {
|
|
1546
1610
|
if (groupNodes.length <= 1) {
|
|
1547
1611
|
return []
|
|
1548
1612
|
}
|
|
@@ -1586,9 +1650,12 @@ const groupEdgesForRenderedGroups = (groupNodes) => {
|
|
|
1586
1650
|
})
|
|
1587
1651
|
}
|
|
1588
1652
|
|
|
1589
|
-
|
|
1653
|
+
const sorted = Array.from(selected.values())
|
|
1590
1654
|
.sort((left, right) => edgeWeight(right) - edgeWeight(left) || left.source.localeCompare(right.source) || left.target.localeCompare(right.target))
|
|
1591
|
-
|
|
1655
|
+
if (options.preferComplete) {
|
|
1656
|
+
return sorted.slice(0, Math.min(sorted.length, hierarchyAbsoluteEdgeSafetyCap))
|
|
1657
|
+
}
|
|
1658
|
+
return sorted.slice(0, Math.min(edgeBudgetForCurrentFrame(), hierarchyAbsoluteEdgeSafetyCap))
|
|
1592
1659
|
}
|
|
1593
1660
|
|
|
1594
1661
|
const computeHierarchyRenderVisibility = (viewport) => {
|
|
@@ -1610,7 +1677,7 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1610
1677
|
state.hierarchyRevealFocusGroupId = null
|
|
1611
1678
|
state.hierarchyRevealBudget = 1
|
|
1612
1679
|
state.renderNodes = groupNodes
|
|
1613
|
-
state.renderEdges = groupEdgesForRenderedGroups(groupNodes)
|
|
1680
|
+
state.renderEdges = groupEdgesForRenderedGroups(groupNodes, { preferComplete: true })
|
|
1614
1681
|
return true
|
|
1615
1682
|
}
|
|
1616
1683
|
|
|
@@ -1619,12 +1686,23 @@ const computeHierarchyRenderVisibility = (viewport) => {
|
|
|
1619
1686
|
const focusChildGroups = focus.childGroupIds
|
|
1620
1687
|
.map(groupId => state.groupById.get(groupId))
|
|
1621
1688
|
.filter(Boolean)
|
|
1689
|
+
if (focusChildGroups.length > 0) {
|
|
1690
|
+
state.leafFocusRootNodeId = null
|
|
1691
|
+
} else if (state.leafFocusRootNodeId && !focusIds.has(state.leafFocusRootNodeId)) {
|
|
1692
|
+
state.leafFocusRootNodeId = null
|
|
1693
|
+
}
|
|
1622
1694
|
const childTargetLimit = Math.min(renderNodeBudget, Math.max(1, Math.floor(renderNodeBudget * revealProgress)))
|
|
1623
1695
|
const childLimit = updateHierarchyChildRevealBudget(focus.id, childTargetLimit)
|
|
1624
1696
|
const focusRenderNode = groupNodes.find(node => node.groupId === focus.id) ?? createGroupRenderNode(focus)
|
|
1625
|
-
const arrangedChildren =
|
|
1626
|
-
|
|
1627
|
-
|
|
1697
|
+
const arrangedChildren = (() => {
|
|
1698
|
+
if (focusChildGroups.length > 0) {
|
|
1699
|
+
return arrangeChildGroupNodes(focusChildGroups, focus, focusRenderNode)
|
|
1700
|
+
}
|
|
1701
|
+
const recursiveNodes = state.leafFocusRootNodeId
|
|
1702
|
+
? recursiveLeafSubgraphNodes(state.leafFocusRootNodeId, renderNodeBudget)
|
|
1703
|
+
: rawChildNodes
|
|
1704
|
+
return projectNodesIntoChildGraph(recursiveNodes, focusRenderNode, focus)
|
|
1705
|
+
})()
|
|
1628
1706
|
const childNodes = selectStableSampleNodes(arrangedChildren, childLimit)
|
|
1629
1707
|
.map(node => interpolateNodeFromGroup(node, focusRenderNode, revealProgress))
|
|
1630
1708
|
const childIds = new Set(childNodes.map(node => node.id))
|
|
@@ -2118,6 +2196,7 @@ const fitView = (options = { useFiltered: true, preferHubCenter: true }) => {
|
|
|
2118
2196
|
const resetView = () => fitView({ useFiltered: false, preferHubCenter: false })
|
|
2119
2197
|
|
|
2120
2198
|
const resetHierarchyFocus = () => {
|
|
2199
|
+
state.leafFocusRootNodeId = null
|
|
2121
2200
|
state.hierarchyFocusGroupId = null
|
|
2122
2201
|
state.hierarchyFocusStack = []
|
|
2123
2202
|
state.hierarchyRevealFocusGroupId = null
|
|
@@ -3125,6 +3204,22 @@ const expandGroupNode = node => {
|
|
|
3125
3204
|
}
|
|
3126
3205
|
}
|
|
3127
3206
|
|
|
3207
|
+
const expandLeafNodeGraph = node => {
|
|
3208
|
+
if (!node || node.isGroupNode) return
|
|
3209
|
+
state.selected = node
|
|
3210
|
+
state.leafFocusRootNodeId = node.id
|
|
3211
|
+
state.zoomTransition = {
|
|
3212
|
+
active: true,
|
|
3213
|
+
source: 'group',
|
|
3214
|
+
screenX: state.viewport.width / 2,
|
|
3215
|
+
screenY: state.viewport.height / 2,
|
|
3216
|
+
worldX: node.x,
|
|
3217
|
+
worldY: node.y,
|
|
3218
|
+
targetScale: clampScale(Math.max(state.transform.scale * 1.08, hierarchyMicroExitScale * 1.02))
|
|
3219
|
+
}
|
|
3220
|
+
state.lastZoomFocus = { x: node.x, y: node.y, at: performance.now() }
|
|
3221
|
+
markRenderDirty()
|
|
3222
|
+
}
|
|
3128
3223
|
const handleGroupNodePrimaryClick = node => {
|
|
3129
3224
|
if (!node?.isGroupNode) return
|
|
3130
3225
|
const alreadyFocused = state.selected?.isGroupNode && state.selected.groupId === node.groupId
|
|
@@ -3135,8 +3230,19 @@ const handleGroupNodePrimaryClick = node => {
|
|
|
3135
3230
|
expandGroupNode(node)
|
|
3136
3231
|
}
|
|
3137
3232
|
|
|
3233
|
+
const handleLeafNodePrimaryClick = node => {
|
|
3234
|
+
if (!node || node.isGroupNode) return
|
|
3235
|
+
const sameFocusedNode = state.leafFocusRootNodeId === node.id
|
|
3236
|
+
if (!sameFocusedNode) {
|
|
3237
|
+
expandLeafNodeGraph(node)
|
|
3238
|
+
return
|
|
3239
|
+
}
|
|
3240
|
+
selectNode(node, { openContent: true })
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3138
3243
|
const selectNode = (node, options = { openContent: false }) => {
|
|
3139
3244
|
if (!node) {
|
|
3245
|
+
state.leafFocusRootNodeId = null
|
|
3140
3246
|
state.selected = null
|
|
3141
3247
|
if (elements.contentDialog.open) {
|
|
3142
3248
|
elements.contentDialog.close()
|
|
@@ -3373,6 +3479,8 @@ const bindEvents = () => {
|
|
|
3373
3479
|
const clickedNode = draggedNode ?? state.hovered
|
|
3374
3480
|
if (clickedNode?.isGroupNode) {
|
|
3375
3481
|
handleGroupNodePrimaryClick(clickedNode)
|
|
3482
|
+
} else if (clickedNode && state.hierarchyFocusGroupId) {
|
|
3483
|
+
handleLeafNodePrimaryClick(clickedNode)
|
|
3376
3484
|
} else {
|
|
3377
3485
|
selectNode(clickedNode, { openContent: true })
|
|
3378
3486
|
}
|
|
@@ -3462,6 +3570,7 @@ const loadGraph = async (options = { reset: false }) => {
|
|
|
3462
3570
|
state.graph = graph
|
|
3463
3571
|
state.nodes = layout.nodes
|
|
3464
3572
|
state.groups = layout.groups
|
|
3573
|
+
state.leafFocusRootNodeId = null
|
|
3465
3574
|
state.hierarchyFocusGroupId = null
|
|
3466
3575
|
state.hierarchyFocusStack = []
|
|
3467
3576
|
state.hierarchyRevealFocusGroupId = null
|
package/package.json
CHANGED