@andespindola/brainlink 1.6.1 → 1.6.2

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.
@@ -1,678 +1,15 @@
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
- colorIndex: new Uint8Array(0),
18
- visible: new Uint8Array(0),
19
- highlighted: new Uint8Array(0),
20
- focused: new Uint8Array(0),
21
- selected: new Uint8Array(0),
22
- edgeSource: new Uint32Array(0),
23
- edgeTarget: new Uint32Array(0),
24
- edgeWeight: new Float32Array(0)
25
- }
26
- const nodeIndexById = new Map()
27
- const highlightedIds = new Set()
28
- const focusedIds = new Set()
29
- let selectedNodeId = null
30
- let dirty = true
31
- let renderScheduled = false
32
- let hoverX = null
33
- let hoverY = null
34
- let lastFrameAt = 0
35
- let lastVisibleEdges = 0
36
- let interactionUntil = 0
37
- let settledRenderTimer = null
38
- let edgePositionsBuffer = new Float32Array(0)
39
- let pointPositionsBuffer = new Float32Array(0)
40
- let pointSizesBuffer = new Float32Array(0)
41
-
42
- const defaultTheme = {
43
- node: [0.30, 0.56, 0.85, 1],
44
- nodeCluster: [0.18, 0.44, 0.71, 1],
45
- nodeHighlight: [0.95, 0.70, 0.25, 1],
46
- nodeSelected: [0.09, 0.13, 0.20, 1],
47
- nodePalette: [
48
- [0.30, 0.56, 0.85, 1],
49
- [0.40, 0.73, 0.43, 1],
50
- [0.94, 0.64, 0.23, 1],
51
- [0.85, 0.37, 0.55, 1],
52
- [0.55, 0.45, 0.85, 1],
53
- [0.33, 0.75, 0.77, 1],
54
- [0.93, 0.42, 0.34, 1],
55
- [0.60, 0.65, 0.70, 1],
56
- [0.72, 0.51, 0.33, 1],
57
- [0.44, 0.62, 0.85, 1]
58
- ],
59
- edge: [0.36, 0.46, 0.60, 0.26],
60
- edgeHeavy: [0.40, 0.52, 0.68, 0.5],
61
- clear: [0.031, 0.075, 0.114, 1]
62
- }
63
-
64
- const theme = { ...defaultTheme }
65
-
66
- const createShader = (type, source) => {
67
- const shader = gl.createShader(type)
68
- if (!shader) return null
69
- gl.shaderSource(shader, source)
70
- gl.compileShader(shader)
71
- if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
72
- gl.deleteShader(shader)
73
- return null
74
- }
75
- return shader
76
- }
77
-
78
- const createProgram = (vertexSource, fragmentSource) => {
79
- const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
80
- const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
81
- if (!vertexShader || !fragmentShader) return null
82
- const program = gl.createProgram()
83
- if (!program) return null
84
- gl.attachShader(program, vertexShader)
85
- gl.attachShader(program, fragmentShader)
86
- gl.linkProgram(program)
87
- gl.deleteShader(vertexShader)
88
- gl.deleteShader(fragmentShader)
89
- if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
90
- gl.deleteProgram(program)
91
- return null
92
- }
93
- return program
94
- }
95
-
96
- let lineProgram = null
97
- let pointProgram = null
98
- let lineBuffer = null
99
- let pointPositionBuffer = null
100
- let pointSizeBuffer = null
101
- let linePositionLocation = -1
102
- let lineResolutionLocation = null
103
- let lineColorLocation = null
104
- let pointPositionLocation = -1
105
- let pointSizeLocation = -1
106
- let pointResolutionLocation = null
107
- let pointColorLocation = null
108
-
109
- const initWebGl = () => {
110
- if (!canvas) {
111
- return false
112
- }
113
-
114
- gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
115
- canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
116
-
117
- if (!gl) {
118
- return false
119
- }
120
-
121
- lineProgram = createProgram(
122
- '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); }',
123
- 'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
124
- )
125
-
126
- pointProgram = createProgram(
127
- '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; }',
128
- '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); }'
129
- )
130
-
131
- if (!lineProgram || !pointProgram) {
132
- return false
133
- }
134
-
135
- lineBuffer = gl.createBuffer()
136
- pointPositionBuffer = gl.createBuffer()
137
- pointSizeBuffer = gl.createBuffer()
138
- if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
139
- return false
140
- }
141
-
142
- linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
143
- lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
144
- lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
145
- pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
146
- pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
147
- pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
148
- pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
149
-
150
- gl.enable(gl.BLEND)
151
- gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
152
- gl.disable(gl.DEPTH_TEST)
153
-
154
- return true
155
- }
156
-
157
- const ensureFloat32Capacity = (buffer, neededLength) => {
158
- if (buffer.length >= neededLength) {
159
- return buffer
160
- }
161
- const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
162
- return new Float32Array(next)
163
- }
164
-
165
- const resizeCanvas = (width, height, ratio) => {
166
- viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
167
- viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
168
- devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
169
-
170
- if (!canvas) return
171
-
172
- canvas.width = Math.floor(viewportWidth * devicePixelRatio)
173
- canvas.height = Math.floor(viewportHeight * devicePixelRatio)
174
- if (gl) {
175
- gl.viewport(0, 0, canvas.width, canvas.height)
176
- }
177
- dirty = true
178
- requestRender()
179
- }
180
-
181
- const toScreenPoint = (x, y) => {
182
- const sx = (x * camera.scale + camera.x) * devicePixelRatio
183
- const sy = (y * camera.scale + camera.y) * devicePixelRatio
184
- return [sx, sy]
185
- }
186
-
187
- const ensureNodeCapacity = (count) => {
188
- if (state.x.length >= count) {
189
- return
190
- }
191
-
192
- const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
193
- state.x = new Float32Array(nextCapacity)
194
- state.y = new Float32Array(nextCapacity)
195
- state.relevance = new Float32Array(nextCapacity)
196
- state.radius = new Float32Array(nextCapacity)
197
- state.colorIndex = new Uint8Array(nextCapacity)
198
- state.visible = new Uint8Array(nextCapacity)
199
- state.highlighted = new Uint8Array(nextCapacity)
200
- state.focused = new Uint8Array(nextCapacity)
201
- state.selected = new Uint8Array(nextCapacity)
202
- }
203
-
204
- const ensureEdgeCapacity = (count) => {
205
- if (state.edgeSource.length >= count) {
206
- return
207
- }
208
-
209
- const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
210
- state.edgeSource = new Uint32Array(nextCapacity)
211
- state.edgeTarget = new Uint32Array(nextCapacity)
212
- state.edgeWeight = new Float32Array(nextCapacity)
213
- }
214
-
215
- const nodeRadius = (relevance, kind) => {
216
- const base = kind === 'cluster' ? 11.2 : 7.2
217
- const modifier = Math.min(7.6, Math.max(0, relevance * 0.78))
218
- return base + modifier
219
- }
220
-
221
- const segmentColorIndex = (segment) => {
222
- const value = String(segment || '')
223
- let hash = 0
224
- for (let index = 0; index < value.length; index += 1) {
225
- hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
226
- }
227
- const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
228
- return Math.abs(hash) % palette.length
229
- }
230
-
231
- const loadChunk = (chunk) => {
232
- const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
233
- const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
234
-
235
- ensureNodeCapacity(nodes.length)
236
- nodeIndexById.clear()
237
- state.ids = new Array(nodes.length)
238
- state.titles = new Array(nodes.length)
239
- state.kinds = new Array(nodes.length)
240
-
241
- for (let index = 0; index < nodes.length; index += 1) {
242
- const row = nodes[index]
243
- const id = typeof row?.[0] === 'string' ? row[0] : ''
244
- if (!id) {
245
- continue
246
- }
247
- const title = typeof row?.[1] === 'string' ? row[1] : id
248
- const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
249
- const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
250
- const segment = typeof row?.[5] === 'string' ? row[5] : ''
251
- const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
252
- const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
253
-
254
- state.ids[index] = id
255
- state.titles[index] = title
256
- state.kinds[index] = kind
257
- state.x[index] = x
258
- state.y[index] = y
259
- state.relevance[index] = relevance
260
- state.radius[index] = nodeRadius(relevance, kind)
261
- state.colorIndex[index] = segmentColorIndex(segment || title)
262
- state.visible[index] = 0
263
- state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
264
- state.focused[index] = focusedIds.has(id) ? 1 : 0
265
- state.selected[index] = selectedNodeId === id ? 1 : 0
266
- nodeIndexById.set(id, index)
267
- }
268
-
269
- state.nodeCount = nodes.length
270
-
271
- ensureEdgeCapacity(edges.length)
272
- let edgeCount = 0
273
- for (let index = 0; index < edges.length; index += 1) {
274
- const row = edges[index]
275
- const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
276
- const targetId = typeof row?.[1] === 'string' ? row[1] : ''
277
- const source = nodeIndexById.get(sourceId)
278
- const target = nodeIndexById.get(targetId)
279
- if (source === undefined || target === undefined || source === target) {
280
- continue
281
- }
282
- state.edgeSource[edgeCount] = source
283
- state.edgeTarget[edgeCount] = target
284
- state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
285
- edgeCount += 1
286
- }
287
-
288
- state.edgeCount = edgeCount
289
- dirty = true
290
- }
291
-
292
- const cullVisibleNodes = () => {
293
- const minX = -280
294
- const minY = -280
295
- const maxX = viewportWidth + 280
296
- const maxY = viewportHeight + 280
297
-
298
- for (let index = 0; index < state.nodeCount; index += 1) {
299
- const radius = state.radius[index] * camera.scale
300
- const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
301
- const screenX = sx / devicePixelRatio
302
- const screenY = sy / devicePixelRatio
303
- state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
304
- }
305
- }
306
-
307
- const drawEdges = () => {
308
- if (!gl || state.edgeCount === 0) return
309
-
310
- edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
311
- let cursor = 0
312
- let visibleEdges = 0
313
- for (let index = 0; index < state.edgeCount; index += 1) {
314
- const source = state.edgeSource[index]
315
- const target = state.edgeTarget[index]
316
- if (state.visible[source] === 0 && state.visible[target] === 0) {
317
- continue
318
- }
319
- const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
320
- const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
321
- edgePositionsBuffer[cursor] = sx
322
- edgePositionsBuffer[cursor + 1] = sy
323
- edgePositionsBuffer[cursor + 2] = tx
324
- edgePositionsBuffer[cursor + 3] = ty
325
- cursor += 4
326
- visibleEdges += 1
327
- }
328
-
329
- lastVisibleEdges = visibleEdges
330
- if (cursor === 0) return
331
-
332
- gl.useProgram(lineProgram)
333
- gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
334
- gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
335
- gl.enableVertexAttribArray(linePositionLocation)
336
- gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
337
- gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
338
- gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
339
- gl.lineWidth(1)
340
- gl.drawArrays(gl.LINES, 0, cursor / 2)
341
- }
342
-
343
- const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
344
- if (!gl || state.nodeCount === 0) return
345
-
346
- pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
347
- pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
348
- let positionCursor = 0
349
- let sizeCursor = 0
350
- for (let index = 0; index < state.nodeCount; index += 1) {
351
- if (!predicate(index)) continue
352
- const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
353
- pointPositionsBuffer[positionCursor] = sx
354
- pointPositionsBuffer[positionCursor + 1] = sy
355
- // Screen-space floor so every node stays a visible dot at any zoom (a low
356
- // floor made nodes sub-pixel until zoomed in). Scaled by devicePixelRatio.
357
- pointSizesBuffer[sizeCursor] = Math.max(3.0 * devicePixelRatio, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
358
- positionCursor += 2
359
- sizeCursor += 1
360
- }
361
-
362
- if (positionCursor === 0) return
363
-
364
- gl.useProgram(pointProgram)
365
- gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
366
- gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
367
- gl.enableVertexAttribArray(pointPositionLocation)
368
- gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
369
-
370
- gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
371
- gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
372
- gl.enableVertexAttribArray(pointSizeLocation)
373
- gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
374
-
375
- gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
376
- gl.uniform4fv(pointColorLocation, color)
377
- gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
378
- }
379
-
380
- const drawColoredNodeLayer = (predicate, radiusBoost = 1) => {
381
- const palette = Array.isArray(theme.nodePalette) && theme.nodePalette.length > 0 ? theme.nodePalette : [theme.node]
382
- for (let colorIndex = 0; colorIndex < palette.length; colorIndex += 1) {
383
- drawNodeLayer((index) => predicate(index) && state.colorIndex[index] === colorIndex, palette[colorIndex], radiusBoost)
384
- }
385
- }
386
-
387
- const clear = () => {
388
- if (!gl || !canvas) return
389
- gl.viewport(0, 0, canvas.width, canvas.height)
390
- gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
391
- gl.clear(gl.COLOR_BUFFER_BIT)
392
- }
393
-
394
- const isCameraInteracting = (now) => now < interactionUntil
395
-
396
- const scheduleSettledRender = (now) => {
397
- if (settledRenderTimer) {
398
- return
399
- }
400
- const delay = Math.max(32, interactionUntil - now + 16)
401
- settledRenderTimer = setTimeout(() => {
402
- settledRenderTimer = null
403
- dirty = true
404
- requestRender()
405
- }, delay)
406
- }
407
-
408
- const renderFrame = (now) => {
409
- renderScheduled = false
410
- if (!dirty) {
411
- return
412
- }
413
-
414
- const delta = now - lastFrameAt
415
- const minInterval = state.nodeCount > 20000 ? 22 : 16
416
- if (delta < minInterval) {
417
- requestRender()
418
- return
419
- }
420
- lastFrameAt = now
421
-
422
- if (!gl) {
423
- return
424
- }
425
-
426
- cullVisibleNodes()
427
- clear()
428
- const cameraInteracting = isCameraInteracting(now)
429
- if (!cameraInteracting || state.edgeCount < 1200) {
430
- drawEdges()
431
- } else {
432
- lastVisibleEdges = 0
433
- scheduleSettledRender(now)
434
- }
435
-
436
- drawColoredNodeLayer(
437
- (index) => state.visible[index] === 1 && state.kinds[index] !== 'cluster' && state.selected[index] === 0 && state.highlighted[index] === 0 && state.focused[index] === 0,
438
- 1
439
- )
440
-
441
- drawColoredNodeLayer(
442
- (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
443
- 1.15
444
- )
445
-
446
- drawNodeLayer(
447
- (index) => state.visible[index] === 1 && state.highlighted[index] === 1,
448
- theme.nodeHighlight,
449
- 1.22
450
- )
451
-
452
- drawNodeLayer(
453
- (index) => state.visible[index] === 1 && state.focused[index] === 1,
454
- theme.nodeHighlight,
455
- 1.12
456
- )
457
-
458
- drawNodeLayer(
459
- (index) => state.visible[index] === 1 && state.selected[index] === 1,
460
- theme.nodeSelected,
461
- 1.32
462
- )
463
-
464
- if (dirty) {
465
- postMessage({
466
- type: 'frame-stats',
467
- visibleNodes: (() => {
468
- let count = 0
469
- for (let index = 0; index < state.nodeCount; index += 1) {
470
- if (state.visible[index] === 1) count += 1
471
- }
472
- return count
473
- })(),
474
- visibleEdges: lastVisibleEdges
475
- })
476
- dirty = false
477
- }
478
- }
479
-
480
- const requestRender = () => {
481
- if (renderScheduled) {
482
- return
483
- }
484
- renderScheduled = true
485
- const raf = self.requestAnimationFrame
486
- if (typeof raf === 'function') {
487
- raf.call(self, renderFrame)
488
- return
489
- }
490
- setTimeout(() => renderFrame(performance.now()), 16)
491
- }
492
-
493
- const setCamera = (nextCamera) => {
494
- if (!nextCamera || typeof nextCamera !== 'object') {
495
- return
496
- }
497
- camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
498
- camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
499
- camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
500
- interactionUntil = performance.now() + 140
501
- dirty = true
502
- requestRender()
503
- }
504
-
505
- const worldAtScreen = (screenX, screenY) => {
506
- const x = (screenX - camera.x) / camera.scale
507
- const y = (screenY - camera.y) / camera.scale
508
- return [x, y]
509
- }
510
-
511
- const pickNode = (screenX, screenY) => {
512
- const [worldX, worldY] = worldAtScreen(screenX, screenY)
513
- let bestIndex = -1
514
- let bestDistance = Infinity
515
-
516
- for (let index = 0; index < state.nodeCount; index += 1) {
517
- if (state.visible[index] === 0) continue
518
- const dx = state.x[index] - worldX
519
- const dy = state.y[index] - worldY
520
- const distance = Math.hypot(dx, dy)
521
- const maxDistance = state.radius[index] * 1.2
522
- if (distance <= maxDistance && distance < bestDistance) {
523
- bestDistance = distance
524
- bestIndex = index
525
- }
526
- }
527
-
528
- if (bestIndex < 0) {
529
- return null
530
- }
531
-
532
- return {
533
- id: state.ids[bestIndex],
534
- title: state.titles[bestIndex],
535
- kind: state.kinds[bestIndex],
536
- x: state.x[bestIndex],
537
- y: state.y[bestIndex]
538
- }
539
- }
540
-
541
- const setHighlights = (ids) => {
542
- highlightedIds.clear()
543
- const list = Array.isArray(ids) ? ids : []
544
- for (let index = 0; index < list.length; index += 1) {
545
- const id = list[index]
546
- if (typeof id === 'string' && id.length > 0) {
547
- highlightedIds.add(id)
548
- }
549
- }
550
-
551
- for (let index = 0; index < state.nodeCount; index += 1) {
552
- state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
553
- }
554
- dirty = true
555
- requestRender()
556
- }
557
-
558
- const setFocus = (ids) => {
559
- focusedIds.clear()
560
- const list = Array.isArray(ids) ? ids : []
561
- for (let index = 0; index < list.length; index += 1) {
562
- const id = list[index]
563
- if (typeof id === 'string' && id.length > 0) {
564
- focusedIds.add(id)
565
- }
566
- }
567
-
568
- for (let index = 0; index < state.nodeCount; index += 1) {
569
- state.focused[index] = focusedIds.has(state.ids[index]) ? 1 : 0
570
- }
571
- dirty = true
572
- requestRender()
573
- }
574
-
575
- const setSelected = (id) => {
576
- selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
577
- for (let index = 0; index < state.nodeCount; index += 1) {
578
- state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
579
- }
580
- dirty = true
581
- requestRender()
582
- }
583
-
584
- const moveNode = (id, x, y) => {
585
- if (typeof id !== 'string' || !Number.isFinite(x) || !Number.isFinite(y)) {
586
- return
587
- }
588
-
589
- const index = nodeIndexById.get(id)
590
- if (index === undefined) {
591
- return
592
- }
593
-
594
- state.x[index] = x
595
- state.y[index] = y
596
- dirty = true
597
- requestRender()
598
- }
599
-
600
- self.onmessage = (event) => {
601
- const payload = event.data
602
- if (!payload || typeof payload !== 'object') {
603
- return
604
- }
605
-
606
- if (payload.type === 'init') {
607
- canvas = payload.canvas
608
- if (payload.theme && typeof payload.theme === 'object') {
609
- Object.assign(theme, payload.theme)
610
- }
611
- const initialized = initWebGl()
612
- if (!initialized) {
613
- postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
614
- return
615
- }
616
- resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
617
- setCamera(payload.camera)
618
- requestRender()
619
- postMessage({ type: 'ready' })
620
- return
621
- }
622
-
623
- if (payload.type === 'resize') {
624
- resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
625
- return
626
- }
627
-
628
- if (payload.type === 'camera') {
629
- setCamera(payload.camera)
630
- return
631
- }
632
-
633
- if (payload.type === 'chunk') {
634
- loadChunk(payload.chunk)
635
- requestRender()
636
- return
637
- }
638
-
639
- if (payload.type === 'highlight') {
640
- setHighlights(payload.ids)
641
- return
642
- }
643
-
644
- if (payload.type === 'focus') {
645
- setFocus(payload.ids)
646
- return
647
- }
648
-
649
- if (payload.type === 'select') {
650
- setSelected(payload.id)
651
- return
652
- }
653
-
654
- if (payload.type === 'move-node') {
655
- moveNode(payload.id, Number(payload.x), Number(payload.y))
656
- return
657
- }
658
-
659
- if (payload.type === 'pick') {
660
- const node = pickNode(
661
- Number.isFinite(payload.x) ? Number(payload.x) : 0,
662
- Number.isFinite(payload.y) ? Number(payload.y) : 0
663
- )
664
- postMessage({
665
- type: 'pick-result',
666
- requestId: payload.requestId,
667
- node
668
- })
669
- return
670
- }
671
-
672
- if (payload.type === 'pointer') {
673
- hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
674
- hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
675
- return
676
- }
677
- }
1
+ import { createGraphRendererJs } from './client/graph-renderer.js';
2
+ // Script servido em /render-worker.js. Reaproveita a factory compartilhada do
3
+ // renderer e liga o host às primitivas do Worker (self.postMessage e
4
+ // self.requestAnimationFrame), preservando exatamente o protocolo anterior.
5
+ export const createClientRenderWorkerJs = () => `${createGraphRendererJs()}
6
+ const brainlinkRenderer = createGraphRenderer({
7
+ postMessage: (message) => self.postMessage(message),
8
+ requestAnimationFrame: (callback) =>
9
+ typeof self.requestAnimationFrame === 'function'
10
+ ? self.requestAnimationFrame(callback)
11
+ : setTimeout(() => callback(performance.now()), 16)
12
+ })
13
+
14
+ self.onmessage = (event) => brainlinkRenderer.handleMessage(event && event.data)
678
15
  `;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@andespindola/brainlink",
3
- "version": "1.6.1",
3
+ "version": "1.6.2",
4
4
  "description": "Local-first knowledge memory for agents with Markdown, backlinks, indexing and context retrieval.",
5
5
  "type": "module",
6
6
  "license": "MIT",