@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.
- package/README.md +6 -5
- package/dist/application/frontend/client-css.js +1 -6
- package/dist/application/frontend/client-html.js +0 -1
- package/dist/application/frontend/client-js.js +580 -3334
- package/dist/application/frontend/client-render-worker-js.js +534 -0
- package/dist/application/get-graph-stream-chunk.js +285 -0
- package/dist/application/server/routes.js +31 -0
- package/package.json +1 -1
|
@@ -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
|
+
`;
|