@andespindola/brainlink 0.1.0-beta.16 → 0.1.0-beta.161

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.
Files changed (50) hide show
  1. package/AGENTS.md +9 -6
  2. package/CHANGELOG.md +27 -0
  3. package/COPYRIGHT.md +5 -0
  4. package/README.md +177 -20
  5. package/dist/application/add-note.js +13 -44
  6. package/dist/application/auto-migrate-configured-vault.js +37 -0
  7. package/dist/application/build-context.js +64 -3
  8. package/dist/application/canonical-context-links.js +209 -0
  9. package/dist/application/dedupe-notes.js +226 -0
  10. package/dist/application/frontend/client-css.js +258 -51
  11. package/dist/application/frontend/client-html.js +50 -27
  12. package/dist/application/frontend/client-js.js +1369 -605
  13. package/dist/application/frontend/client-render-worker-js.js +645 -0
  14. package/dist/application/frontend/client-worker-js.js +66 -0
  15. package/dist/application/get-graph-contexts.js +33 -0
  16. package/dist/application/get-graph-layout.js +62 -8
  17. package/dist/application/get-graph-stream-chunk.js +326 -0
  18. package/dist/application/get-graph-view.js +246 -0
  19. package/dist/application/graph-view-state.js +66 -0
  20. package/dist/application/import-legacy-sqlite.js +266 -0
  21. package/dist/application/index-vault.js +262 -23
  22. package/dist/application/migrate-context-links.js +79 -0
  23. package/dist/application/offline-pack-backup.js +44 -0
  24. package/dist/application/search-graph-node-ids.js +63 -3
  25. package/dist/application/server/routes.js +247 -7
  26. package/dist/application/start-server.js +75 -4
  27. package/dist/application/watch-vault.js +23 -2
  28. package/dist/cli/commands/agent-commands.js +7 -0
  29. package/dist/cli/commands/write-commands.js +924 -14
  30. package/dist/cli/runtime.js +10 -2
  31. package/dist/domain/context.js +54 -11
  32. package/dist/domain/graph-contexts.js +180 -0
  33. package/dist/domain/graph-layout.js +389 -18
  34. package/dist/domain/markdown.js +53 -9
  35. package/dist/domain/middle-out.js +18 -0
  36. package/dist/infrastructure/config.js +121 -4
  37. package/dist/infrastructure/file-index.js +76 -6
  38. package/dist/infrastructure/file-system-vault.js +15 -0
  39. package/dist/infrastructure/index-state.js +58 -0
  40. package/dist/infrastructure/private-pack-codec.js +71 -10
  41. package/dist/infrastructure/search-packs.js +286 -15
  42. package/dist/infrastructure/vault-migration-state.js +69 -0
  43. package/dist/infrastructure/volatile-memory.js +100 -0
  44. package/dist/mcp/runtime.js +20 -0
  45. package/dist/mcp/server.js +39 -11
  46. package/dist/mcp/tools.js +183 -7
  47. package/docs/AGENT_USAGE.md +96 -5
  48. package/docs/ARCHITECTURE.md +8 -0
  49. package/docs/QUICKSTART.md +7 -0
  50. package/package.json +7 -2
@@ -0,0 +1,645 @@
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
+ focused: new Uint8Array(0),
20
+ selected: new Uint8Array(0),
21
+ edgeSource: new Uint32Array(0),
22
+ edgeTarget: new Uint32Array(0),
23
+ edgeWeight: new Float32Array(0)
24
+ }
25
+ const nodeIndexById = new Map()
26
+ const highlightedIds = new Set()
27
+ const focusedIds = new Set()
28
+ let selectedNodeId = null
29
+ let dirty = true
30
+ let renderScheduled = false
31
+ let hoverX = null
32
+ let hoverY = null
33
+ let lastFrameAt = 0
34
+ let lastVisibleEdges = 0
35
+ let interactionUntil = 0
36
+ let settledRenderTimer = null
37
+ let edgePositionsBuffer = new Float32Array(0)
38
+ let pointPositionsBuffer = new Float32Array(0)
39
+ let pointSizesBuffer = new Float32Array(0)
40
+
41
+ const defaultTheme = {
42
+ node: [0.68, 0.72, 0.78, 1],
43
+ nodeCluster: [0.42, 0.76, 0.92, 1],
44
+ nodeHighlight: [0.95, 0.76, 0.22, 1],
45
+ nodeSelected: [0.99, 0.99, 1, 1],
46
+ edge: [0.58, 0.64, 0.74, 0.24],
47
+ edgeHeavy: [0.78, 0.84, 0.92, 0.44],
48
+ clear: [0.05, 0.06, 0.08, 1]
49
+ }
50
+
51
+ const theme = { ...defaultTheme }
52
+
53
+ const createShader = (type, source) => {
54
+ const shader = gl.createShader(type)
55
+ if (!shader) return null
56
+ gl.shaderSource(shader, source)
57
+ gl.compileShader(shader)
58
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
59
+ gl.deleteShader(shader)
60
+ return null
61
+ }
62
+ return shader
63
+ }
64
+
65
+ const createProgram = (vertexSource, fragmentSource) => {
66
+ const vertexShader = createShader(gl.VERTEX_SHADER, vertexSource)
67
+ const fragmentShader = createShader(gl.FRAGMENT_SHADER, fragmentSource)
68
+ if (!vertexShader || !fragmentShader) return null
69
+ const program = gl.createProgram()
70
+ if (!program) return null
71
+ gl.attachShader(program, vertexShader)
72
+ gl.attachShader(program, fragmentShader)
73
+ gl.linkProgram(program)
74
+ gl.deleteShader(vertexShader)
75
+ gl.deleteShader(fragmentShader)
76
+ if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
77
+ gl.deleteProgram(program)
78
+ return null
79
+ }
80
+ return program
81
+ }
82
+
83
+ let lineProgram = null
84
+ let pointProgram = null
85
+ let lineBuffer = null
86
+ let pointPositionBuffer = null
87
+ let pointSizeBuffer = null
88
+ let linePositionLocation = -1
89
+ let lineResolutionLocation = null
90
+ let lineColorLocation = null
91
+ let pointPositionLocation = -1
92
+ let pointSizeLocation = -1
93
+ let pointResolutionLocation = null
94
+ let pointColorLocation = null
95
+
96
+ const initWebGl = () => {
97
+ if (!canvas) {
98
+ return false
99
+ }
100
+
101
+ gl = canvas.getContext('webgl2', { alpha: false, antialias: true, depth: false, stencil: false }) ||
102
+ canvas.getContext('webgl', { alpha: false, antialias: true, depth: false, stencil: false })
103
+
104
+ if (!gl) {
105
+ return false
106
+ }
107
+
108
+ lineProgram = createProgram(
109
+ '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); }',
110
+ 'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
111
+ )
112
+
113
+ pointProgram = createProgram(
114
+ '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; }',
115
+ '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); }'
116
+ )
117
+
118
+ if (!lineProgram || !pointProgram) {
119
+ return false
120
+ }
121
+
122
+ lineBuffer = gl.createBuffer()
123
+ pointPositionBuffer = gl.createBuffer()
124
+ pointSizeBuffer = gl.createBuffer()
125
+ if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) {
126
+ return false
127
+ }
128
+
129
+ linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
130
+ lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
131
+ lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
132
+ pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
133
+ pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
134
+ pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
135
+ pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
136
+
137
+ gl.enable(gl.BLEND)
138
+ gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
139
+ gl.disable(gl.DEPTH_TEST)
140
+
141
+ return true
142
+ }
143
+
144
+ const ensureFloat32Capacity = (buffer, neededLength) => {
145
+ if (buffer.length >= neededLength) {
146
+ return buffer
147
+ }
148
+ const next = Math.max(neededLength, Math.ceil(buffer.length * 1.6), 1024)
149
+ return new Float32Array(next)
150
+ }
151
+
152
+ const resizeCanvas = (width, height, ratio) => {
153
+ viewportWidth = Math.max(320, Number.isFinite(width) ? width : viewportWidth)
154
+ viewportHeight = Math.max(320, Number.isFinite(height) ? height : viewportHeight)
155
+ devicePixelRatio = Math.max(1, Number.isFinite(ratio) ? ratio : devicePixelRatio)
156
+
157
+ if (!canvas) return
158
+
159
+ canvas.width = Math.floor(viewportWidth * devicePixelRatio)
160
+ canvas.height = Math.floor(viewportHeight * devicePixelRatio)
161
+ if (gl) {
162
+ gl.viewport(0, 0, canvas.width, canvas.height)
163
+ }
164
+ dirty = true
165
+ requestRender()
166
+ }
167
+
168
+ const toScreenPoint = (x, y) => {
169
+ const sx = (x * camera.scale + camera.x) * devicePixelRatio
170
+ const sy = (y * camera.scale + camera.y) * devicePixelRatio
171
+ return [sx, sy]
172
+ }
173
+
174
+ const ensureNodeCapacity = (count) => {
175
+ if (state.x.length >= count) {
176
+ return
177
+ }
178
+
179
+ const nextCapacity = Math.max(count, Math.ceil(state.x.length * 1.5), 512)
180
+ state.x = new Float32Array(nextCapacity)
181
+ state.y = new Float32Array(nextCapacity)
182
+ state.relevance = new Float32Array(nextCapacity)
183
+ state.radius = new Float32Array(nextCapacity)
184
+ state.visible = new Uint8Array(nextCapacity)
185
+ state.highlighted = new Uint8Array(nextCapacity)
186
+ state.focused = new Uint8Array(nextCapacity)
187
+ state.selected = new Uint8Array(nextCapacity)
188
+ }
189
+
190
+ const ensureEdgeCapacity = (count) => {
191
+ if (state.edgeSource.length >= count) {
192
+ return
193
+ }
194
+
195
+ const nextCapacity = Math.max(count, Math.ceil(state.edgeSource.length * 1.5), 1024)
196
+ state.edgeSource = new Uint32Array(nextCapacity)
197
+ state.edgeTarget = new Uint32Array(nextCapacity)
198
+ state.edgeWeight = new Float32Array(nextCapacity)
199
+ }
200
+
201
+ const nodeRadius = (relevance, kind) => {
202
+ const base = kind === 'cluster' ? 7.8 : 4.6
203
+ const modifier = Math.min(4.8, Math.max(0, relevance * 0.55))
204
+ return base + modifier
205
+ }
206
+
207
+ const loadChunk = (chunk) => {
208
+ const nodes = Array.isArray(chunk?.nodes) ? chunk.nodes : []
209
+ const edges = Array.isArray(chunk?.edges) ? chunk.edges : []
210
+
211
+ ensureNodeCapacity(nodes.length)
212
+ nodeIndexById.clear()
213
+ state.ids = new Array(nodes.length)
214
+ state.titles = new Array(nodes.length)
215
+ state.kinds = new Array(nodes.length)
216
+
217
+ for (let index = 0; index < nodes.length; index += 1) {
218
+ const row = nodes[index]
219
+ const id = typeof row?.[0] === 'string' ? row[0] : ''
220
+ if (!id) {
221
+ continue
222
+ }
223
+ const title = typeof row?.[1] === 'string' ? row[1] : id
224
+ const x = Number.isFinite(row?.[2]) ? Number(row[2]) : 0
225
+ const y = Number.isFinite(row?.[3]) ? Number(row[3]) : 0
226
+ const kind = row?.[6] === 'cluster' ? 'cluster' : 'node'
227
+ const relevance = Number.isFinite(row?.[7]) ? Number(row[7]) : 0
228
+
229
+ state.ids[index] = id
230
+ state.titles[index] = title
231
+ state.kinds[index] = kind
232
+ state.x[index] = x
233
+ state.y[index] = y
234
+ state.relevance[index] = relevance
235
+ state.radius[index] = nodeRadius(relevance, kind)
236
+ state.visible[index] = 0
237
+ state.highlighted[index] = highlightedIds.has(id) ? 1 : 0
238
+ state.focused[index] = focusedIds.has(id) ? 1 : 0
239
+ state.selected[index] = selectedNodeId === id ? 1 : 0
240
+ nodeIndexById.set(id, index)
241
+ }
242
+
243
+ state.nodeCount = nodes.length
244
+
245
+ ensureEdgeCapacity(edges.length)
246
+ let edgeCount = 0
247
+ for (let index = 0; index < edges.length; index += 1) {
248
+ const row = edges[index]
249
+ const sourceId = typeof row?.[0] === 'string' ? row[0] : ''
250
+ const targetId = typeof row?.[1] === 'string' ? row[1] : ''
251
+ const source = nodeIndexById.get(sourceId)
252
+ const target = nodeIndexById.get(targetId)
253
+ if (source === undefined || target === undefined || source === target) {
254
+ continue
255
+ }
256
+ state.edgeSource[edgeCount] = source
257
+ state.edgeTarget[edgeCount] = target
258
+ state.edgeWeight[edgeCount] = Number.isFinite(row?.[2]) ? Number(row[2]) : 1
259
+ edgeCount += 1
260
+ }
261
+
262
+ state.edgeCount = edgeCount
263
+ dirty = true
264
+ }
265
+
266
+ const cullVisibleNodes = () => {
267
+ const minX = -280
268
+ const minY = -280
269
+ const maxX = viewportWidth + 280
270
+ const maxY = viewportHeight + 280
271
+
272
+ for (let index = 0; index < state.nodeCount; index += 1) {
273
+ const radius = state.radius[index] * camera.scale
274
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
275
+ const screenX = sx / devicePixelRatio
276
+ const screenY = sy / devicePixelRatio
277
+ state.visible[index] = screenX + radius >= minX && screenX - radius <= maxX && screenY + radius >= minY && screenY - radius <= maxY ? 1 : 0
278
+ }
279
+ }
280
+
281
+ const drawEdges = () => {
282
+ if (!gl || state.edgeCount === 0) return
283
+
284
+ edgePositionsBuffer = ensureFloat32Capacity(edgePositionsBuffer, state.edgeCount * 4)
285
+ let cursor = 0
286
+ let visibleEdges = 0
287
+ for (let index = 0; index < state.edgeCount; index += 1) {
288
+ const source = state.edgeSource[index]
289
+ const target = state.edgeTarget[index]
290
+ if (state.visible[source] === 0 && state.visible[target] === 0) {
291
+ continue
292
+ }
293
+ const [sx, sy] = toScreenPoint(state.x[source], state.y[source])
294
+ const [tx, ty] = toScreenPoint(state.x[target], state.y[target])
295
+ edgePositionsBuffer[cursor] = sx
296
+ edgePositionsBuffer[cursor + 1] = sy
297
+ edgePositionsBuffer[cursor + 2] = tx
298
+ edgePositionsBuffer[cursor + 3] = ty
299
+ cursor += 4
300
+ visibleEdges += 1
301
+ }
302
+
303
+ lastVisibleEdges = visibleEdges
304
+ if (cursor === 0) return
305
+
306
+ gl.useProgram(lineProgram)
307
+ gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
308
+ gl.bufferData(gl.ARRAY_BUFFER, edgePositionsBuffer.subarray(0, cursor), gl.STREAM_DRAW)
309
+ gl.enableVertexAttribArray(linePositionLocation)
310
+ gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
311
+ gl.uniform2f(lineResolutionLocation, canvas.width, canvas.height)
312
+ gl.uniform4fv(lineColorLocation, state.nodeCount > 4000 ? theme.edge : theme.edgeHeavy)
313
+ gl.lineWidth(1)
314
+ gl.drawArrays(gl.LINES, 0, cursor / 2)
315
+ }
316
+
317
+ const drawNodeLayer = (predicate, color, radiusBoost = 1) => {
318
+ if (!gl || state.nodeCount === 0) return
319
+
320
+ pointPositionsBuffer = ensureFloat32Capacity(pointPositionsBuffer, state.nodeCount * 2)
321
+ pointSizesBuffer = ensureFloat32Capacity(pointSizesBuffer, state.nodeCount)
322
+ let positionCursor = 0
323
+ let sizeCursor = 0
324
+ for (let index = 0; index < state.nodeCount; index += 1) {
325
+ if (!predicate(index)) continue
326
+ const [sx, sy] = toScreenPoint(state.x[index], state.y[index])
327
+ pointPositionsBuffer[positionCursor] = sx
328
+ pointPositionsBuffer[positionCursor + 1] = sy
329
+ pointSizesBuffer[sizeCursor] = Math.max(1.2, state.radius[index] * camera.scale * devicePixelRatio * radiusBoost)
330
+ positionCursor += 2
331
+ sizeCursor += 1
332
+ }
333
+
334
+ if (positionCursor === 0) return
335
+
336
+ gl.useProgram(pointProgram)
337
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
338
+ gl.bufferData(gl.ARRAY_BUFFER, pointPositionsBuffer.subarray(0, positionCursor), gl.STREAM_DRAW)
339
+ gl.enableVertexAttribArray(pointPositionLocation)
340
+ gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
341
+
342
+ gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
343
+ gl.bufferData(gl.ARRAY_BUFFER, pointSizesBuffer.subarray(0, sizeCursor), gl.STREAM_DRAW)
344
+ gl.enableVertexAttribArray(pointSizeLocation)
345
+ gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
346
+
347
+ gl.uniform2f(pointResolutionLocation, canvas.width, canvas.height)
348
+ gl.uniform4fv(pointColorLocation, color)
349
+ gl.drawArrays(gl.POINTS, 0, positionCursor / 2)
350
+ }
351
+
352
+ const clear = () => {
353
+ if (!gl || !canvas) return
354
+ gl.viewport(0, 0, canvas.width, canvas.height)
355
+ gl.clearColor(theme.clear[0], theme.clear[1], theme.clear[2], theme.clear[3])
356
+ gl.clear(gl.COLOR_BUFFER_BIT)
357
+ }
358
+
359
+ const isCameraInteracting = (now) => now < interactionUntil
360
+
361
+ const scheduleSettledRender = (now) => {
362
+ if (settledRenderTimer) {
363
+ return
364
+ }
365
+ const delay = Math.max(32, interactionUntil - now + 16)
366
+ settledRenderTimer = setTimeout(() => {
367
+ settledRenderTimer = null
368
+ dirty = true
369
+ requestRender()
370
+ }, delay)
371
+ }
372
+
373
+ const renderFrame = (now) => {
374
+ renderScheduled = false
375
+ if (!dirty) {
376
+ return
377
+ }
378
+
379
+ const delta = now - lastFrameAt
380
+ const minInterval = state.nodeCount > 20000 ? 22 : 16
381
+ if (delta < minInterval) {
382
+ requestRender()
383
+ return
384
+ }
385
+ lastFrameAt = now
386
+
387
+ if (!gl) {
388
+ return
389
+ }
390
+
391
+ cullVisibleNodes()
392
+ clear()
393
+ const cameraInteracting = isCameraInteracting(now)
394
+ if (!cameraInteracting || state.edgeCount < 1200) {
395
+ drawEdges()
396
+ } else {
397
+ lastVisibleEdges = 0
398
+ scheduleSettledRender(now)
399
+ }
400
+
401
+ drawNodeLayer(
402
+ (index) => state.visible[index] === 1 && state.selected[index] === 0 && state.highlighted[index] === 0,
403
+ theme.node,
404
+ 1
405
+ )
406
+
407
+ drawNodeLayer(
408
+ (index) => state.visible[index] === 1 && state.kinds[index] === 'cluster' && state.selected[index] === 0,
409
+ theme.nodeCluster,
410
+ 1.15
411
+ )
412
+
413
+ drawNodeLayer(
414
+ (index) => state.visible[index] === 1 && state.highlighted[index] === 1,
415
+ theme.nodeHighlight,
416
+ 1.22
417
+ )
418
+
419
+ drawNodeLayer(
420
+ (index) => state.visible[index] === 1 && state.focused[index] === 1,
421
+ theme.nodeHighlight,
422
+ 1.12
423
+ )
424
+
425
+ drawNodeLayer(
426
+ (index) => state.visible[index] === 1 && state.selected[index] === 1,
427
+ theme.nodeSelected,
428
+ 1.32
429
+ )
430
+
431
+ if (dirty) {
432
+ postMessage({
433
+ type: 'frame-stats',
434
+ visibleNodes: (() => {
435
+ let count = 0
436
+ for (let index = 0; index < state.nodeCount; index += 1) {
437
+ if (state.visible[index] === 1) count += 1
438
+ }
439
+ return count
440
+ })(),
441
+ visibleEdges: lastVisibleEdges
442
+ })
443
+ dirty = false
444
+ }
445
+ }
446
+
447
+ const requestRender = () => {
448
+ if (renderScheduled) {
449
+ return
450
+ }
451
+ renderScheduled = true
452
+ const raf = self.requestAnimationFrame
453
+ if (typeof raf === 'function') {
454
+ raf.call(self, renderFrame)
455
+ return
456
+ }
457
+ setTimeout(() => renderFrame(performance.now()), 16)
458
+ }
459
+
460
+ const setCamera = (nextCamera) => {
461
+ if (!nextCamera || typeof nextCamera !== 'object') {
462
+ return
463
+ }
464
+ camera.x = Number.isFinite(nextCamera.x) ? Number(nextCamera.x) : camera.x
465
+ camera.y = Number.isFinite(nextCamera.y) ? Number(nextCamera.y) : camera.y
466
+ camera.scale = Number.isFinite(nextCamera.scale) ? Math.max(0.0002, Math.min(8, Number(nextCamera.scale))) : camera.scale
467
+ interactionUntil = performance.now() + 140
468
+ dirty = true
469
+ requestRender()
470
+ }
471
+
472
+ const worldAtScreen = (screenX, screenY) => {
473
+ const x = (screenX - camera.x) / camera.scale
474
+ const y = (screenY - camera.y) / camera.scale
475
+ return [x, y]
476
+ }
477
+
478
+ const pickNode = (screenX, screenY) => {
479
+ const [worldX, worldY] = worldAtScreen(screenX, screenY)
480
+ let bestIndex = -1
481
+ let bestDistance = Infinity
482
+
483
+ for (let index = 0; index < state.nodeCount; index += 1) {
484
+ if (state.visible[index] === 0) continue
485
+ const dx = state.x[index] - worldX
486
+ const dy = state.y[index] - worldY
487
+ const distance = Math.hypot(dx, dy)
488
+ const maxDistance = state.radius[index] * 1.2
489
+ if (distance <= maxDistance && distance < bestDistance) {
490
+ bestDistance = distance
491
+ bestIndex = index
492
+ }
493
+ }
494
+
495
+ if (bestIndex < 0) {
496
+ return null
497
+ }
498
+
499
+ return {
500
+ id: state.ids[bestIndex],
501
+ title: state.titles[bestIndex],
502
+ kind: state.kinds[bestIndex],
503
+ x: state.x[bestIndex],
504
+ y: state.y[bestIndex]
505
+ }
506
+ }
507
+
508
+ const setHighlights = (ids) => {
509
+ highlightedIds.clear()
510
+ const list = Array.isArray(ids) ? ids : []
511
+ for (let index = 0; index < list.length; index += 1) {
512
+ const id = list[index]
513
+ if (typeof id === 'string' && id.length > 0) {
514
+ highlightedIds.add(id)
515
+ }
516
+ }
517
+
518
+ for (let index = 0; index < state.nodeCount; index += 1) {
519
+ state.highlighted[index] = highlightedIds.has(state.ids[index]) ? 1 : 0
520
+ }
521
+ dirty = true
522
+ requestRender()
523
+ }
524
+
525
+ const setFocus = (ids) => {
526
+ focusedIds.clear()
527
+ const list = Array.isArray(ids) ? ids : []
528
+ for (let index = 0; index < list.length; index += 1) {
529
+ const id = list[index]
530
+ if (typeof id === 'string' && id.length > 0) {
531
+ focusedIds.add(id)
532
+ }
533
+ }
534
+
535
+ for (let index = 0; index < state.nodeCount; index += 1) {
536
+ state.focused[index] = focusedIds.has(state.ids[index]) ? 1 : 0
537
+ }
538
+ dirty = true
539
+ requestRender()
540
+ }
541
+
542
+ const setSelected = (id) => {
543
+ selectedNodeId = typeof id === 'string' && id.length > 0 ? id : null
544
+ for (let index = 0; index < state.nodeCount; index += 1) {
545
+ state.selected[index] = selectedNodeId === state.ids[index] ? 1 : 0
546
+ }
547
+ dirty = true
548
+ requestRender()
549
+ }
550
+
551
+ const moveNode = (id, x, y) => {
552
+ if (typeof id !== 'string' || !Number.isFinite(x) || !Number.isFinite(y)) {
553
+ return
554
+ }
555
+
556
+ const index = nodeIndexById.get(id)
557
+ if (index === undefined) {
558
+ return
559
+ }
560
+
561
+ state.x[index] = x
562
+ state.y[index] = y
563
+ dirty = true
564
+ requestRender()
565
+ }
566
+
567
+ self.onmessage = (event) => {
568
+ const payload = event.data
569
+ if (!payload || typeof payload !== 'object') {
570
+ return
571
+ }
572
+
573
+ if (payload.type === 'init') {
574
+ canvas = payload.canvas
575
+ if (payload.theme && typeof payload.theme === 'object') {
576
+ Object.assign(theme, payload.theme)
577
+ }
578
+ const initialized = initWebGl()
579
+ if (!initialized) {
580
+ postMessage({ type: 'fatal', message: 'WebGL is not available in render worker.' })
581
+ return
582
+ }
583
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
584
+ setCamera(payload.camera)
585
+ requestRender()
586
+ postMessage({ type: 'ready' })
587
+ return
588
+ }
589
+
590
+ if (payload.type === 'resize') {
591
+ resizeCanvas(payload.width, payload.height, payload.devicePixelRatio)
592
+ return
593
+ }
594
+
595
+ if (payload.type === 'camera') {
596
+ setCamera(payload.camera)
597
+ return
598
+ }
599
+
600
+ if (payload.type === 'chunk') {
601
+ loadChunk(payload.chunk)
602
+ requestRender()
603
+ return
604
+ }
605
+
606
+ if (payload.type === 'highlight') {
607
+ setHighlights(payload.ids)
608
+ return
609
+ }
610
+
611
+ if (payload.type === 'focus') {
612
+ setFocus(payload.ids)
613
+ return
614
+ }
615
+
616
+ if (payload.type === 'select') {
617
+ setSelected(payload.id)
618
+ return
619
+ }
620
+
621
+ if (payload.type === 'move-node') {
622
+ moveNode(payload.id, Number(payload.x), Number(payload.y))
623
+ return
624
+ }
625
+
626
+ if (payload.type === 'pick') {
627
+ const node = pickNode(
628
+ Number.isFinite(payload.x) ? Number(payload.x) : 0,
629
+ Number.isFinite(payload.y) ? Number(payload.y) : 0
630
+ )
631
+ postMessage({
632
+ type: 'pick-result',
633
+ requestId: payload.requestId,
634
+ node
635
+ })
636
+ return
637
+ }
638
+
639
+ if (payload.type === 'pointer') {
640
+ hoverX = Number.isFinite(payload.x) ? Number(payload.x) : null
641
+ hoverY = Number.isFinite(payload.y) ? Number(payload.y) : null
642
+ return
643
+ }
644
+ }
645
+ `;
@@ -0,0 +1,66 @@
1
+ export const createClientWorkerJs = () => `const normalize = value => String(value || '')
2
+ .normalize('NFKD')
3
+ .replace(/\\p{Diacritic}/gu, '')
4
+ .toLowerCase()
5
+
6
+ let nodeIndex = []
7
+
8
+ const toNodeIndex = nodes =>
9
+ (Array.isArray(nodes) ? nodes : [])
10
+ .map(node => {
11
+ const id = typeof node.id === 'string' ? node.id : ''
12
+ if (!id) {
13
+ return null
14
+ }
15
+ const title = normalize(node.title)
16
+ const path = normalize(node.path)
17
+ const tags = Array.isArray(node.tags) ? node.tags.map(tag => normalize(tag)) : []
18
+ return {
19
+ id,
20
+ text: [title, path, ...tags].join(' ')
21
+ }
22
+ })
23
+ .filter(Boolean)
24
+
25
+ const scoreText = (text, query) => {
26
+ if (!query) return 0
27
+ if (!text.includes(query)) return 0
28
+ if (text.startsWith(query)) return 4
29
+ return 1
30
+ }
31
+
32
+ const filterIds = (query, limit) => {
33
+ const normalizedQuery = normalize(query).trim()
34
+ if (!normalizedQuery) {
35
+ return []
36
+ }
37
+ const rows = []
38
+ for (let index = 0; index < nodeIndex.length; index += 1) {
39
+ const row = nodeIndex[index]
40
+ const score = scoreText(row.text, normalizedQuery)
41
+ if (score > 0) {
42
+ rows.push({ id: row.id, score })
43
+ }
44
+ }
45
+ rows.sort((left, right) => right.score - left.score || left.id.localeCompare(right.id))
46
+ return rows.slice(0, Math.max(1, Number.isFinite(limit) ? limit : rows.length)).map(row => row.id)
47
+ }
48
+
49
+ self.onmessage = event => {
50
+ const payload = event.data
51
+ if (!payload || typeof payload !== 'object') {
52
+ return
53
+ }
54
+ if (payload.type === 'load-nodes') {
55
+ nodeIndex = toNodeIndex(payload.nodes)
56
+ return
57
+ }
58
+ if (payload.type === 'filter') {
59
+ const token = payload.token
60
+ const ids = filterIds(payload.query, payload.limit)
61
+ self.postMessage({ type: 'filter-result', token, ids })
62
+ }
63
+ }
64
+
65
+ self.postMessage({ type: 'ready' })
66
+ `;