@andespindola/brainlink 0.1.0-beta.60 → 0.1.0-beta.62
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.
|
@@ -22,6 +22,9 @@ const zoomCapTargetViewportShare = 0.72
|
|
|
22
22
|
const meshEdgeScaleThreshold = 0.09
|
|
23
23
|
const meshEdgeMinBudget = 140
|
|
24
24
|
const meshEdgeMaxBudget = 1400
|
|
25
|
+
const layeredCoreScaleThreshold = 0.55
|
|
26
|
+
const dragNeighborhoodMaxAffected = 180
|
|
27
|
+
const dragSettleRounds = 3
|
|
25
28
|
const state = {
|
|
26
29
|
graph: { nodes: [], edges: [] },
|
|
27
30
|
nodes: [],
|
|
@@ -567,6 +570,70 @@ const nodeBudgetForScale = (scale) => {
|
|
|
567
570
|
return renderNodeBudget
|
|
568
571
|
}
|
|
569
572
|
|
|
573
|
+
const layerWindowForScale = (scale) => {
|
|
574
|
+
if (scale < 0.08) return { inner: 0.78, outer: 1 }
|
|
575
|
+
if (scale < 0.14) return { inner: 0.62, outer: 0.9 }
|
|
576
|
+
if (scale < 0.24) return { inner: 0.46, outer: 0.74 }
|
|
577
|
+
if (scale < 0.36) return { inner: 0.3, outer: 0.58 }
|
|
578
|
+
if (scale < layeredCoreScaleThreshold) return { inner: 0.16, outer: 0.42 }
|
|
579
|
+
if (scale < 0.9) return { inner: 0.06, outer: 0.26 }
|
|
580
|
+
return { inner: 0, outer: 0.14 }
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const selectLayeredNodesForScale = (sourceNodes) => {
|
|
584
|
+
const hub = state.primaryHub
|
|
585
|
+
if (!hub || sourceNodes.length <= 1200 || state.visibleNodes.length <= massiveGraphNodeThreshold) {
|
|
586
|
+
return sourceNodes
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
let maxDistance = 0
|
|
590
|
+
const distances = sourceNodes.map((node) => {
|
|
591
|
+
const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
|
|
592
|
+
if (distance > maxDistance) {
|
|
593
|
+
maxDistance = distance
|
|
594
|
+
}
|
|
595
|
+
return { node, distance }
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
if (maxDistance <= 0.001) {
|
|
599
|
+
return sourceNodes
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const window = layerWindowForScale(state.transform.scale)
|
|
603
|
+
const inner = window.inner * maxDistance
|
|
604
|
+
const outer = window.outer * maxDistance
|
|
605
|
+
const layered = distances
|
|
606
|
+
.filter((item) => item.distance >= inner && item.distance <= outer)
|
|
607
|
+
.map((item) => item.node)
|
|
608
|
+
|
|
609
|
+
if (state.transform.scale >= layeredCoreScaleThreshold && !layered.some((node) => node.id === hub.id)) {
|
|
610
|
+
layered.push(hub)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (layered.length > 0) {
|
|
614
|
+
return layered
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const midpoint = (window.inner + window.outer) / 2
|
|
618
|
+
const fallback = [...distances]
|
|
619
|
+
.sort((left, right) => {
|
|
620
|
+
const leftNorm = left.distance / maxDistance
|
|
621
|
+
const rightNorm = right.distance / maxDistance
|
|
622
|
+
const leftDelta = Math.abs(leftNorm - midpoint)
|
|
623
|
+
const rightDelta = Math.abs(rightNorm - midpoint)
|
|
624
|
+
if (leftDelta !== rightDelta) return leftDelta - rightDelta
|
|
625
|
+
return left.node.id.localeCompare(right.node.id)
|
|
626
|
+
})
|
|
627
|
+
.slice(0, Math.min(900, sourceNodes.length))
|
|
628
|
+
.map((item) => item.node)
|
|
629
|
+
|
|
630
|
+
if (state.transform.scale >= layeredCoreScaleThreshold && !fallback.some((node) => node.id === hub.id)) {
|
|
631
|
+
fallback.push(hub)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return fallback
|
|
635
|
+
}
|
|
636
|
+
|
|
570
637
|
const edgeIdentityKey = edge => {
|
|
571
638
|
if (!edge.target) return ''
|
|
572
639
|
const pair = edge.source < edge.target
|
|
@@ -1289,6 +1356,104 @@ const worldPoint = event => {
|
|
|
1289
1356
|
}
|
|
1290
1357
|
}
|
|
1291
1358
|
|
|
1359
|
+
const connectedNodeIdsFor = (nodeId) => {
|
|
1360
|
+
const edges = state.visibleEdgeByNode.get(nodeId) ?? []
|
|
1361
|
+
const ids = new Set()
|
|
1362
|
+
|
|
1363
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
1364
|
+
const edge = edges[index]
|
|
1365
|
+
if (!edge.target) continue
|
|
1366
|
+
if (edge.source === nodeId) {
|
|
1367
|
+
ids.add(edge.target)
|
|
1368
|
+
} else if (edge.target === nodeId) {
|
|
1369
|
+
ids.add(edge.source)
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
return ids
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
const applyDragNeighborhoodAdjustment = (dragNode, deltaX, deltaY) => {
|
|
1377
|
+
if (!dragNode) return
|
|
1378
|
+
if (!Number.isFinite(deltaX) || !Number.isFinite(deltaY)) return
|
|
1379
|
+
if (Math.abs(deltaX) + Math.abs(deltaY) <= 0.001) return
|
|
1380
|
+
|
|
1381
|
+
const scale = Math.max(state.transform.scale, 0.0001)
|
|
1382
|
+
const influenceRadius = Math.max(220, Math.min(920, 440 / scale))
|
|
1383
|
+
const influenceRadiusSquared = influenceRadius * influenceRadius
|
|
1384
|
+
const connectedIds = connectedNodeIdsFor(dragNode.id)
|
|
1385
|
+
const candidates = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
|
|
1386
|
+
let adjusted = 0
|
|
1387
|
+
|
|
1388
|
+
for (let index = 0; index < candidates.length && adjusted < dragNeighborhoodMaxAffected; index += 1) {
|
|
1389
|
+
const node = candidates[index]
|
|
1390
|
+
if (node.id === dragNode.id) continue
|
|
1391
|
+
|
|
1392
|
+
const isConnected = connectedIds.has(node.id)
|
|
1393
|
+
const dx = node.x - dragNode.x
|
|
1394
|
+
const dy = node.y - dragNode.y
|
|
1395
|
+
const distanceSquared = dx * dx + dy * dy
|
|
1396
|
+
const withinRadius = distanceSquared <= influenceRadiusSquared
|
|
1397
|
+
if (!isConnected && !withinRadius) continue
|
|
1398
|
+
|
|
1399
|
+
const distance = Math.max(Math.sqrt(distanceSquared), 0.0001)
|
|
1400
|
+
const proximity = withinRadius ? 1 - (distance / influenceRadius) : 0
|
|
1401
|
+
const coupledStrength = isConnected ? 0.28 : 0.12
|
|
1402
|
+
const influence = Math.min(0.46, coupledStrength + proximity * 0.34)
|
|
1403
|
+
node.x += deltaX * influence
|
|
1404
|
+
node.y += deltaY * influence
|
|
1405
|
+
node.vx = (Number.isFinite(node.vx) ? node.vx : 0) + deltaX * influence * 0.06
|
|
1406
|
+
node.vy = (Number.isFinite(node.vy) ? node.vy : 0) + deltaY * influence * 0.06
|
|
1407
|
+
adjusted += 1
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
const settleNeighborhoodAroundNode = (dragNode) => {
|
|
1412
|
+
if (!dragNode) return
|
|
1413
|
+
|
|
1414
|
+
const scale = Math.max(state.transform.scale, 0.0001)
|
|
1415
|
+
const settleRadius = Math.max(240, Math.min(980, 520 / scale))
|
|
1416
|
+
const settleRadiusSquared = settleRadius * settleRadius
|
|
1417
|
+
const connectedIds = connectedNodeIdsFor(dragNode.id)
|
|
1418
|
+
const candidates = (state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes)
|
|
1419
|
+
.filter((node) => {
|
|
1420
|
+
if (node.id === dragNode.id) return true
|
|
1421
|
+
const dx = node.x - dragNode.x
|
|
1422
|
+
const dy = node.y - dragNode.y
|
|
1423
|
+
const distanceSquared = dx * dx + dy * dy
|
|
1424
|
+
return connectedIds.has(node.id) || distanceSquared <= settleRadiusSquared
|
|
1425
|
+
})
|
|
1426
|
+
.slice(0, dragNeighborhoodMaxAffected)
|
|
1427
|
+
|
|
1428
|
+
if (candidates.length <= 1) return
|
|
1429
|
+
|
|
1430
|
+
for (let round = 0; round < dragSettleRounds; round += 1) {
|
|
1431
|
+
for (let leftIndex = 0; leftIndex < candidates.length; leftIndex += 1) {
|
|
1432
|
+
const left = candidates[leftIndex]
|
|
1433
|
+
for (let rightIndex = leftIndex + 1; rightIndex < candidates.length; rightIndex += 1) {
|
|
1434
|
+
const right = candidates[rightIndex]
|
|
1435
|
+
const dx = right.x - left.x
|
|
1436
|
+
const dy = right.y - left.y
|
|
1437
|
+
const distance = Math.max(Math.hypot(dx, dy), 0.001)
|
|
1438
|
+
const minDistance = baseNodeRadius(left) + baseNodeRadius(right) + 10
|
|
1439
|
+
if (distance >= minDistance) continue
|
|
1440
|
+
|
|
1441
|
+
const push = (minDistance - distance) * 0.36
|
|
1442
|
+
const ux = dx / distance
|
|
1443
|
+
const uy = dy / distance
|
|
1444
|
+
if (left.id !== dragNode.id) {
|
|
1445
|
+
left.x -= ux * push
|
|
1446
|
+
left.y -= uy * push
|
|
1447
|
+
}
|
|
1448
|
+
if (right.id !== dragNode.id) {
|
|
1449
|
+
right.x += ux * push
|
|
1450
|
+
right.y += uy * push
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1292
1457
|
const hitNode = point => {
|
|
1293
1458
|
computeRenderVisibility()
|
|
1294
1459
|
if (state.renderClusters.length > 0) {
|
|
@@ -1465,10 +1630,11 @@ const computeRenderVisibility = () => {
|
|
|
1465
1630
|
if (state.visibleNodes.length > massiveGraphNodeThreshold) {
|
|
1466
1631
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
1467
1632
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
1633
|
+
const layeredNodes = selectLayeredNodesForScale(sourceNodes)
|
|
1468
1634
|
const sampleLimit = nodeBudgetForScale(state.transform.scale)
|
|
1469
|
-
const sampled =
|
|
1470
|
-
? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget),
|
|
1471
|
-
:
|
|
1635
|
+
const sampled = layeredNodes.length > sampleLimit
|
|
1636
|
+
? sampleVisibleNodes(Math.min(sampleLimit, renderNodeBudget), layeredNodes)
|
|
1637
|
+
: layeredNodes.slice(0, Math.min(layeredNodes.length, renderNodeBudget))
|
|
1472
1638
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
1473
1639
|
let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
|
|
1474
1640
|
let sampledNodes = ensureHubNodesInRenderedSet(sampled)
|
|
@@ -1964,8 +2130,12 @@ const bindEvents = () => {
|
|
|
1964
2130
|
state.pointer.y = event.clientY
|
|
1965
2131
|
state.pointer.moved = state.pointer.moved || Math.abs(dx) + Math.abs(dy) > 3
|
|
1966
2132
|
if (state.pointer.dragNode) {
|
|
1967
|
-
state.pointer.dragNode
|
|
1968
|
-
|
|
2133
|
+
const dragNode = state.pointer.dragNode
|
|
2134
|
+
const previousX = dragNode.x
|
|
2135
|
+
const previousY = dragNode.y
|
|
2136
|
+
dragNode.x = point.x
|
|
2137
|
+
dragNode.y = point.y
|
|
2138
|
+
applyDragNeighborhoodAdjustment(dragNode, dragNode.x - previousX, dragNode.y - previousY)
|
|
1969
2139
|
markRenderDirty()
|
|
1970
2140
|
return
|
|
1971
2141
|
}
|
|
@@ -1977,8 +2147,13 @@ const bindEvents = () => {
|
|
|
1977
2147
|
markRenderDirty()
|
|
1978
2148
|
})
|
|
1979
2149
|
canvas.addEventListener('pointerup', event => {
|
|
1980
|
-
|
|
1981
|
-
if (
|
|
2150
|
+
const draggedNode = state.pointer.dragNode
|
|
2151
|
+
if (draggedNode && state.pointer.moved) {
|
|
2152
|
+
settleNeighborhoodAroundNode(draggedNode)
|
|
2153
|
+
markRenderDirty()
|
|
2154
|
+
}
|
|
2155
|
+
if (draggedNode && !state.pointer.moved) selectNode(draggedNode, { openContent: true })
|
|
2156
|
+
if (!draggedNode && !state.pointer.moved) selectNode(state.hovered, { openContent: true })
|
|
1982
2157
|
state.pointer = { x: 0, y: 0, down: false, dragNode: null, moved: false }
|
|
1983
2158
|
canvas.releasePointerCapture(event.pointerId)
|
|
1984
2159
|
})
|
package/package.json
CHANGED