@andespindola/brainlink 0.1.0-beta.81 → 0.1.0-beta.83
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
CHANGED
|
@@ -83,8 +83,10 @@ Legacy `.jsonl.gz` packs are upgraded to `.blpk` automatically on first search/c
|
|
|
83
83
|
- Realtime graph UI with agent selector and colored knowledge groups.
|
|
84
84
|
- Graph renderer optimized for large datasets with viewport-driven node culling and edge lookup by visible nodes.
|
|
85
85
|
- Canvas graph rendering uses the same batched node and edge pipeline for every graph size, reducing per-frame draw calls while keeping selected and hovered items highlighted.
|
|
86
|
+
- WebGL acceleration is used when available for dense node and edge drawing, with Canvas 2D preserved as the interaction and fallback layer.
|
|
86
87
|
- Large graph layout API automatically uses compact payload encoding with link-coverage-aware edge selection to reduce initial client load without hiding major relationships.
|
|
87
88
|
- Large-segment layout spacing now grows logarithmically to keep initial visual density consistent between medium and very large vaults (for example, ~1k vs ~50k notes).
|
|
89
|
+
- Graph coordinates are visually compacted across graph sizes so reset starts from a stable macro mass and zoom-in progressively expands toward local detail.
|
|
88
90
|
- Zoomed-out graph LOD clusters dense regions and progressively expands the focused viewport as zoom increases, including very large vaults.
|
|
89
91
|
- Graph reset starts in macro "galaxy" overview mode and progressively reveals nearby nodes as zoom increases, including smaller vaults.
|
|
90
92
|
- Graph filtering runs in a dedicated browser worker to keep the UI thread responsive during heavy datasets.
|
|
@@ -596,6 +598,8 @@ The graph UI shows:
|
|
|
596
598
|
- double-click on canvas zooms in at cursor position
|
|
597
599
|
- floating graph totals (notes, links, tags) below the Brainlink title
|
|
598
600
|
- graph rendering safeguards (batched canvas drawing across graph sizes, edge draw caps, lower redraw rate, zoom-aware interaction)
|
|
601
|
+
- WebGL node and edge acceleration when supported, falling back to Canvas 2D without changing graph behavior
|
|
602
|
+
- compact macro-to-micro density progression so reset keeps the graph mass oriented and zoom-in separates local neighborhoods progressively
|
|
599
603
|
- massive-graph LOD progression: very low zoom uses spatial overview sampling plus hub-neighborhood edge previews to preserve whole-vault shape and orientation, then progressively raises the focused node budget as zoom increases so dense local areas keep nearby notes and links visible
|
|
600
604
|
|
|
601
605
|
The server indexes before starting by default. Use `--no-index` to skip that step:
|
|
@@ -77,16 +77,33 @@ select {
|
|
|
77
77
|
font-size: 18px;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
.graph-stage {
|
|
81
|
+
position: relative;
|
|
82
82
|
width: 100%;
|
|
83
83
|
height: 100%;
|
|
84
84
|
background:
|
|
85
85
|
radial-gradient(circle at 18% 20%, rgba(53, 208, 162, 0.12), transparent 28rem),
|
|
86
86
|
linear-gradient(135deg, #0d0f12 0%, #12161c 55%, #0a0d10 100%);
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
#graph,
|
|
91
|
+
#graphGl {
|
|
92
|
+
display: block;
|
|
93
|
+
position: absolute;
|
|
94
|
+
inset: 0;
|
|
95
|
+
width: 100%;
|
|
96
|
+
height: 100%;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
#graph {
|
|
87
100
|
cursor: grab;
|
|
88
101
|
}
|
|
89
102
|
|
|
103
|
+
#graphGl {
|
|
104
|
+
pointer-events: none;
|
|
105
|
+
}
|
|
106
|
+
|
|
90
107
|
#graph:active {
|
|
91
108
|
cursor: grabbing;
|
|
92
109
|
}
|
|
@@ -43,7 +43,10 @@ export const createClientHtml = () => `<!doctype html>
|
|
|
43
43
|
</div>
|
|
44
44
|
</div>
|
|
45
45
|
</header>
|
|
46
|
-
<
|
|
46
|
+
<div class="graph-stage">
|
|
47
|
+
<canvas id="graphGl" aria-hidden="true"></canvas>
|
|
48
|
+
<canvas id="graph" aria-label="Brainlink knowledge graph"></canvas>
|
|
49
|
+
</div>
|
|
47
50
|
</section>
|
|
48
51
|
</main>
|
|
49
52
|
<footer class="app-footer" aria-label="Copyright notice">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const createClientJs = () => `const canvas = document.getElementById('graph')
|
|
2
|
+
const glCanvas = document.getElementById('graphGl')
|
|
2
3
|
const ctx = canvas.getContext('2d')
|
|
3
4
|
const largeGraphNodeThreshold = 4000
|
|
4
5
|
const massiveGraphNodeThreshold = 20000
|
|
@@ -175,6 +176,177 @@ const graphTheme = {
|
|
|
175
176
|
label: '#edf2f7'
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
const parseRgb = color => {
|
|
180
|
+
const normalized = color.trim()
|
|
181
|
+
if (normalized.startsWith('#')) {
|
|
182
|
+
const value = normalized.slice(1)
|
|
183
|
+
const expanded = value.length === 3
|
|
184
|
+
? value.split('').map(char => char + char).join('')
|
|
185
|
+
: value
|
|
186
|
+
const parsed = Number.parseInt(expanded, 16)
|
|
187
|
+
return [
|
|
188
|
+
((parsed >> 16) & 255) / 255,
|
|
189
|
+
((parsed >> 8) & 255) / 255,
|
|
190
|
+
(parsed & 255) / 255
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const match = normalized.match(/rgba?\\(([^)]+)\\)/)
|
|
195
|
+
if (!match) return [1, 1, 1]
|
|
196
|
+
const parts = match[1].split(',').map(part => Number.parseFloat(part.trim()))
|
|
197
|
+
return [
|
|
198
|
+
Math.max(0, Math.min(1, (parts[0] ?? 255) / 255)),
|
|
199
|
+
Math.max(0, Math.min(1, (parts[1] ?? 255) / 255)),
|
|
200
|
+
Math.max(0, Math.min(1, (parts[2] ?? 255) / 255))
|
|
201
|
+
]
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const rgba = (color, alpha = 1) => {
|
|
205
|
+
const [red, green, blue] = parseRgb(color)
|
|
206
|
+
return [red, green, blue, Math.max(0, Math.min(1, alpha))]
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const createShader = (gl, type, source) => {
|
|
210
|
+
const shader = gl.createShader(type)
|
|
211
|
+
if (!shader) return null
|
|
212
|
+
gl.shaderSource(shader, source)
|
|
213
|
+
gl.compileShader(shader)
|
|
214
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
215
|
+
gl.deleteShader(shader)
|
|
216
|
+
return null
|
|
217
|
+
}
|
|
218
|
+
return shader
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const createProgram = (gl, vertexSource, fragmentSource) => {
|
|
222
|
+
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexSource)
|
|
223
|
+
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentSource)
|
|
224
|
+
if (!vertexShader || !fragmentShader) return null
|
|
225
|
+
const program = gl.createProgram()
|
|
226
|
+
if (!program) return null
|
|
227
|
+
gl.attachShader(program, vertexShader)
|
|
228
|
+
gl.attachShader(program, fragmentShader)
|
|
229
|
+
gl.linkProgram(program)
|
|
230
|
+
gl.deleteShader(vertexShader)
|
|
231
|
+
gl.deleteShader(fragmentShader)
|
|
232
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
|
233
|
+
gl.deleteProgram(program)
|
|
234
|
+
return null
|
|
235
|
+
}
|
|
236
|
+
return program
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const createWebGlRenderer = targetCanvas => {
|
|
240
|
+
if (!targetCanvas) return null
|
|
241
|
+
|
|
242
|
+
const gl = targetCanvas.getContext('webgl2', { alpha: true, antialias: true }) ||
|
|
243
|
+
targetCanvas.getContext('webgl', { alpha: true, antialias: true })
|
|
244
|
+
if (!gl) return null
|
|
245
|
+
|
|
246
|
+
const lineProgram = createProgram(
|
|
247
|
+
gl,
|
|
248
|
+
'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); }',
|
|
249
|
+
'precision mediump float; uniform vec4 u_color; void main() { gl_FragColor = u_color; }'
|
|
250
|
+
)
|
|
251
|
+
const pointProgram = createProgram(
|
|
252
|
+
gl,
|
|
253
|
+
'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; }',
|
|
254
|
+
'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 edge = smoothstep(0.5, 0.42, distanceFromCenter); gl_FragColor = vec4(u_color.rgb, u_color.a * edge); }'
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if (!lineProgram || !pointProgram) return null
|
|
258
|
+
|
|
259
|
+
const lineBuffer = gl.createBuffer()
|
|
260
|
+
const pointPositionBuffer = gl.createBuffer()
|
|
261
|
+
const pointSizeBuffer = gl.createBuffer()
|
|
262
|
+
if (!lineBuffer || !pointPositionBuffer || !pointSizeBuffer) return null
|
|
263
|
+
|
|
264
|
+
const linePositionLocation = gl.getAttribLocation(lineProgram, 'a_position')
|
|
265
|
+
const lineResolutionLocation = gl.getUniformLocation(lineProgram, 'u_resolution')
|
|
266
|
+
const lineColorLocation = gl.getUniformLocation(lineProgram, 'u_color')
|
|
267
|
+
const pointPositionLocation = gl.getAttribLocation(pointProgram, 'a_position')
|
|
268
|
+
const pointSizeLocation = gl.getAttribLocation(pointProgram, 'a_size')
|
|
269
|
+
const pointResolutionLocation = gl.getUniformLocation(pointProgram, 'u_resolution')
|
|
270
|
+
const pointColorLocation = gl.getUniformLocation(pointProgram, 'u_color')
|
|
271
|
+
|
|
272
|
+
gl.enable(gl.BLEND)
|
|
273
|
+
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
|
274
|
+
|
|
275
|
+
const setViewport = (width, height) => {
|
|
276
|
+
gl.viewport(0, 0, targetCanvas.width, targetCanvas.height)
|
|
277
|
+
return [targetCanvas.width / Math.max(width, 1), targetCanvas.height / Math.max(height, 1)]
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const screenPoint = (node, ratioX, ratioY) => [
|
|
281
|
+
(node.x * state.transform.scale + state.transform.x) * ratioX,
|
|
282
|
+
(node.y * state.transform.scale + state.transform.y) * ratioY
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
const clear = (width, height) => {
|
|
286
|
+
setViewport(width, height)
|
|
287
|
+
gl.clearColor(0, 0, 0, 0)
|
|
288
|
+
gl.clear(gl.COLOR_BUFFER_BIT)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const drawLines = (edges, color, width, height) => {
|
|
292
|
+
if (edges.length === 0) return
|
|
293
|
+
const [ratioX, ratioY] = setViewport(width, height)
|
|
294
|
+
const positions = new Float32Array(edges.length * 4)
|
|
295
|
+
for (let index = 0; index < edges.length; index += 1) {
|
|
296
|
+
const edge = edges[index]
|
|
297
|
+
const source = screenPoint(edge.sourceNode, ratioX, ratioY)
|
|
298
|
+
const target = screenPoint(edge.targetNode, ratioX, ratioY)
|
|
299
|
+
const offset = index * 4
|
|
300
|
+
positions[offset] = source[0]
|
|
301
|
+
positions[offset + 1] = source[1]
|
|
302
|
+
positions[offset + 2] = target[0]
|
|
303
|
+
positions[offset + 3] = target[1]
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
gl.useProgram(lineProgram)
|
|
307
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, lineBuffer)
|
|
308
|
+
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STREAM_DRAW)
|
|
309
|
+
gl.enableVertexAttribArray(linePositionLocation)
|
|
310
|
+
gl.vertexAttribPointer(linePositionLocation, 2, gl.FLOAT, false, 0, 0)
|
|
311
|
+
gl.uniform2f(lineResolutionLocation, targetCanvas.width, targetCanvas.height)
|
|
312
|
+
gl.uniform4fv(lineColorLocation, color)
|
|
313
|
+
gl.lineWidth(1)
|
|
314
|
+
gl.drawArrays(gl.LINES, 0, edges.length * 2)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const drawPoints = (nodes, color, sizeForNode, width, height) => {
|
|
318
|
+
if (nodes.length === 0) return
|
|
319
|
+
const [ratioX, ratioY] = setViewport(width, height)
|
|
320
|
+
const positions = new Float32Array(nodes.length * 2)
|
|
321
|
+
const sizes = new Float32Array(nodes.length)
|
|
322
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
323
|
+
const node = nodes[index]
|
|
324
|
+
const point = screenPoint(node, ratioX, ratioY)
|
|
325
|
+
const offset = index * 2
|
|
326
|
+
positions[offset] = point[0]
|
|
327
|
+
positions[offset + 1] = point[1]
|
|
328
|
+
sizes[index] = sizeForNode(node) * ((ratioX + ratioY) / 2)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
gl.useProgram(pointProgram)
|
|
332
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, pointPositionBuffer)
|
|
333
|
+
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STREAM_DRAW)
|
|
334
|
+
gl.enableVertexAttribArray(pointPositionLocation)
|
|
335
|
+
gl.vertexAttribPointer(pointPositionLocation, 2, gl.FLOAT, false, 0, 0)
|
|
336
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, pointSizeBuffer)
|
|
337
|
+
gl.bufferData(gl.ARRAY_BUFFER, sizes, gl.STREAM_DRAW)
|
|
338
|
+
gl.enableVertexAttribArray(pointSizeLocation)
|
|
339
|
+
gl.vertexAttribPointer(pointSizeLocation, 1, gl.FLOAT, false, 0, 0)
|
|
340
|
+
gl.uniform2f(pointResolutionLocation, targetCanvas.width, targetCanvas.height)
|
|
341
|
+
gl.uniform4fv(pointColorLocation, color)
|
|
342
|
+
gl.drawArrays(gl.POINTS, 0, nodes.length)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return { clear, drawLines, drawPoints }
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const webGlRenderer = createWebGlRenderer(glCanvas)
|
|
349
|
+
|
|
178
350
|
const initFilterWorker = () => {
|
|
179
351
|
if (typeof Worker === 'undefined') {
|
|
180
352
|
return
|
|
@@ -243,6 +415,10 @@ const resize = () => {
|
|
|
243
415
|
const ratio = window.devicePixelRatio || 1
|
|
244
416
|
canvas.width = Math.floor(width * ratio)
|
|
245
417
|
canvas.height = Math.floor(height * ratio)
|
|
418
|
+
if (glCanvas) {
|
|
419
|
+
glCanvas.width = Math.floor(width * ratio)
|
|
420
|
+
glCanvas.height = Math.floor(height * ratio)
|
|
421
|
+
}
|
|
246
422
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
247
423
|
markRenderDirty()
|
|
248
424
|
}
|
|
@@ -1065,6 +1241,105 @@ const drawGraphNodes = () => {
|
|
|
1065
1241
|
priorityNodes.forEach(node => drawSingleNode(node))
|
|
1066
1242
|
}
|
|
1067
1243
|
|
|
1244
|
+
const partitionGraphForAcceleratedRenderer = () => {
|
|
1245
|
+
const regularNodes = []
|
|
1246
|
+
const priorityNodes = []
|
|
1247
|
+
const regularEdges = []
|
|
1248
|
+
const inferredEdges = []
|
|
1249
|
+
const selectedEdges = []
|
|
1250
|
+
|
|
1251
|
+
for (let index = 0; index < state.renderNodes.length; index += 1) {
|
|
1252
|
+
const node = state.renderNodes[index]
|
|
1253
|
+
const isPriority =
|
|
1254
|
+
state.selected?.id === node.id ||
|
|
1255
|
+
state.hovered?.id === node.id
|
|
1256
|
+
if (isPriority) {
|
|
1257
|
+
priorityNodes.push(node)
|
|
1258
|
+
} else {
|
|
1259
|
+
regularNodes.push(node)
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
for (let index = 0; index < state.renderEdges.length; index += 1) {
|
|
1264
|
+
const edge = state.renderEdges[index]
|
|
1265
|
+
const isSelected = state.selected && (edge.source === state.selected.id || edge.target === state.selected.id)
|
|
1266
|
+
if (isSelected) {
|
|
1267
|
+
selectedEdges.push(edge)
|
|
1268
|
+
} else if (edge.inferred) {
|
|
1269
|
+
inferredEdges.push(edge)
|
|
1270
|
+
} else {
|
|
1271
|
+
regularEdges.push(edge)
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return { regularNodes, priorityNodes, regularEdges, inferredEdges, selectedEdges }
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
const drawGraphLabels = nodes => {
|
|
1279
|
+
if (!(state.transform.scale >= 0.62 && state.renderNodes.length <= 1200)) {
|
|
1280
|
+
return
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
ctx.fillStyle = graphTheme.label
|
|
1284
|
+
ctx.font = '12px Inter, system-ui, sans-serif'
|
|
1285
|
+
ctx.textAlign = 'center'
|
|
1286
|
+
ctx.textBaseline = 'top'
|
|
1287
|
+
for (let index = 0; index < nodes.length; index += 1) {
|
|
1288
|
+
const node = nodes[index]
|
|
1289
|
+
ctx.fillText(node.title.slice(0, 34), node.x, node.y + nodeRadius(node) + 8)
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
const drawAcceleratedGraph = (width, height, drawEdges) => {
|
|
1294
|
+
if (!webGlRenderer || state.renderClusters.length > 0) {
|
|
1295
|
+
return false
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const graphParts = partitionGraphForAcceleratedRenderer()
|
|
1299
|
+
const scale = state.transform.scale
|
|
1300
|
+
webGlRenderer.clear(width, height)
|
|
1301
|
+
if (drawEdges) {
|
|
1302
|
+
webGlRenderer.drawLines(
|
|
1303
|
+
graphParts.regularEdges,
|
|
1304
|
+
rgba('rgb(153, 165, 181)', edgeOpacityForScale({ inferred: false }, scale)),
|
|
1305
|
+
width,
|
|
1306
|
+
height
|
|
1307
|
+
)
|
|
1308
|
+
webGlRenderer.drawLines(
|
|
1309
|
+
graphParts.inferredEdges,
|
|
1310
|
+
rgba('rgb(203, 213, 225)', edgeOpacityForScale({ inferred: true }, scale)),
|
|
1311
|
+
width,
|
|
1312
|
+
height
|
|
1313
|
+
)
|
|
1314
|
+
}
|
|
1315
|
+
webGlRenderer.drawPoints(
|
|
1316
|
+
graphParts.regularNodes,
|
|
1317
|
+
rgba(graphTheme.nodeHalo, 0.28),
|
|
1318
|
+
node => Math.max((nodeRadius(node) + 3) * state.transform.scale * 2, 1.5),
|
|
1319
|
+
width,
|
|
1320
|
+
height
|
|
1321
|
+
)
|
|
1322
|
+
webGlRenderer.drawPoints(
|
|
1323
|
+
graphParts.regularNodes,
|
|
1324
|
+
rgba(graphTheme.node, 1),
|
|
1325
|
+
node => Math.max(nodeRadius(node) * state.transform.scale * 2, 1.2),
|
|
1326
|
+
width,
|
|
1327
|
+
height
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
ctx.save()
|
|
1331
|
+
ctx.translate(state.transform.x, state.transform.y)
|
|
1332
|
+
ctx.scale(state.transform.scale, state.transform.scale)
|
|
1333
|
+
if (drawEdges) {
|
|
1334
|
+
graphParts.selectedEdges.forEach(edge => drawGraphEdge(edge))
|
|
1335
|
+
}
|
|
1336
|
+
drawGraphLabels(graphParts.regularNodes)
|
|
1337
|
+
graphParts.priorityNodes.forEach(node => drawSingleNode(node))
|
|
1338
|
+
ctx.restore()
|
|
1339
|
+
|
|
1340
|
+
return true
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1068
1343
|
const edgePairKey = (source, target) =>
|
|
1069
1344
|
source < target ? source + '|' + target : target + '|' + source
|
|
1070
1345
|
|
|
@@ -1379,10 +1654,10 @@ const ensureHubNodesInRenderedSet = (nodes) => {
|
|
|
1379
1654
|
}
|
|
1380
1655
|
|
|
1381
1656
|
const zoomCapByNodeCount = (nodeCount) => {
|
|
1382
|
-
if (nodeCount > 50000) return
|
|
1383
|
-
if (nodeCount > 20000) return
|
|
1384
|
-
if (nodeCount > 6000) return 2
|
|
1385
|
-
if (nodeCount > 2000) return
|
|
1657
|
+
if (nodeCount > 50000) return 5.4
|
|
1658
|
+
if (nodeCount > 20000) return 4.8
|
|
1659
|
+
if (nodeCount > 6000) return 4.2
|
|
1660
|
+
if (nodeCount > 2000) return 4
|
|
1386
1661
|
return zoomRange.max
|
|
1387
1662
|
}
|
|
1388
1663
|
|
|
@@ -1541,10 +1816,15 @@ const focusPrimaryHub = () => {
|
|
|
1541
1816
|
}
|
|
1542
1817
|
|
|
1543
1818
|
const layoutDensityScaleForNodeCount = (nodeCount) => {
|
|
1544
|
-
if (nodeCount > 50000) return 0.
|
|
1545
|
-
if (nodeCount > 20000) return 0.
|
|
1546
|
-
if (nodeCount > 6000) return 0.
|
|
1547
|
-
return
|
|
1819
|
+
if (nodeCount > 50000) return 0.26
|
|
1820
|
+
if (nodeCount > 20000) return 0.3
|
|
1821
|
+
if (nodeCount > 6000) return 0.36
|
|
1822
|
+
if (nodeCount > 2000) return 0.42
|
|
1823
|
+
if (nodeCount > 600) return 0.5
|
|
1824
|
+
if (nodeCount > 180) return 0.58
|
|
1825
|
+
if (nodeCount > 60) return 0.68
|
|
1826
|
+
if (nodeCount > 20) return 0.78
|
|
1827
|
+
return 0.88
|
|
1548
1828
|
}
|
|
1549
1829
|
|
|
1550
1830
|
const createLayout = graph => {
|
|
@@ -2256,6 +2536,7 @@ const render = now => {
|
|
|
2256
2536
|
resetView()
|
|
2257
2537
|
}
|
|
2258
2538
|
ctx.clearRect(0, 0, width, height)
|
|
2539
|
+
webGlRenderer?.clear(width, height)
|
|
2259
2540
|
if (state.nodes.length === 0) {
|
|
2260
2541
|
ctx.fillStyle = '#99a5b5'
|
|
2261
2542
|
ctx.font = '14px Inter, system-ui, sans-serif'
|
|
@@ -2264,9 +2545,6 @@ const render = now => {
|
|
|
2264
2545
|
requestAnimationFrame(render)
|
|
2265
2546
|
return
|
|
2266
2547
|
}
|
|
2267
|
-
ctx.save()
|
|
2268
|
-
ctx.translate(state.transform.x, state.transform.y)
|
|
2269
|
-
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2270
2548
|
|
|
2271
2549
|
computeRenderVisibility()
|
|
2272
2550
|
tick(delta)
|
|
@@ -2299,11 +2577,12 @@ const render = now => {
|
|
|
2299
2577
|
const drawEdges =
|
|
2300
2578
|
state.renderClusters.length === 0 &&
|
|
2301
2579
|
state.transform.scale >= minimumEdgeScale
|
|
2302
|
-
if (drawEdges) {
|
|
2303
|
-
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
|
|
2580
|
+
if (drawAcceleratedGraph(width, height, drawEdges)) {
|
|
2581
|
+
// WebGL handles the dense node/edge layer; the 2D canvas remains the interaction overlay.
|
|
2582
|
+
} else if (state.renderClusters.length > 0) {
|
|
2583
|
+
ctx.save()
|
|
2584
|
+
ctx.translate(state.transform.x, state.transform.y)
|
|
2585
|
+
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2307
2586
|
const safeScale = Math.max(state.transform.scale, 0.0001)
|
|
2308
2587
|
state.renderClusters.forEach(cluster => {
|
|
2309
2588
|
const isMacro = cluster.id === 'macro-galaxy'
|
|
@@ -2332,11 +2611,17 @@ const render = now => {
|
|
|
2332
2611
|
}
|
|
2333
2612
|
// Keep cluster markers minimal and faster to draw on large graphs.
|
|
2334
2613
|
})
|
|
2614
|
+
ctx.restore()
|
|
2335
2615
|
} else {
|
|
2616
|
+
ctx.save()
|
|
2617
|
+
ctx.translate(state.transform.x, state.transform.y)
|
|
2618
|
+
ctx.scale(state.transform.scale, state.transform.scale)
|
|
2619
|
+
if (drawEdges) {
|
|
2620
|
+
drawGraphEdges()
|
|
2621
|
+
}
|
|
2336
2622
|
drawGraphNodes()
|
|
2623
|
+
ctx.restore()
|
|
2337
2624
|
}
|
|
2338
|
-
|
|
2339
|
-
ctx.restore()
|
|
2340
2625
|
if (state.renderNodes.length === 0 && state.renderClusters.length === 0) {
|
|
2341
2626
|
ctx.fillStyle = '#99a5b5'
|
|
2342
2627
|
ctx.font = '12px Inter, system-ui, sans-serif'
|
package/package.json
CHANGED