@andespindola/brainlink 0.1.0-beta.15 → 0.1.0-beta.16
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.
|
@@ -2,12 +2,17 @@ export const createClientJs = () => `const canvas = document.getElementById('gra
|
|
|
2
2
|
const ctx = canvas.getContext('2d')
|
|
3
3
|
const largeGraphNodeThreshold = 4000
|
|
4
4
|
const largeGraphEdgeRenderLimit = 16000
|
|
5
|
+
const renderNodeBudget = 1800
|
|
6
|
+
const minNodePixelRadius = 1.8
|
|
7
|
+
const viewportPaddingPx = 280
|
|
5
8
|
const state = {
|
|
6
9
|
graph: { nodes: [], edges: [] },
|
|
7
10
|
nodes: [],
|
|
8
11
|
edges: [],
|
|
9
12
|
visibleNodes: [],
|
|
10
13
|
visibleEdges: [],
|
|
14
|
+
renderNodes: [],
|
|
15
|
+
renderEdges: [],
|
|
11
16
|
nodeDegrees: new Map(),
|
|
12
17
|
selected: null,
|
|
13
18
|
hovered: null,
|
|
@@ -133,7 +138,7 @@ const graphBounds = nodes => {
|
|
|
133
138
|
let maxY = Number.NEGATIVE_INFINITY
|
|
134
139
|
|
|
135
140
|
nodes.forEach(node => {
|
|
136
|
-
const radius =
|
|
141
|
+
const radius = baseNodeRadius(node)
|
|
137
142
|
minX = Math.min(minX, node.x - radius)
|
|
138
143
|
maxX = Math.max(maxX, node.x + radius)
|
|
139
144
|
minY = Math.min(minY, node.y - radius)
|
|
@@ -165,7 +170,9 @@ const fitView = (options = { useFiltered: true }) => {
|
|
|
165
170
|
const padding = 100
|
|
166
171
|
const scaleX = width / (bounds.width + padding * 2)
|
|
167
172
|
const scaleY = height / (bounds.height + padding * 2)
|
|
168
|
-
const
|
|
173
|
+
const fitScale = clampScale(Math.min(scaleX, scaleY))
|
|
174
|
+
const minimumLargeGraphScale = nodes.length > largeGraphNodeThreshold ? 0.13 : zoomRange.min
|
|
175
|
+
const scale = Math.max(fitScale, minimumLargeGraphScale)
|
|
169
176
|
const centerX = (bounds.minX + bounds.maxX) / 2
|
|
170
177
|
const centerY = (bounds.minY + bounds.maxY) / 2
|
|
171
178
|
|
|
@@ -267,8 +274,8 @@ const scheduleContentFilterSync = () => {
|
|
|
267
274
|
}
|
|
268
275
|
|
|
269
276
|
const tick = delta => {
|
|
270
|
-
const nodes = state.visibleNodes
|
|
271
|
-
const edges = state.visibleEdges
|
|
277
|
+
const nodes = state.renderNodes.length > 0 ? state.renderNodes : state.visibleNodes
|
|
278
|
+
const edges = state.renderEdges.length > 0 ? state.renderEdges : state.visibleEdges
|
|
272
279
|
if (nodes.length > 1200) {
|
|
273
280
|
return
|
|
274
281
|
}
|
|
@@ -334,7 +341,7 @@ const hitNode = point => {
|
|
|
334
341
|
return null
|
|
335
342
|
}
|
|
336
343
|
|
|
337
|
-
const nodes = state.
|
|
344
|
+
const nodes = state.renderNodes
|
|
338
345
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
|
339
346
|
const node = nodes[index]
|
|
340
347
|
const radius = nodeRadius(node)
|
|
@@ -343,15 +350,88 @@ const hitNode = point => {
|
|
|
343
350
|
return null
|
|
344
351
|
}
|
|
345
352
|
|
|
346
|
-
const
|
|
353
|
+
const baseNodeRadius = node => {
|
|
347
354
|
const degree = state.nodeDegrees.get(node.id) ?? 0
|
|
348
355
|
return 9 + Math.min(degree, 8) * 1.6
|
|
349
356
|
}
|
|
350
357
|
|
|
358
|
+
const nodeRadius = node => Math.max(baseNodeRadius(node), minNodePixelRadius / Math.max(state.transform.scale, 0.0001))
|
|
359
|
+
|
|
360
|
+
const worldViewportBounds = () => {
|
|
361
|
+
const rect = canvas.getBoundingClientRect()
|
|
362
|
+
const width = Math.max(rect.width, 320)
|
|
363
|
+
const height = Math.max(rect.height, 320)
|
|
364
|
+
const padding = viewportPaddingPx
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
minX: (-state.transform.x - padding) / state.transform.scale,
|
|
368
|
+
maxX: (width - state.transform.x + padding) / state.transform.scale,
|
|
369
|
+
minY: (-state.transform.y - padding) / state.transform.scale,
|
|
370
|
+
maxY: (height - state.transform.y + padding) / state.transform.scale
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const isNodeInViewport = (node, viewport) =>
|
|
375
|
+
node.x >= viewport.minX &&
|
|
376
|
+
node.x <= viewport.maxX &&
|
|
377
|
+
node.y >= viewport.minY &&
|
|
378
|
+
node.y <= viewport.maxY
|
|
379
|
+
|
|
380
|
+
const viewportNodeStride = () => {
|
|
381
|
+
if (state.nodes.length <= largeGraphNodeThreshold) {
|
|
382
|
+
return 1
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
if (state.transform.scale >= 0.95) {
|
|
386
|
+
return 1
|
|
387
|
+
}
|
|
388
|
+
if (state.transform.scale >= 0.7) {
|
|
389
|
+
return 2
|
|
390
|
+
}
|
|
391
|
+
if (state.transform.scale >= 0.48) {
|
|
392
|
+
return 3
|
|
393
|
+
}
|
|
394
|
+
if (state.transform.scale >= 0.28) {
|
|
395
|
+
return 5
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return 8
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const computeRenderVisibility = () => {
|
|
402
|
+
const viewport = worldViewportBounds()
|
|
403
|
+
const stride = viewportNodeStride()
|
|
404
|
+
const picked = []
|
|
405
|
+
|
|
406
|
+
for (let index = 0; index < state.visibleNodes.length; index += 1) {
|
|
407
|
+
const node = state.visibleNodes[index]
|
|
408
|
+
if (!isNodeInViewport(node, viewport)) {
|
|
409
|
+
continue
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const isPriority =
|
|
413
|
+
node.id === state.selected?.id ||
|
|
414
|
+
node.id === state.hovered?.id ||
|
|
415
|
+
node.id === state.pointer.dragNode?.id
|
|
416
|
+
if (isPriority || index % stride === 0) {
|
|
417
|
+
picked.push(node)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const nodes = picked.length > renderNodeBudget
|
|
422
|
+
? picked.slice(0, renderNodeBudget)
|
|
423
|
+
: picked
|
|
424
|
+
const nodeIds = new Set(nodes.map((node) => node.id))
|
|
425
|
+
const edges = state.visibleEdges.filter((edge) => nodeIds.has(edge.source) && edge.target && nodeIds.has(edge.target))
|
|
426
|
+
|
|
427
|
+
state.renderNodes = nodes
|
|
428
|
+
state.renderEdges = edges
|
|
429
|
+
}
|
|
430
|
+
|
|
351
431
|
const render = now => {
|
|
352
432
|
const delta = now - state.last
|
|
353
433
|
state.last = now
|
|
354
|
-
const minFrameIntervalMs = state.nodes.length > largeGraphNodeThreshold ?
|
|
434
|
+
const minFrameIntervalMs = state.nodes.length > largeGraphNodeThreshold ? 48 : 16
|
|
355
435
|
if (delta < minFrameIntervalMs) {
|
|
356
436
|
requestAnimationFrame(render)
|
|
357
437
|
return
|
|
@@ -372,10 +452,11 @@ const render = now => {
|
|
|
372
452
|
ctx.translate(state.transform.x, state.transform.y)
|
|
373
453
|
ctx.scale(state.transform.scale, state.transform.scale)
|
|
374
454
|
|
|
455
|
+
computeRenderVisibility()
|
|
375
456
|
tick(delta)
|
|
376
457
|
const drawEdges = !(state.nodes.length > largeGraphNodeThreshold && state.transform.scale < 0.22)
|
|
377
458
|
if (drawEdges) {
|
|
378
|
-
state.
|
|
459
|
+
state.renderEdges.forEach(edge => {
|
|
379
460
|
const selectedEdge = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
380
461
|
ctx.beginPath()
|
|
381
462
|
ctx.moveTo(edge.sourceNode.x, edge.sourceNode.y)
|
|
@@ -386,7 +467,7 @@ const render = now => {
|
|
|
386
467
|
})
|
|
387
468
|
}
|
|
388
469
|
|
|
389
|
-
state.
|
|
470
|
+
state.renderNodes.forEach(node => {
|
|
390
471
|
const radius = nodeRadius(node)
|
|
391
472
|
const isSelected = state.selected?.id === node.id
|
|
392
473
|
const isHovered = state.hovered?.id === node.id
|
|
@@ -416,6 +497,12 @@ const render = now => {
|
|
|
416
497
|
})
|
|
417
498
|
|
|
418
499
|
ctx.restore()
|
|
500
|
+
if (state.renderNodes.length === 0) {
|
|
501
|
+
ctx.fillStyle = '#99a5b5'
|
|
502
|
+
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
503
|
+
ctx.textAlign = 'center'
|
|
504
|
+
ctx.fillText('Move or zoom to reveal nearby notes', width / 2, height / 2)
|
|
505
|
+
}
|
|
419
506
|
requestAnimationFrame(render)
|
|
420
507
|
}
|
|
421
508
|
|
package/package.json
CHANGED