@andespindola/brainlink 1.0.4 → 1.0.6
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 +17 -9
- package/dist/application/add-note.js +2 -2
- package/dist/application/build-context.js +16 -10
- package/dist/application/canonical-context-links.js +44 -5
- package/dist/application/check-package-update.js +105 -0
- package/dist/application/frontend/client/chunk-fetch.js +236 -0
- package/dist/application/frontend/client/controls.js +178 -0
- package/dist/application/frontend/client/elements.js +122 -0
- package/dist/application/frontend/client/input.js +202 -0
- package/dist/application/frontend/client/node-details.js +191 -0
- package/dist/application/frontend/client/rendering.js +296 -0
- package/dist/application/frontend/client/scope-theme.js +114 -0
- package/dist/application/frontend/client/spatial.js +98 -0
- package/dist/application/frontend/client/storage.js +215 -0
- package/dist/application/frontend/client/upload.js +90 -0
- package/dist/application/frontend/client/worker-bootstrap.js +147 -0
- package/dist/application/frontend/client-js.js +24 -1837
- package/dist/application/frontend/client-render-worker-js.js +1 -1
- package/dist/application/index-vault-phases.js +189 -0
- package/dist/application/index-vault.js +44 -165
- package/dist/application/server/routes.js +12 -9
- package/dist/cli/commands/write/dedupe-commands.js +59 -0
- package/dist/cli/commands/write/index-commands.js +205 -0
- package/dist/cli/commands/write/link-commands.js +68 -0
- package/dist/cli/commands/write/note-commands.js +146 -0
- package/dist/cli/commands/write/server-commands.js +553 -0
- package/dist/cli/commands/write/shared.js +35 -0
- package/dist/cli/commands/write/vault-lifecycle-commands.js +270 -0
- package/dist/cli/commands/write-commands.js +12 -1303
- package/dist/cli/main.js +39 -3
- package/dist/domain/context.js +39 -3
- package/dist/domain/embeddings.js +31 -5
- package/dist/domain/graph-contexts.js +62 -57
- package/dist/domain/graph-layout/cauliflower-layout.js +116 -0
- package/dist/domain/graph-layout/collisions.js +100 -0
- package/dist/domain/graph-layout/hierarchy.js +135 -0
- package/dist/domain/graph-layout/metrics.js +111 -0
- package/dist/domain/graph-layout/segments.js +76 -0
- package/dist/domain/graph-layout/star-layout.js +110 -0
- package/dist/domain/graph-layout.js +4 -625
- package/dist/infrastructure/config.js +10 -4
- package/dist/infrastructure/file-index.js +13 -4
- package/dist/infrastructure/semantic-prefilter.js +24 -0
- package/dist/mcp/server.js +7 -0
- package/dist/mcp/tool-guard.js +29 -0
- package/dist/mcp/tools/maintenance-tools.js +409 -0
- package/dist/mcp/tools/read-tools.js +504 -0
- package/dist/mcp/tools/shared.js +216 -0
- package/dist/mcp/tools/write-tools.js +247 -0
- package/dist/mcp/tools.js +3 -1357
- package/docs/AGENT_USAGE.md +4 -4
- package/docs/QUICKSTART.md +5 -1
- package/package.json +2 -2
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
export const createRenderingJs = () => `
|
|
2
|
+
const drawFallback = () => {
|
|
3
|
+
if (state.rendererMode !== 'fallback') {
|
|
4
|
+
return
|
|
5
|
+
}
|
|
6
|
+
ctx2dFallback = ctx2dFallback ?? canvas.getContext('2d')
|
|
7
|
+
if (!ctx2dFallback) {
|
|
8
|
+
return
|
|
9
|
+
}
|
|
10
|
+
const width = state.viewport.width
|
|
11
|
+
const height = state.viewport.height
|
|
12
|
+
const ratio = state.viewport.ratio
|
|
13
|
+
canvas.width = Math.floor(width * ratio)
|
|
14
|
+
canvas.height = Math.floor(height * ratio)
|
|
15
|
+
ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
16
|
+
ctx2dFallback.fillStyle = '#08131d'
|
|
17
|
+
ctx2dFallback.fillRect(0, 0, width, height)
|
|
18
|
+
|
|
19
|
+
const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
|
|
20
|
+
const edges = Array.isArray(state.chunk.edges) ? state.chunk.edges : []
|
|
21
|
+
const nodeById = new Map()
|
|
22
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
23
|
+
nodeById.set(nodes[i][0], nodes[i])
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
ctx2dFallback.strokeStyle = 'rgba(151,181,212,0.18)'
|
|
27
|
+
ctx2dFallback.lineWidth = 1
|
|
28
|
+
for (let i = 0; i < edges.length; i += 1) {
|
|
29
|
+
const edge = edges[i]
|
|
30
|
+
const source = nodeById.get(edge[0])
|
|
31
|
+
const target = nodeById.get(edge[1])
|
|
32
|
+
if (!source || !target) continue
|
|
33
|
+
const from = worldToScreen(source[2], source[3])
|
|
34
|
+
const to = worldToScreen(target[2], target[3])
|
|
35
|
+
ctx2dFallback.beginPath()
|
|
36
|
+
ctx2dFallback.moveTo(from.x, from.y)
|
|
37
|
+
ctx2dFallback.lineTo(to.x, to.y)
|
|
38
|
+
ctx2dFallback.stroke()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < nodes.length; i += 1) {
|
|
42
|
+
const node = nodes[i]
|
|
43
|
+
const p = worldToScreen(node[2], node[3])
|
|
44
|
+
const selected = state.selectedNodeId === node[0]
|
|
45
|
+
const color = segmentColor(node[5] || node[4] || node[1])
|
|
46
|
+
const radius = Math.max(3.2, Math.min(16.5, 5 + node[7] * 0.65))
|
|
47
|
+
|
|
48
|
+
ctx2dFallback.beginPath()
|
|
49
|
+
ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
|
|
50
|
+
ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
|
|
51
|
+
ctx2dFallback.fill()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ctx2dFallback.fillStyle = '#97a9bd'
|
|
55
|
+
ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
|
|
56
|
+
ctx2dFallback.textAlign = 'center'
|
|
57
|
+
ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const updateTotals = () => {
|
|
61
|
+
elements.nodeCount.textContent = String(state.totals.nodes)
|
|
62
|
+
elements.edgeCount.textContent = String(state.totals.edges)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const updateWorkerCamera = () => {
|
|
66
|
+
updateGraphOverlays()
|
|
67
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
if (state.cameraSyncScheduled) {
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
state.cameraSyncScheduled = true
|
|
74
|
+
requestAnimationFrame(() => {
|
|
75
|
+
state.cameraSyncScheduled = false
|
|
76
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
77
|
+
return
|
|
78
|
+
}
|
|
79
|
+
state.renderWorker.postMessage({
|
|
80
|
+
type: 'camera',
|
|
81
|
+
camera: state.camera
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const updateWorkerSize = () => {
|
|
87
|
+
updateGraphOverlays()
|
|
88
|
+
if (!state.renderWorker || !state.workerReady) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
state.renderWorker.postMessage({
|
|
92
|
+
type: 'resize',
|
|
93
|
+
width: state.viewport.width,
|
|
94
|
+
height: state.viewport.height,
|
|
95
|
+
devicePixelRatio: state.viewport.ratio
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const normalizeList = (items) => Array.isArray(items) ? items : []
|
|
100
|
+
|
|
101
|
+
const applyManualNodePositions = (nodes) => normalizeList(nodes).map((node) => {
|
|
102
|
+
const id = typeof node?.[0] === 'string' ? node[0] : ''
|
|
103
|
+
const position = id ? state.nodePositions.get(id) : null
|
|
104
|
+
if (!position || !Number.isFinite(position.x) || !Number.isFinite(position.y)) {
|
|
105
|
+
return node
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const next = [...node]
|
|
109
|
+
next[2] = position.x
|
|
110
|
+
next[3] = position.y
|
|
111
|
+
return next
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const updateNodePositionInChunk = (nodeId, x, y) => {
|
|
115
|
+
if (!nodeId || !Number.isFinite(x) || !Number.isFinite(y)) {
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
state.chunk = {
|
|
120
|
+
...state.chunk,
|
|
121
|
+
nodes: normalizeList(state.chunk.nodes).map((node) => {
|
|
122
|
+
if (node?.[0] !== nodeId) {
|
|
123
|
+
return node
|
|
124
|
+
}
|
|
125
|
+
const next = [...node]
|
|
126
|
+
next[2] = x
|
|
127
|
+
next[3] = y
|
|
128
|
+
return next
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
state.spatialIndex.key = ''
|
|
132
|
+
|
|
133
|
+
if (state.renderWorker && state.workerReady) {
|
|
134
|
+
state.renderWorker.postMessage({ type: 'move-node', id: nodeId, x, y })
|
|
135
|
+
}
|
|
136
|
+
updateGraphOverlays()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const focusNodeInViewport = (nodeId, nextScale = null) => {
|
|
140
|
+
const node = nodeByIdFromChunk().get(nodeId)
|
|
141
|
+
if (!node) {
|
|
142
|
+
return false
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const x = Number(node[2])
|
|
146
|
+
const y = Number(node[3])
|
|
147
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
148
|
+
return false
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (Number.isFinite(nextScale)) {
|
|
152
|
+
state.camera.scale = clampScale(Number(nextScale))
|
|
153
|
+
}
|
|
154
|
+
state.camera.x = state.viewport.width / 2 - x * state.camera.scale
|
|
155
|
+
state.camera.y = state.viewport.height / 2 - y * state.camera.scale
|
|
156
|
+
updateWorkerCamera()
|
|
157
|
+
scheduleChunkFetch()
|
|
158
|
+
return true
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const showTooltip = (node, pointer) => {
|
|
162
|
+
if (!elements.tooltip || !node) {
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
elements.tooltip.hidden = false
|
|
167
|
+
elements.tooltip.innerHTML =
|
|
168
|
+
'<strong>' + escapeHtml(node[1] || node[0]) + '</strong>' +
|
|
169
|
+
'<small>' + escapeHtml(node[4] || node[5] || '') + '</small>'
|
|
170
|
+
elements.tooltip.style.left = Math.min(state.viewport.width - 24, pointer.x + 14) + 'px'
|
|
171
|
+
elements.tooltip.style.top = Math.min(state.viewport.height - 24, pointer.y + 14) + 'px'
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const hideTooltip = () => {
|
|
175
|
+
if (elements.tooltip) {
|
|
176
|
+
elements.tooltip.hidden = true
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const labelCandidates = () => {
|
|
181
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
182
|
+
const visible = nodes.filter((node) => {
|
|
183
|
+
const x = Number(node?.[2])
|
|
184
|
+
const y = Number(node?.[3])
|
|
185
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return false
|
|
186
|
+
const point = worldToScreen(x, y)
|
|
187
|
+
return point.x >= -80 && point.x <= state.viewport.width + 80 && point.y >= -80 && point.y <= state.viewport.height + 80
|
|
188
|
+
})
|
|
189
|
+
const shouldShowMany = state.camera.scale >= 0.72 || visible.length <= 120
|
|
190
|
+
const focused = state.focusedNodeIds
|
|
191
|
+
|
|
192
|
+
return visible
|
|
193
|
+
.filter((node) => shouldShowMany || focused.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId || Number(node?.[7]) > 5.5)
|
|
194
|
+
.sort((left, right) => {
|
|
195
|
+
const leftFocused = focused.has(left[0]) || left[0] === state.hoveredNodeId || left[0] === state.selectedNodeId ? 1 : 0
|
|
196
|
+
const rightFocused = focused.has(right[0]) || right[0] === state.hoveredNodeId || right[0] === state.selectedNodeId ? 1 : 0
|
|
197
|
+
if (rightFocused !== leftFocused) return rightFocused - leftFocused
|
|
198
|
+
return Number(right?.[7] ?? 0) - Number(left?.[7] ?? 0)
|
|
199
|
+
})
|
|
200
|
+
.slice(0, state.camera.scale >= 0.72 ? 160 : 48)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const drawLabels = () => {
|
|
204
|
+
if (!elements.labels) {
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
elements.labels.innerHTML = labelCandidates().map((node) => {
|
|
209
|
+
const point = worldToScreen(Number(node[2]), Number(node[3]))
|
|
210
|
+
const focused = state.focusedNodeIds.has(node[0]) || node[0] === state.hoveredNodeId || node[0] === state.selectedNodeId
|
|
211
|
+
return '<span class="graph-label' + (focused ? ' is-focused' : '') + '" style="left:' +
|
|
212
|
+
point.x.toFixed(1) + 'px;top:' + point.y.toFixed(1) + 'px">' + escapeHtml(node[1] || node[0]) + '</span>'
|
|
213
|
+
}).join('')
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const drawMiniMap = () => {
|
|
217
|
+
const miniMap = elements.miniMap
|
|
218
|
+
if (!(miniMap instanceof HTMLCanvasElement)) {
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
const nodes = normalizeList(state.chunk.nodes)
|
|
222
|
+
const ctx = miniMap.getContext('2d')
|
|
223
|
+
if (!ctx || nodes.length === 0) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const ratio = window.devicePixelRatio || 1
|
|
228
|
+
const width = miniMap.clientWidth || 180
|
|
229
|
+
const height = miniMap.clientHeight || 120
|
|
230
|
+
miniMap.width = Math.floor(width * ratio)
|
|
231
|
+
miniMap.height = Math.floor(height * ratio)
|
|
232
|
+
ctx.setTransform(ratio, 0, 0, ratio, 0, 0)
|
|
233
|
+
ctx.clearRect(0, 0, width, height)
|
|
234
|
+
ctx.fillStyle = 'rgba(8, 19, 29, 0.88)'
|
|
235
|
+
ctx.fillRect(0, 0, width, height)
|
|
236
|
+
|
|
237
|
+
const xs = nodes.map((node) => Number(node[2])).filter(Number.isFinite)
|
|
238
|
+
const ys = nodes.map((node) => Number(node[3])).filter(Number.isFinite)
|
|
239
|
+
const minX = Math.min(...xs)
|
|
240
|
+
const maxX = Math.max(...xs)
|
|
241
|
+
const minY = Math.min(...ys)
|
|
242
|
+
const maxY = Math.max(...ys)
|
|
243
|
+
const graphWidth = Math.max(1, maxX - minX)
|
|
244
|
+
const graphHeight = Math.max(1, maxY - minY)
|
|
245
|
+
const scale = Math.min((width - 18) / graphWidth, (height - 18) / graphHeight)
|
|
246
|
+
const offsetX = (width - graphWidth * scale) / 2
|
|
247
|
+
const offsetY = (height - graphHeight * scale) / 2
|
|
248
|
+
const toMini = (x, y) => ({
|
|
249
|
+
x: offsetX + (x - minX) * scale,
|
|
250
|
+
y: offsetY + (y - minY) * scale
|
|
251
|
+
})
|
|
252
|
+
state.miniMapView = { minX, minY, scale, offsetX, offsetY, width, height }
|
|
253
|
+
|
|
254
|
+
ctx.fillStyle = 'rgba(90, 168, 255, 0.62)'
|
|
255
|
+
nodes.forEach((node) => {
|
|
256
|
+
const point = toMini(Number(node[2]), Number(node[3]))
|
|
257
|
+
ctx.fillRect(point.x - 1, point.y - 1, 2, 2)
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
const worldTopLeft = screenToWorld(0, 0)
|
|
261
|
+
const worldBottomRight = screenToWorld(state.viewport.width, state.viewport.height)
|
|
262
|
+
const topLeft = toMini(Math.min(worldTopLeft.x, worldBottomRight.x), Math.min(worldTopLeft.y, worldBottomRight.y))
|
|
263
|
+
const bottomRight = toMini(Math.max(worldTopLeft.x, worldBottomRight.x), Math.max(worldTopLeft.y, worldBottomRight.y))
|
|
264
|
+
ctx.strokeStyle = 'rgba(90, 168, 255, 0.86)'
|
|
265
|
+
ctx.lineWidth = 1
|
|
266
|
+
ctx.strokeRect(topLeft.x, topLeft.y, Math.max(3, bottomRight.x - topLeft.x), Math.max(3, bottomRight.y - topLeft.y))
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const shouldDeferGraphOverlays = () => state.pointer.down || performance.now() - state.lastWheelAt < 150
|
|
270
|
+
|
|
271
|
+
const updateGraphOverlays = () => {
|
|
272
|
+
if (state.overlayScheduled) {
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
state.overlayScheduled = true
|
|
276
|
+
requestAnimationFrame(() => {
|
|
277
|
+
state.overlayScheduled = false
|
|
278
|
+
if (shouldDeferGraphOverlays()) {
|
|
279
|
+
elements.labels?.classList.add('is-stale')
|
|
280
|
+
if (!state.overlayIdleTimer) {
|
|
281
|
+
state.overlayIdleTimer = setTimeout(() => {
|
|
282
|
+
state.overlayIdleTimer = null
|
|
283
|
+
updateGraphOverlays()
|
|
284
|
+
}, 170)
|
|
285
|
+
}
|
|
286
|
+
return
|
|
287
|
+
}
|
|
288
|
+
elements.labels?.classList.remove('is-stale')
|
|
289
|
+
drawLabels()
|
|
290
|
+
if (state.miniMapDirty) {
|
|
291
|
+
drawMiniMap()
|
|
292
|
+
state.miniMapDirty = false
|
|
293
|
+
}
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
`;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export const createScopeThemeJs = () => `
|
|
2
|
+
const initialAgentFromUrl = (() => {
|
|
3
|
+
try {
|
|
4
|
+
const raw = new URL(window.location.href).searchParams.get('agent')
|
|
5
|
+
const value = raw?.trim() ?? ''
|
|
6
|
+
return value.length > 0 ? value : ''
|
|
7
|
+
} catch {
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
})()
|
|
11
|
+
|
|
12
|
+
const initialContextFromUrl = (() => {
|
|
13
|
+
try {
|
|
14
|
+
const raw = new URL(window.location.href).searchParams.get('context')
|
|
15
|
+
const value = raw?.trim() ?? ''
|
|
16
|
+
return value.length > 0 ? value : ''
|
|
17
|
+
} catch {
|
|
18
|
+
return ''
|
|
19
|
+
}
|
|
20
|
+
})()
|
|
21
|
+
|
|
22
|
+
const scopeQuery = (separator = '?') => {
|
|
23
|
+
const params = new URLSearchParams()
|
|
24
|
+
if (state.agentId) {
|
|
25
|
+
params.set('agent', state.agentId)
|
|
26
|
+
}
|
|
27
|
+
if (state.contextId) {
|
|
28
|
+
params.set('context', state.contextId)
|
|
29
|
+
}
|
|
30
|
+
const query = params.toString()
|
|
31
|
+
|
|
32
|
+
return query ? separator + query : ''
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const parseColor = (hex) => {
|
|
36
|
+
const normalized = String(hex || '#ffffff').replace('#', '')
|
|
37
|
+
const expanded = normalized.length === 3
|
|
38
|
+
? normalized.split('').map((char) => char + char).join('')
|
|
39
|
+
: normalized.padEnd(6, 'f')
|
|
40
|
+
const value = Number.parseInt(expanded, 16)
|
|
41
|
+
return [
|
|
42
|
+
((value >> 16) & 255) / 255,
|
|
43
|
+
((value >> 8) & 255) / 255,
|
|
44
|
+
(value & 255) / 255,
|
|
45
|
+
1
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const graphTheme = {
|
|
50
|
+
node: parseColor('#5aa8ff'),
|
|
51
|
+
nodeCluster: parseColor('#3f7fbd'),
|
|
52
|
+
nodeHighlight: parseColor('#ffcb67'),
|
|
53
|
+
nodeSelected: parseColor('#edf4ff'),
|
|
54
|
+
nodePalette: [
|
|
55
|
+
parseColor('#5aa8ff'),
|
|
56
|
+
parseColor('#5ecf92'),
|
|
57
|
+
parseColor('#ffb65c'),
|
|
58
|
+
parseColor('#ff7dac'),
|
|
59
|
+
parseColor('#a88fff'),
|
|
60
|
+
parseColor('#59d0dd'),
|
|
61
|
+
parseColor('#ff8f6a'),
|
|
62
|
+
parseColor('#a4b3c3'),
|
|
63
|
+
parseColor('#c9945f'),
|
|
64
|
+
parseColor('#7cb6ff')
|
|
65
|
+
],
|
|
66
|
+
edge: [0.59, 0.71, 0.83, 0.14],
|
|
67
|
+
edgeHeavy: [0.59, 0.71, 0.83, 0.3],
|
|
68
|
+
clear: parseColor('#08131d')
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const segmentPalette = ['#5aa8ff', '#5ecf92', '#ffb65c', '#ff7dac', '#a88fff', '#59d0dd', '#ff8f6a', '#a4b3c3', '#c9945f', '#7cb6ff']
|
|
72
|
+
|
|
73
|
+
const segmentColorIndex = (segment) => {
|
|
74
|
+
const value = String(segment || '')
|
|
75
|
+
let hash = 0
|
|
76
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
77
|
+
hash = ((hash << 5) - hash + value.charCodeAt(index)) | 0
|
|
78
|
+
}
|
|
79
|
+
return Math.abs(hash) % segmentPalette.length
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const segmentColor = (segment) => segmentPalette[segmentColorIndex(segment)] || segmentPalette[0]
|
|
83
|
+
const nodeKind = (node) => node?.[6] === 'cluster' ? 'cluster' : 'node'
|
|
84
|
+
const isRealGraphNode = (node) => nodeKind(node) === 'node'
|
|
85
|
+
|
|
86
|
+
const clampScale = (scale) => Math.max(zoomRange.min, Math.min(zoomRange.max, scale))
|
|
87
|
+
|
|
88
|
+
const getZoomNodeBudget = () => {
|
|
89
|
+
const scale = state.camera.scale
|
|
90
|
+
if (scale < 0.06) return 900
|
|
91
|
+
if (scale < 0.12) return 1600
|
|
92
|
+
if (scale < 0.24) return 2600
|
|
93
|
+
if (scale < 0.7) return 4000
|
|
94
|
+
return 6000
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const getZoomEdgeBudget = () => {
|
|
98
|
+
const scale = state.camera.scale
|
|
99
|
+
if (scale < 0.06) return 2000
|
|
100
|
+
if (scale < 0.12) return 4800
|
|
101
|
+
if (scale < 0.24) return 9000
|
|
102
|
+
if (scale < 0.7) return 15000
|
|
103
|
+
return 26000
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const zoomDetailBand = () => {
|
|
107
|
+
const scale = state.camera.scale
|
|
108
|
+
if (scale < 0.06) return 'far'
|
|
109
|
+
if (scale < 0.12) return 'wide'
|
|
110
|
+
if (scale < 0.24) return 'mid'
|
|
111
|
+
if (scale < 0.7) return 'near'
|
|
112
|
+
return 'detail'
|
|
113
|
+
}
|
|
114
|
+
`;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export const createSpatialJs = () => `
|
|
2
|
+
const graphStreamRequestKey = ({ x, y, w, h }) => {
|
|
3
|
+
const grid = Math.max(80, Math.min(720, Math.max(w, h) / 6))
|
|
4
|
+
return [
|
|
5
|
+
state.agentId || '*',
|
|
6
|
+
state.contextId || '*',
|
|
7
|
+
zoomDetailBand(),
|
|
8
|
+
getZoomNodeBudget(),
|
|
9
|
+
getZoomEdgeBudget(),
|
|
10
|
+
Math.round(x / grid),
|
|
11
|
+
Math.round(y / grid),
|
|
12
|
+
Math.round(w / grid),
|
|
13
|
+
Math.round(h / grid)
|
|
14
|
+
].join(':')
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const screenToWorld = (screenX, screenY) => ({
|
|
18
|
+
x: (screenX - state.camera.x) / state.camera.scale,
|
|
19
|
+
y: (screenY - state.camera.y) / state.camera.scale
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
const worldToScreen = (x, y) => ({
|
|
23
|
+
x: x * state.camera.scale + state.camera.x,
|
|
24
|
+
y: y * state.camera.scale + state.camera.y
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const spatialIndexKey = () => [
|
|
28
|
+
state.graphSignature,
|
|
29
|
+
state.camera.x.toFixed(1),
|
|
30
|
+
state.camera.y.toFixed(1),
|
|
31
|
+
state.camera.scale.toFixed(4),
|
|
32
|
+
normalizeList(state.chunk.nodes).length
|
|
33
|
+
].join(':')
|
|
34
|
+
|
|
35
|
+
const rebuildSpatialIndex = () => {
|
|
36
|
+
const key = spatialIndexKey()
|
|
37
|
+
if (state.spatialIndex.key === key) {
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const cellSize = 44
|
|
42
|
+
const cells = new Map()
|
|
43
|
+
normalizeList(state.chunk.nodes).forEach((node) => {
|
|
44
|
+
const id = typeof node?.[0] === 'string' ? node[0] : ''
|
|
45
|
+
if (!id) return
|
|
46
|
+
const x = Number(node?.[2])
|
|
47
|
+
const y = Number(node?.[3])
|
|
48
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) return
|
|
49
|
+
const point = worldToScreen(x, y)
|
|
50
|
+
const cellX = Math.floor(point.x / cellSize)
|
|
51
|
+
const cellY = Math.floor(point.y / cellSize)
|
|
52
|
+
const key = cellX + ',' + cellY
|
|
53
|
+
const bucket = cells.get(key)
|
|
54
|
+
if (bucket) {
|
|
55
|
+
bucket.push(node)
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
cells.set(key, [node])
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
state.spatialIndex = { key, cells }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const spatialCandidates = (screenX, screenY) => {
|
|
65
|
+
rebuildSpatialIndex()
|
|
66
|
+
const cellSize = 44
|
|
67
|
+
const cellX = Math.floor(screenX / cellSize)
|
|
68
|
+
const cellY = Math.floor(screenY / cellSize)
|
|
69
|
+
const nodes = []
|
|
70
|
+
|
|
71
|
+
for (let y = cellY - 1; y <= cellY + 1; y += 1) {
|
|
72
|
+
for (let x = cellX - 1; x <= cellX + 1; x += 1) {
|
|
73
|
+
nodes.push(...(state.spatialIndex.cells.get(x + ',' + y) ?? []))
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return nodes
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const nodeByIdFromChunk = () => new Map(normalizeList(state.chunk.nodes).map((node) => [node[0], node]))
|
|
81
|
+
|
|
82
|
+
const linkedNodeIds = (nodeId) => {
|
|
83
|
+
const ids = new Set(nodeId ? [nodeId] : [])
|
|
84
|
+
normalizeList(state.chunk.edges).forEach((edge) => {
|
|
85
|
+
if (edge?.[0] === nodeId && typeof edge?.[1] === 'string') ids.add(edge[1])
|
|
86
|
+
if (edge?.[1] === nodeId && typeof edge?.[0] === 'string') ids.add(edge[0])
|
|
87
|
+
})
|
|
88
|
+
return ids
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const setFocusedNodeIds = (ids) => {
|
|
92
|
+
state.focusedNodeIds = ids
|
|
93
|
+
if (state.renderWorker && state.workerReady) {
|
|
94
|
+
state.renderWorker.postMessage({ type: 'focus', ids: Array.from(ids) })
|
|
95
|
+
}
|
|
96
|
+
updateGraphOverlays()
|
|
97
|
+
}
|
|
98
|
+
`;
|