@andespindola/brainlink 0.1.0-beta.54 → 0.1.0-beta.55
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.
|
@@ -18,6 +18,7 @@ const transformCoordinateLimit = 20_000_000
|
|
|
18
18
|
const hoverHitTestIntervalMs = 64
|
|
19
19
|
const overviewClusterMaxCount = 1400
|
|
20
20
|
const zoomRecoveryGuardMs = 1500
|
|
21
|
+
const zoomCapTargetViewportShare = 0.72
|
|
21
22
|
const state = {
|
|
22
23
|
graph: { nodes: [], edges: [] },
|
|
23
24
|
nodes: [],
|
|
@@ -53,6 +54,7 @@ const state = {
|
|
|
53
54
|
macroCenter: { x: 0, y: 0 },
|
|
54
55
|
macroRepresentative: null,
|
|
55
56
|
primaryHub: null,
|
|
57
|
+
hubNeighborDistance: Number.POSITIVE_INFINITY,
|
|
56
58
|
filterWorker: null,
|
|
57
59
|
filterReady: false,
|
|
58
60
|
lastHoverHitAt: 0,
|
|
@@ -232,6 +234,19 @@ const resize = () => {
|
|
|
232
234
|
const normalizeQuery = value => value.trim().toLowerCase()
|
|
233
235
|
const hubNodeRetentionLimit = 2
|
|
234
236
|
const hubNodePattern = /\b(memory\s*hub|knowledge\s*hub|hub|moc|map|memory\s*map|mapa)\b/i
|
|
237
|
+
const memoryHubPathPattern = /\bmemory[-_\s]*hub\b/i
|
|
238
|
+
|
|
239
|
+
const hubNodeScore = node => {
|
|
240
|
+
const title = node.title.trim().toLowerCase()
|
|
241
|
+
if (title === 'memory hub') return 6
|
|
242
|
+
if (title === 'knowledge hub') return 5
|
|
243
|
+
if (memoryHubPathPattern.test(node.path || '')) return 4
|
|
244
|
+
if (node.tags.some(tag => tag.trim().toLowerCase() === 'memory-hub')) return 3
|
|
245
|
+
if (/\bmoc\b/i.test(node.title)) return 2
|
|
246
|
+
return hubNodePattern.test(node.title) || hubNodePattern.test(node.path || '') || node.tags.some(tag => hubNodePattern.test(tag))
|
|
247
|
+
? 1
|
|
248
|
+
: 0
|
|
249
|
+
}
|
|
235
250
|
|
|
236
251
|
const localFilteredNodes = query =>
|
|
237
252
|
state.nodes.filter(node =>
|
|
@@ -246,8 +261,10 @@ const rankedHubNodes = () => {
|
|
|
246
261
|
}
|
|
247
262
|
|
|
248
263
|
const byTitleAndDegree = [...state.nodes]
|
|
249
|
-
.filter(node =>
|
|
264
|
+
.filter(node => hubNodeScore(node) > 0)
|
|
250
265
|
.sort((left, right) => {
|
|
266
|
+
const byHubScore = hubNodeScore(right) - hubNodeScore(left)
|
|
267
|
+
if (byHubScore !== 0) return byHubScore
|
|
251
268
|
const byDegree = (state.nodeDegrees.get(right.id) ?? 0) - (state.nodeDegrees.get(left.id) ?? 0)
|
|
252
269
|
if (byDegree !== 0) return byDegree
|
|
253
270
|
return left.title.localeCompare(right.title)
|
|
@@ -292,7 +309,10 @@ const resolveMacroRepresentative = (nodes) => {
|
|
|
292
309
|
return null
|
|
293
310
|
}
|
|
294
311
|
|
|
295
|
-
|
|
312
|
+
const hubCandidate = state.primaryHub && nodes.some(node => node.id === state.primaryHub.id)
|
|
313
|
+
? state.primaryHub
|
|
314
|
+
: null
|
|
315
|
+
let best = hubCandidate ?? nodes[0]
|
|
296
316
|
let bestDegree = state.nodeDegrees.get(best.id) ?? 0
|
|
297
317
|
|
|
298
318
|
for (let index = 1; index < nodes.length; index += 1) {
|
|
@@ -307,6 +327,24 @@ const resolveMacroRepresentative = (nodes) => {
|
|
|
307
327
|
return best
|
|
308
328
|
}
|
|
309
329
|
|
|
330
|
+
const nearestHubNeighborDistance = (hub, nodes) => {
|
|
331
|
+
if (!hub || nodes.length <= 1) {
|
|
332
|
+
return Number.POSITIVE_INFINITY
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
let minimum = Number.POSITIVE_INFINITY
|
|
336
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
337
|
+
const node = nodes[index]
|
|
338
|
+
if (node.id === hub.id) continue
|
|
339
|
+
const distance = Math.hypot(node.x - hub.x, node.y - hub.y)
|
|
340
|
+
if (distance < minimum) {
|
|
341
|
+
minimum = distance
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return minimum
|
|
346
|
+
}
|
|
347
|
+
|
|
310
348
|
const recomputeVisibility = () => {
|
|
311
349
|
const nodes = filteredNodes()
|
|
312
350
|
const ids = new Set(nodes.map(node => node.id))
|
|
@@ -322,15 +360,17 @@ const recomputeVisibility = () => {
|
|
|
322
360
|
state.visibleNodeSpatial = createSpatialIndex(nodes)
|
|
323
361
|
state.visibleEdgeByNode = createVisibleEdgeLookup(limitedEdges)
|
|
324
362
|
state.overviewClusters = nodes.length > massiveGraphNodeThreshold ? buildOverviewClusters(nodes) : []
|
|
363
|
+
const primaryHub = rankedHubNodes()[0] ?? null
|
|
364
|
+
state.primaryHub = primaryHub
|
|
365
|
+
state.hubNeighborDistance = nearestHubNeighborDistance(primaryHub, nodes)
|
|
325
366
|
const bounds = graphBounds(nodes)
|
|
326
367
|
state.macroCenter = bounds
|
|
327
368
|
? {
|
|
328
|
-
x: (bounds.minX + bounds.maxX) / 2,
|
|
329
|
-
y: (bounds.minY + bounds.maxY) / 2
|
|
369
|
+
x: primaryHub ? primaryHub.x : (bounds.minX + bounds.maxX) / 2,
|
|
370
|
+
y: primaryHub ? primaryHub.y : (bounds.minY + bounds.maxY) / 2
|
|
330
371
|
}
|
|
331
372
|
: { x: 0, y: 0 }
|
|
332
373
|
state.macroRepresentative = resolveMacroRepresentative(nodes)
|
|
333
|
-
state.primaryHub = rankedHubNodes()[0] ?? null
|
|
334
374
|
markRenderDirty()
|
|
335
375
|
}
|
|
336
376
|
|
|
@@ -641,23 +681,61 @@ const ensureHubNodesInRenderedSet = (nodes) => {
|
|
|
641
681
|
return nodes
|
|
642
682
|
}
|
|
643
683
|
|
|
644
|
-
const maxNodes = Math.max(renderNodeBudget, nodes.length)
|
|
684
|
+
const maxNodes = Math.max(Math.min(renderNodeBudget, nodes.length), 1)
|
|
645
685
|
const ids = new Set(nodes.map((node) => node.id))
|
|
646
686
|
const hubs = rankedHubNodes()
|
|
647
687
|
const merged = [...nodes]
|
|
648
688
|
|
|
649
|
-
for (let index = 0; index < hubs.length
|
|
689
|
+
for (let index = 0; index < hubs.length; index += 1) {
|
|
650
690
|
const hub = hubs[index]
|
|
651
|
-
if (
|
|
691
|
+
if (ids.has(hub.id)) {
|
|
692
|
+
continue
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
if (merged.length < maxNodes) {
|
|
652
696
|
merged.push(hub)
|
|
653
697
|
ids.add(hub.id)
|
|
698
|
+
continue
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const replacementIndex = merged.findIndex((node) => !hubs.some((candidate) => candidate.id === node.id))
|
|
702
|
+
if (replacementIndex >= 0) {
|
|
703
|
+
ids.delete(merged[replacementIndex].id)
|
|
704
|
+
merged[replacementIndex] = hub
|
|
705
|
+
ids.add(hub.id)
|
|
654
706
|
}
|
|
655
707
|
}
|
|
656
708
|
|
|
657
709
|
return merged
|
|
658
710
|
}
|
|
659
711
|
|
|
660
|
-
const
|
|
712
|
+
const zoomCapByNodeCount = (nodeCount) => {
|
|
713
|
+
if (nodeCount > 50000) return 0.88
|
|
714
|
+
if (nodeCount > 20000) return 1.15
|
|
715
|
+
if (nodeCount > 6000) return 1.65
|
|
716
|
+
if (nodeCount > 2000) return 2.2
|
|
717
|
+
return zoomRange.max
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const zoomCapByHubDistance = (distance) => {
|
|
721
|
+
if (!Number.isFinite(distance) || distance <= 0) {
|
|
722
|
+
return zoomRange.max
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const rect = canvas.getBoundingClientRect()
|
|
726
|
+
const viewportWidth = Math.max(rect.width, 320)
|
|
727
|
+
const viewportHeight = Math.max(rect.height, 320)
|
|
728
|
+
const reference = Math.max(220, Math.min(viewportWidth, viewportHeight) * zoomCapTargetViewportShare)
|
|
729
|
+
return Math.max(0.3, Math.min(zoomRange.max, reference / distance))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const currentZoomMax = () => {
|
|
733
|
+
const nodeCount = state.visibleNodes.length > 0 ? state.visibleNodes.length : state.nodes.length
|
|
734
|
+
const capped = Math.min(zoomCapByNodeCount(nodeCount), zoomCapByHubDistance(state.hubNeighborDistance))
|
|
735
|
+
return Math.max(zoomRange.min * 2, capped)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const clampScale = value => Math.max(zoomRange.min, Math.min(currentZoomMax(), value))
|
|
661
739
|
const isFiniteNumber = value => Number.isFinite(value)
|
|
662
740
|
const isReasonableCoordinate = value => isFiniteNumber(value) && Math.abs(value) <= worldCoordinateLimit
|
|
663
741
|
const clampTransformCoordinate = value => {
|
|
@@ -1123,7 +1201,7 @@ const computeRenderVisibility = () => {
|
|
|
1123
1201
|
if (shouldRenderMacroGalaxy) {
|
|
1124
1202
|
const viewportNodes = viewportNodesFromSpatialIndex(viewport)
|
|
1125
1203
|
const sourceNodes = viewportNodes.length > 0 ? viewportNodes : state.visibleNodes
|
|
1126
|
-
const representative = state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
1204
|
+
const representative = state.primaryHub ?? state.macroRepresentative ?? sourceNodes[0] ?? null
|
|
1127
1205
|
if (representative) {
|
|
1128
1206
|
state.renderClusters = [
|
|
1129
1207
|
{
|
|
@@ -1371,6 +1449,13 @@ const render = now => {
|
|
|
1371
1449
|
ctx.lineWidth = 1.4 / safeScale
|
|
1372
1450
|
ctx.strokeStyle = isMacro ? '#ffffff' : graphTheme.nodeStroke
|
|
1373
1451
|
ctx.stroke()
|
|
1452
|
+
if (isMacro && cluster.representative?.title) {
|
|
1453
|
+
ctx.fillStyle = '#edf2f7'
|
|
1454
|
+
ctx.font = 12 / safeScale + 'px Inter, system-ui, sans-serif'
|
|
1455
|
+
ctx.textAlign = 'center'
|
|
1456
|
+
ctx.textBaseline = 'top'
|
|
1457
|
+
ctx.fillText(cluster.representative.title.slice(0, 28), cluster.x, cluster.y + (radiusPx + 9) / safeScale)
|
|
1458
|
+
}
|
|
1374
1459
|
// Keep cluster markers minimal and faster to draw on large graphs.
|
|
1375
1460
|
})
|
|
1376
1461
|
} else {
|
package/package.json
CHANGED