@andespindola/brainlink 0.1.0-beta.142 → 0.1.0-beta.143

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.
@@ -0,0 +1,534 @@
1
+ export const createClientRenderWorkerJs = () => `let canvas = null
2
+ let gl = null
3
+ let viewportWidth = 320
4
+ let viewportHeight = 320
5
+ let devicePixelRatio = 1
6
+ const camera = { x: 0, y: 0, scale: 1 }
7
+ const state = {
8
+ nodeCount: 0,
9
+ edgeCount: 0,
10
+ ids: [],
11
+ titles: [],
12
+ kinds: [],
13
+ x: new Float32Array(0),
14
+ y: new Float32Array(0),
15
+ relevance: new Float32Array(0),
16
+ radius: new Float32Array(0),
17
+ visible: new Uint8Array(0),
18
+ highlighted: new Uint8Array(0),
19
+ selected: new Uint8Array(0),
20
+ edgeSource: new Uint32Array(0),
21
+ edgeTarget: new Uint32Array(0),
22
+ edgeWeight: new Float32Array(0)
23
+ }
24
+ const nodeIndexById = new Map()
25
+ const highlightedIds = new Set()
26
+ let selectedNodeId = null
27
+ let dirty = true
28
+ let running = false
29
+ let hoverX = null
30
+ let hoverY = null
31
+ let lastFrameAt = 0
32
+
33
+ const defaultTheme = {
34
+ node: [0.68, 0.72, 0.78, 1],
35
+ nodeCluster: [0.42, 0.76, 0.92, 1],
36
+ nodeHighlight: [0.95, 0.76, 0.22, 1],
37
+ nodeSelected: [0.99, 0.99, 1, 1],
38
+ edge: [0.58, 0.64, 0.74, 0.24],
39
+ edgeHeavy: [0.78, 0.84, 0.92, 0.44],
40
+ clear: [0.05, 0.06, 0.08, 1]
41
+ }
42
+
43
+ const theme = { ...defaultTheme }
44
+
45
+ const createShader = (type, source) => {
46
+ const shader = gl.createShader(type)
47
+ if (!shader) return null
48
+ gl.shaderSource(shader, source)
49
+ gl.compileShader(shader)
50
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
51
+ gl.deleteShader(shader)
52
+ return null
53
+ }
54
+ return shader
55
+ }
56
+
57
+ const createProgram = (vertexSource, fragmentSource) => {
58
+ const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
59
+ const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
60
+ if (!vertexShader || !fragmentShader) return null
61
+ const program = gl.createProgram()
62
+ if (!program) return null
63
+ gl.attachShader(program, vertexShader)
64
+ gl.attachShader(program, fragmentShader)
65
+ gl.linkProgram(program)
66
+ gl.deleteShader(vertexShader)
67
+ gl.deleteShader(fragmentShader)
68
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
69
+ gl.deleteProgram(program)
70
+ return null
71
+ }
72
+ return program
73
+ }
74
+
75
+ let lineProgram = null
76
+ let pointProgram = null
77
+ let lineBuffer = null
78
+ let pointPositionBuffer = null
79
+ let pointSizeBuffer = null
80
+ let linePositionLocation = -1
81
+ let lineResolutionLocation = null
82
+ let lineColorLocation = null
83
+ let pointPositionLocation = -1
84
+ let pointSizeLocation = -1
85
+ let pointResolutionLocation = null
86
+ let pointColorLocation = null
87
+
88
+ const initWebGl = () => {
89
+ if (!canvas) {
90
+ return false
91
+ }
92
+
93
+ gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
94
+ canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
95
+
96
+ if (!gl) {
97
+ return false
98
+ }
99
+
100
+ lineProgram = createProgram(
101
+ 'attribute vec2 a_position; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); }',
102
+ 'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
103
+ )
104
+
105
+ pointProgram = createProgram(
106
+ 'attribute vec2 a_position; attribute float a_size; uniform vec2 u_resolution; void main() { vec2 zeroToOne = a_position / u_resolution; vec2 clip = zeroToOne * 2.0 - 1.0; gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0); gl_PointSize = a_size; }',
107
+ 'precision mediump float; uniform vec4 u_color; void main() { vec2 center = gl_PointCoord - vec2(0.5); float distanceFromCenter = length(center); if (distanceFromCenter > 0.5) discard; float alpha = smoothstep(0.5, 0.38, distanceFromCenter); gl_FragColor = vec4(u_color.rgb, u_color.a * alpha); }'
108
+ )
109
+
110
+ if (!lineProgram || !pointProgram) {
111
+ return false
112
+ }
113
+
114
+ lineBuffer = gl.createBuffer()
115
+ pointPositionBuffer = gl.createBuffer()
116
+ pointSizeBuffer = gl.createBuffer()
117
+ if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
118
+ return false
119
+ }
120
+
121
+ linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
122
+ lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
123
+ lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
124
+ pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
125
+ pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
126
+ pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
127
+ pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
128
+
129
+ gl.enable(gl.BLEND)
130
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
131
+ gl.disable(gl.DEPTH_TEST)
132
+
133
+ return true
134
+ }
135
+
136
+ const resizeCanvas = (width, height, ratio) => {
137
+ viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
138
+ viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
139
+ devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
140
+
141
+ if (!canvas) return
142
+
143
+ canvas.width = Math.floor(viewportWidth * devicePixelRatio)
144
+ canvas.height = Math.floor(viewportHeight * devicePixelRatio)
145
+ if (gl) {
146
+ gl.viewport(0, 0, canvas.width, canvas.height)
147
+ }
148
+ dirty = true
149
+ }
150
+
151
+ const toScreenPoint = (x, y) => {
152
+ const sx = (x * camera.scale + camera.x) * devicePixelRatio
153
+ const sy = (y * camera.scale + camera.y) * devicePixelRatio
154
+ return [sx, sy]
155
+ }
156
+
157
+ const ensureNodeCapacity = (count) => {
158
+ if (state.x.length >= count) {
159
+ return
160
+ }
161
+
162
+ const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
163
+ state.x = new Float32Array(nextCapacity)
164
+ state.y = new Float32Array(nextCapacity)
165
+ state.relevance = new Float32Array(nextCapacity)
166
+ state.radius = new Float32Array(nextCapacity)
167
+ state.visible = new Uint8Array(nextCapacity)
168
+ state.highlighted = new Uint8Array(nextCapacity)
169
+ state.selected = new Uint8Array(nextCapacity)
170
+ }
171
+
172
+ const ensureEdgeCapacity = (count) => {
173
+ if (state.edgeSource.length >= count) {
174
+ return
175
+ }
176
+
177
+ const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
178
+ state.edgeSource = new Uint32Array(nextCapacity)
179
+ state.edgeTarget = new Uint32Array(nextCapacity)
180
+ state.edgeWeight = new Float32Array(nextCapacity)
181
+ }
182
+
183
+ const nodeRadius = (relevance, kind) => {
184
+ const base = kind === 'cluster' ? 7.8 : 4.6
185
+ const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
186
+ return base + modifier
187
+ }
188
+
189
+ const loadChunk = (chunk) => {
190
+ const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
191
+ const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
192
+
193
+ ensureNodeCapacity(nodes.length)
194
+ nodeIndexById.clear()
195
+ state.ids = new Array(nodes.length)
196
+ state.titles = new Array(nodes.length)
197
+ state.kinds = new Array(nodes.length)
198
+
199
+ for (let index = 0; index < nodes.length; index += 1) {
200
+ const row = nodes[index]
201
+ const id = typeof row?.[0] === 'string' ? row[0] : ''
202
+ if (!id) {
203
+ continue
204
+ }
205
+ const title = typeof row?.[1] === 'string' ? row[1] : id
206
+ const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
207
+ const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
208
+ const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
209
+ const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
210
+
211
+ state.ids[index] = id
212
+ state.titles[index] = title
213
+ state.kinds[index] = kind
214
+ state.x[index] = x
215
+ state.y[index] = y
216
+ state.relevance[index] = relevance
217
+ state.radius[index] = nodeRadius(relevance, kind)
218
+ state.visible[index] = 0
219
+ state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
220
+ state.selected[index] = selectedNodeId === id ? 1 : 0
221
+ nodeIndexById.set(id, index)
222
+ }
223
+
224
+ state.nodeCount = nodes.length
225
+
226
+ ensureEdgeCapacity(edges.length)
227
+ let edgeCount = 0
228
+ for (let index = 0; index < edges.length; index += 1) {
229
+ const row = edges[index]
230
+ const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
231
+ const targetId = typeof row?.[1] === 'string' ? row[1] : ''
232
+ const source = nodeIndexById.get(sourceId)
233
+ const target = nodeIndexById.get(targetId)
234
+ if (source === undefined || target === undefined || source === target) {
235
+ continue
236
+ }
237
+ state.edgeSource[edgeCount] = source
238
+ state.edgeTarget[edgeCount] = target
239
+ state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
240
+ edgeCount += 1
241
+ }
242
+
243
+ state.edgeCount = edgeCount
244
+ dirty = true
245
+ }
246
+
247
+ const cullVisibleNodes = () => {
248
+ const minX = -280
249
+ const minY = -280
250
+ const maxX = viewportWidth + 280
251
+ const maxY = viewportHeight + 280
252
+
253
+ for (let index = 0; index < state.nodeCount; index += 1) {
254
+ const radius = state.radius[index] * camera.scale
255
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
256
+ const screenX = sx / devicePixelRatio
257
+ const screenY = sy / devicePixelRatio
258
+ state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
259
+ }
260
+ }
261
+
262
+ const drawEdges = () => {
263
+ if (!gl || state.edgeCount === 0) return
264
+
265
+ const positions = []
266
+ for (let index = 0; index < state.edgeCount; index += 1) {
267
+ const source = state.edgeSource[index]
268
+ const target = state.edgeTarget[index]
269
+ if (state.visible[source] === 0 && state.visible[target] === 0) {
270
+ continue
271
+ }
272
+ const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
273
+ const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
274
+ positions.push(sx, sy, tx, ty)
275
+ }
276
+
277
+ if (positions.length === 0) return
278
+
279
+ gl.useProgram(lineProgram)
280
+ gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
281
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
282
+ gl.enableVertexAttribArray(linePositionLocation)
283
+ gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
284
+ gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
285
+ gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
286
+ gl.lineWidth(1)
287
+ gl.drawArrays(gl.LINES, 0, positions.length / 2)
288
+ }
289
+
290
+ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
291
+ if (!gl || state.nodeCount === 0) return
292
+
293
+ const positions = []
294
+ const sizes = []
295
+ for (let index = 0; index < state.nodeCount; index += 1) {
296
+ if (!predicate(index)) continue
297
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
298
+ positions.push(sx, sy)
299
+ sizes.push(Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost))
300
+ }
301
+
302
+ if (positions.length === 0) return
303
+
304
+ gl.useProgram(pointProgram)
305
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
306
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STREAM_DRAW)
307
+ gl.enableVertexAttribArray(pointPositionLocation)
308
+ gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
309
+
310
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
311
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(sizes), gl.STREAM_DRAW)
312
+ gl.enableVertexAttribArray(pointSizeLocation)
313
+ gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
314
+
315
+ gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
316
+ gl.uniform4fv(pointColorLocation, color)
317
+ gl.drawArrays(gl.POINTS, 0, positions.length / 2)
318
+ }
319
+
320
+ const clear = () => {
321
+ if (!gl || !canvas) return
322
+ gl.viewport(0, 0, canvas.width, canvas.height)
323
+ gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
324
+ gl.clear(gl.COLOR_BUFFER_BIT)
325
+ }
326
+
327
+ const renderFrame = (now) => {
328
+ const delta = now - lastFrameAt
329
+ const minInterval = state.nodeCount > 20000 ? 22 : 16
330
+ if (delta < minInterval && running) {
331
+ scheduleNextFrame()
332
+ return
333
+ }
334
+ lastFrameAt = now
335
+
336
+ if (!gl) {
337
+ scheduleNextFrame()
338
+ return
339
+ }
340
+
341
+ cullVisibleNodes()
342
+ clear()
343
+ drawEdges()
344
+
345
+ drawNodeLayer(
346
+ (index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
347
+ theme.node,
348
+ 1
349
+ )
350
+
351
+ drawNodeLayer(
352
+ (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
353
+ theme.nodeCluster,
354
+ 1.15
355
+ )
356
+
357
+ drawNodeLayer(
358
+ (index) => state.visible[index] === 1 && state.highlighted[index] === 1,
359
+ theme.nodeHighlight,
360
+ 1.22
361
+ )
362
+
363
+ drawNodeLayer(
364
+ (index) => state.visible[index] === 1 && state.selected[index] === 1,
365
+ theme.nodeSelected,
366
+ 1.32
367
+ )
368
+
369
+ if (dirty) {
370
+ postMessage({
371
+ type: 'frame-stats',
372
+ visibleNodes: (() => {
373
+ let count = 0
374
+ for (let index = 0; index < state.nodeCount; index += 1) {
375
+ if (state.visible[index] === 1) count += 1
376
+ }
377
+ return count
378
+ })(),
379
+ visibleEdges: state.edgeCount
380
+ })
381
+ dirty = false
382
+ }
383
+
384
+ scheduleNextFrame()
385
+ }
386
+
387
+ const scheduleNextFrame = () => {
388
+ const raf = self.requestAnimationFrame
389
+ if (typeof raf === 'function') {
390
+ raf.call(self, renderFrame)
391
+ return
392
+ }
393
+ setTimeout(() => renderFrame(performance.now()), 16)
394
+ }
395
+
396
+ const setCamera = (nextCamera) => {
397
+ if (!nextCamera || typeof nextCamera !== 'object') {
398
+ return
399
+ }
400
+ camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
401
+ camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
402
+ camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
403
+ dirty = true
404
+ }
405
+
406
+ const worldAtScreen = (screenX, screenY) => {
407
+ const x = (screenX - camera.x) / camera.scale
408
+ const y = (screenY - camera.y) / camera.scale
409
+ return [x, y]
410
+ }
411
+
412
+ const pickNode = (screenX, screenY) => {
413
+ const [worldX, worldY] = worldAtScreen(screenX, screenY)
414
+ let bestIndex = -1
415
+ let bestDistance = Infinity
416
+
417
+ for (let index = 0; index < state.nodeCount; index += 1) {
418
+ if (state.visible[index] === 0) continue
419
+ const dx = state.x[index] - worldX
420
+ const dy = state.y[index] - worldY
421
+ const distance = Math.hypot(dx, dy)
422
+ const maxDistance = state.radius[index] * 1.2
423
+ if (distance <= maxDistance && distance < bestDistance) {
424
+ bestDistance = distance
425
+ bestIndex = index
426
+ }
427
+ }
428
+
429
+ if (bestIndex < 0) {
430
+ return null
431
+ }
432
+
433
+ return {
434
+ id: state.ids[bestIndex],
435
+ title: state.titles[bestIndex],
436
+ kind: state.kinds[bestIndex],
437
+ x: state.x[bestIndex],
438
+ y: state.y[bestIndex]
439
+ }
440
+ }
441
+
442
+ const setHighlights = (ids) => {
443
+ highlightedIds.clear()
444
+ const list = Array.isArray(ids) ? ids : []
445
+ for (let index = 0; index < list.length; index += 1) {
446
+ const id = list[index]
447
+ if (typeof id === 'string' && id.length > 0) {
448
+ highlightedIds.add(id)
449
+ }
450
+ }
451
+
452
+ for (let index = 0; index < state.nodeCount; index += 1) {
453
+ state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
454
+ }
455
+ dirty = true
456
+ }
457
+
458
+ const setSelected = (id) => {
459
+ selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
460
+ for (let index = 0; index < state.nodeCount; index += 1) {
461
+ state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
462
+ }
463
+ dirty = true
464
+ }
465
+
466
+ self.onmessage = (event) => {
467
+ const payload = event.data
468
+ if (!payload || typeof payload !== 'object') {
469
+ return
470
+ }
471
+
472
+ if (payload.type === 'init') {
473
+ canvas = payload.canvas
474
+ if (payload.theme && typeof payload.theme === 'object') {
475
+ Object.assign(theme, payload.theme)
476
+ }
477
+ const initialized = initWebGl()
478
+ if (!initialized) {
479
+ postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
480
+ return
481
+ }
482
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
483
+ setCamera(payload.camera)
484
+ running = true
485
+ scheduleNextFrame()
486
+ postMessage({ type: 'ready' })
487
+ return
488
+ }
489
+
490
+ if (payload.type === 'resize') {
491
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
492
+ return
493
+ }
494
+
495
+ if (payload.type === 'camera') {
496
+ setCamera(payload.camera)
497
+ return
498
+ }
499
+
500
+ if (payload.type === 'chunk') {
501
+ loadChunk(payload.chunk)
502
+ return
503
+ }
504
+
505
+ if (payload.type === 'highlight') {
506
+ setHighlights(payload.ids)
507
+ return
508
+ }
509
+
510
+ if (payload.type === 'select') {
511
+ setSelected(payload.id)
512
+ return
513
+ }
514
+
515
+ if (payload.type === 'pick') {
516
+ const node = pickNode(
517
+ Number.isFinite(payload.x) ? Number(payload.x) : 0,
518
+ Number.isFinite(payload.y) ? Number(payload.y) : 0
519
+ )
520
+ postMessage({
521
+ type: 'pick-result',
522
+ requestId: payload.requestId,
523
+ node
524
+ })
525
+ return
526
+ }
527
+
528
+ if (payload.type === 'pointer') {
529
+ hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
530
+ hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
531
+ return
532
+ }
533
+ }
534
+ `;