@andespindola/brainlink 0.1.0-beta.51 → 0.1.0-beta.53
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.
|
@@ -17,7 +17,7 @@ const worldCoordinateLimit = 5_000_000
|
|
|
17
17
|
const transformCoordinateLimit = 20_000_000
|
|
18
18
|
const hoverHitTestIntervalMs = 64
|
|
19
19
|
const overviewClusterMaxCount = 1400
|
|
20
|
-
const zoomRecoveryGuardMs =
|
|
20
|
+
const zoomRecoveryGuardMs = 1500
|
|
21
21
|
const state = {
|
|
22
22
|
graph: { nodes: [], edges: [] },
|
|
23
23
|
nodes: [],
|
|
@@ -52,6 +52,7 @@ const state = {
|
|
|
52
52
|
overviewClusters: [],
|
|
53
53
|
macroCenter: { x: 0, y: 0 },
|
|
54
54
|
macroRepresentative: null,
|
|
55
|
+
primaryHub: null,
|
|
55
56
|
filterWorker: null,
|
|
56
57
|
filterReady: false,
|
|
57
58
|
lastHoverHitAt: 0,
|
|
@@ -296,6 +297,7 @@ const recomputeVisibility = () => {
|
|
|
296
297
|
}
|
|
297
298
|
: { x: 0, y: 0 }
|
|
298
299
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
300
|
+
state.primaryHub = rankedHubNodes()[0] ?? null
|
|
299
301
|
markRenderDirty()
|
|
300
302
|
}
|
|
301
303
|
|
|
@@ -601,6 +603,27 @@ const enrichSampleWithNeighbors = (nodes) => {
|
|
|
601
603
|
}
|
|
602
604
|
}
|
|
603
605
|
|
|
606
|
+
const ensureHubNodesInRenderedSet = (nodes) => {
|
|
607
|
+
if (nodes.length === 0) {
|
|
608
|
+
return nodes
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const maxNodes = Math.max(renderNodeBudget, nodes.length)
|
|
612
|
+
const ids = new Set(nodes.map((node) => node.id))
|
|
613
|
+
const hubs = rankedHubNodes()
|
|
614
|
+
const merged = [...nodes]
|
|
615
|
+
|
|
616
|
+
for (let index = 0; index < hubs.length && merged.length < maxNodes; index += 1) {
|
|
617
|
+
const hub = hubs[index]
|
|
618
|
+
if (!ids.has(hub.id)) {
|
|
619
|
+
merged.push(hub)
|
|
620
|
+
ids.add(hub.id)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return merged
|
|
625
|
+
}
|
|
626
|
+
|
|
604
627
|
const clampScale = value => Math.max(zoomRange.min, Math.min(zoomRange.max, value))
|
|
605
628
|
const isFiniteNumber = value => Number.isFinite(value)
|
|
606
629
|
const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
|
|
@@ -658,7 +681,7 @@ const autoFitScaleRangeByNodeCount = nodeCount => {
|
|
|
658
681
|
return { min: 0.0008, max: 0.24 }
|
|
659
682
|
}
|
|
660
683
|
|
|
661
|
-
const fitView = (options = { useFiltered: true, macro: false }) => {
|
|
684
|
+
const fitView = (options = { useFiltered: true, macro: false, preferHubCenter: true }) => {
|
|
662
685
|
const rect = canvas.getBoundingClientRect()
|
|
663
686
|
const width = Math.max(rect.width, 320)
|
|
664
687
|
const height = Math.max(rect.height, 320)
|
|
@@ -695,8 +718,12 @@ const fitView = (options = { useFiltered: true, macro: false }) => {
|
|
|
695
718
|
: nodes.length > massiveGraphNodeThreshold
|
|
696
719
|
? clampScale(Math.min(baselineScale, massiveAutoFitMacroScale))
|
|
697
720
|
: baselineScale
|
|
698
|
-
const
|
|
699
|
-
|
|
721
|
+
const hubCenter =
|
|
722
|
+
options.preferHubCenter && state.primaryHub && nodes.some((node) => node.id === state.primaryHub.id)
|
|
723
|
+
? state.primaryHub
|
|
724
|
+
: null
|
|
725
|
+
const centerX = hubCenter ? hubCenter.x : (bounds.minX + bounds.maxX) / 2
|
|
726
|
+
const centerY = hubCenter ? hubCenter.y : (bounds.minY + bounds.maxY) / 2
|
|
700
727
|
|
|
701
728
|
state.transform = {
|
|
702
729
|
x: clampTransformCoordinate(width / 2 - centerX * scale),
|
|
@@ -708,7 +735,7 @@ const fitView = (options = { useFiltered: true, macro: false }) => {
|
|
|
708
735
|
markRenderDirty()
|
|
709
736
|
}
|
|
710
737
|
|
|
711
|
-
const resetView = () => fitView({ useFiltered: false, macro: true })
|
|
738
|
+
const resetView = () => fitView({ useFiltered: false, macro: true, preferHubCenter: true })
|
|
712
739
|
|
|
713
740
|
const createLayout = graph => {
|
|
714
741
|
const nodeRows = Array.isArray(graph.nodes) ? graph.nodes : []
|
|
@@ -1100,12 +1127,13 @@ const computeRenderVisibility = () => {
|
|
|
1100
1127
|
: sourceNodes.slice(0, Math.min(sourceNodes.length, renderNodeBudget))
|
|
1101
1128
|
const sampledIds = new Set(sampled.map((node) => node.id))
|
|
1102
1129
|
let sampledEdges = state.transform.scale >= 0.035 ? collectVisibleEdgesForNodes(sampledIds) : []
|
|
1103
|
-
let sampledNodes = sampled
|
|
1130
|
+
let sampledNodes = ensureHubNodesInRenderedSet(sampled)
|
|
1104
1131
|
|
|
1105
1132
|
if (state.transform.scale >= 0.035 && sampledEdges.length === 0) {
|
|
1106
|
-
const enriched = enrichSampleWithNeighbors(
|
|
1107
|
-
sampledNodes = enriched.nodes
|
|
1108
|
-
|
|
1133
|
+
const enriched = enrichSampleWithNeighbors(sampledNodes)
|
|
1134
|
+
sampledNodes = ensureHubNodesInRenderedSet(enriched.nodes)
|
|
1135
|
+
const sampledWithHubsIds = new Set(sampledNodes.map((node) => node.id))
|
|
1136
|
+
sampledEdges = collectVisibleEdgesForNodes(sampledWithHubsIds)
|
|
1109
1137
|
}
|
|
1110
1138
|
|
|
1111
1139
|
state.renderClusters = []
|
|
@@ -1159,10 +1187,11 @@ const computeRenderVisibility = () => {
|
|
|
1159
1187
|
return
|
|
1160
1188
|
}
|
|
1161
1189
|
|
|
1162
|
-
const
|
|
1190
|
+
const normalizedNodes = ensureHubNodesInRenderedSet(nodes)
|
|
1191
|
+
const nodeIds = new Set(normalizedNodes.map((node) => node.id))
|
|
1163
1192
|
const edges = collectVisibleEdgesForNodes(nodeIds)
|
|
1164
1193
|
|
|
1165
|
-
state.renderNodes =
|
|
1194
|
+
state.renderNodes = normalizedNodes
|
|
1166
1195
|
state.renderEdges = edges
|
|
1167
1196
|
|
|
1168
1197
|
if (state.renderNodes.length === 0 && state.visibleNodes.length > 0) {
|
|
@@ -1456,17 +1485,19 @@ const selectNodeById = id => {
|
|
|
1456
1485
|
}
|
|
1457
1486
|
|
|
1458
1487
|
const zoomAtPoint = (screenX, screenY, factor, source = 'generic') => {
|
|
1488
|
+
if (source === 'wheel') {
|
|
1489
|
+
state.lastManualZoomAt = performance.now()
|
|
1490
|
+
}
|
|
1459
1491
|
const nextScale = clampScale(state.transform.scale * factor)
|
|
1460
|
-
if (nextScale === state.transform.scale)
|
|
1492
|
+
if (nextScale === state.transform.scale) {
|
|
1493
|
+
return
|
|
1494
|
+
}
|
|
1461
1495
|
const worldX = (screenX - state.transform.x) / state.transform.scale
|
|
1462
1496
|
const worldY = (screenY - state.transform.y) / state.transform.scale
|
|
1463
1497
|
state.transform.scale = clampScale(nextScale)
|
|
1464
1498
|
state.transform.x = clampTransformCoordinate(screenX - worldX * nextScale)
|
|
1465
1499
|
state.transform.y = clampTransformCoordinate(screenY - worldY * nextScale)
|
|
1466
1500
|
state.offscreenFrameCount = 0
|
|
1467
|
-
if (source === 'wheel') {
|
|
1468
|
-
state.lastManualZoomAt = performance.now()
|
|
1469
|
-
}
|
|
1470
1501
|
markRenderDirty()
|
|
1471
1502
|
}
|
|
1472
1503
|
|
|
@@ -20,6 +20,7 @@ const segmentAngles = {
|
|
|
20
20
|
Evaluation: 2.08,
|
|
21
21
|
Security: 2.82
|
|
22
22
|
};
|
|
23
|
+
const hubTitlePattern = /\b(memory\s*hub|knowledge\s*root|moc|map)\b/i;
|
|
23
24
|
const hashText = (value) => Array.from(value).reduce((hash, char) => ((hash << 5) - hash + char.charCodeAt(0)) | 0, 0);
|
|
24
25
|
const jitter = (value, range) => {
|
|
25
26
|
const normalized = Math.abs(hashText(value) % 1000) / 1000;
|
|
@@ -62,6 +63,44 @@ const byDegreeThenTitle = (degrees) => (left, right) => {
|
|
|
62
63
|
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
63
64
|
return degreeDelta === 0 ? byTitle(left, right) : degreeDelta;
|
|
64
65
|
};
|
|
66
|
+
const hubScore = (node) => {
|
|
67
|
+
const title = node.title.trim().toLowerCase();
|
|
68
|
+
if (title === 'memory hub')
|
|
69
|
+
return 5;
|
|
70
|
+
if (title === 'knowledge root')
|
|
71
|
+
return 4;
|
|
72
|
+
if (/\bmoc\b/i.test(node.title))
|
|
73
|
+
return 3;
|
|
74
|
+
return hubTitlePattern.test(node.title) ? 2 : 0;
|
|
75
|
+
};
|
|
76
|
+
const selectPrimaryHubId = (nodes, degrees) => {
|
|
77
|
+
const ranked = [...nodes]
|
|
78
|
+
.filter((node) => hubScore(node) > 0)
|
|
79
|
+
.sort((left, right) => {
|
|
80
|
+
const scoreDelta = hubScore(right) - hubScore(left);
|
|
81
|
+
if (scoreDelta !== 0)
|
|
82
|
+
return scoreDelta;
|
|
83
|
+
const degreeDelta = (degrees.get(right.id) ?? 0) - (degrees.get(left.id) ?? 0);
|
|
84
|
+
if (degreeDelta !== 0)
|
|
85
|
+
return degreeDelta;
|
|
86
|
+
return left.title.localeCompare(right.title);
|
|
87
|
+
});
|
|
88
|
+
return ranked[0]?.id ?? null;
|
|
89
|
+
};
|
|
90
|
+
const centerLayoutByNode = (nodes, nodeId) => {
|
|
91
|
+
if (!nodeId) {
|
|
92
|
+
return nodes;
|
|
93
|
+
}
|
|
94
|
+
const anchor = nodes.find((node) => node.id === nodeId);
|
|
95
|
+
if (!anchor) {
|
|
96
|
+
return nodes;
|
|
97
|
+
}
|
|
98
|
+
return nodes.map((node) => ({
|
|
99
|
+
...node,
|
|
100
|
+
x: node.x - anchor.x,
|
|
101
|
+
y: node.y - anchor.y
|
|
102
|
+
}));
|
|
103
|
+
};
|
|
65
104
|
const naturalSegmentSeed = (node) => groupKey(node) === '00-maps' || /\b(moc|map)\b/i.test(node.title);
|
|
66
105
|
const segmentName = (node) => node.title.replace(/^MOC\s+/i, '').replace(/\s+Memory Map$/i, '').trim() || node.title;
|
|
67
106
|
const collectComponent = (adjacency, startId, visited) => {
|
|
@@ -246,8 +285,10 @@ export const createCauliflowerGraphLayout = (graph) => {
|
|
|
246
285
|
const segmentGroups = Array.from(groupNodesBySegment(graph.nodes, segments).entries())
|
|
247
286
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
248
287
|
const nodes = relaxCollisions(segmentGroups.flatMap(createSegmentNodes(segments, degrees, segmentGroups.length)));
|
|
288
|
+
const primaryHubId = selectPrimaryHubId(graph.nodes, degrees);
|
|
289
|
+
const centeredNodes = centerLayoutByNode(nodes, primaryHubId);
|
|
249
290
|
return {
|
|
250
|
-
nodes,
|
|
291
|
+
nodes: centeredNodes,
|
|
251
292
|
edges: graph.edges
|
|
252
293
|
};
|
|
253
294
|
};
|
package/package.json
CHANGED