@andespindola/brainlink 1.6.1 → 1.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,7 +12,7 @@ const mainThreadWebGlAvailable = () => {
12
12
  // Uma vez que o controle do canvas é transferido para o worker (OffscreenCanvas),
13
13
  // o elemento original não fornece mais nenhum contexto (2D ou WebGL). Quando o
14
14
  // worker falha ao iniciar o WebGL, trocamos por um canvas novo para que o
15
- // fallback 2D consiga desenhar o grafo.
15
+ // renderer da thread principal (WebGL ou o fallback 2D) consiga desenhar.
16
16
  const replaceGraphCanvas = () => {
17
17
  const replacement = canvas.cloneNode(false)
18
18
  if (canvas.parentNode) {
@@ -24,6 +24,8 @@ const replaceGraphCanvas = () => {
24
24
 
25
25
  const enterFallbackMode = (rebindInput) => {
26
26
  state.rendererMode = 'fallback'
27
+ state.renderWorker = null
28
+ state.workerReady = false
27
29
  if (rebindInput) {
28
30
  setupInput()
29
31
  }
@@ -31,58 +33,126 @@ const enterFallbackMode = (rebindInput) => {
31
33
  drawFallback()
32
34
  }
33
35
 
36
+ // Renderiza com WebGL na própria thread principal quando o worker não está
37
+ // disponível ou falhou. Reaproveita exatamente o mesmo renderer do worker,
38
+ // dirigido por chamadas diretas em vez de mensagens de Worker: state.renderWorker
39
+ // recebe um shim com .postMessage/.terminate, então todos os call-sites do
40
+ // cliente (chunk/camera/pick/etc.) continuam iguais.
41
+ const setupMainThreadWebGl = (rebindInput) => {
42
+ if (!mainThreadWebGlAvailable()) {
43
+ enterFallbackMode(rebindInput)
44
+ return false
45
+ }
46
+
47
+ try {
48
+ const renderer = createGraphRenderer({
49
+ postMessage: (message) => handleRendererMessage(message),
50
+ requestAnimationFrame: (callback) => window.requestAnimationFrame(callback)
51
+ })
52
+
53
+ state.rendererMode = 'mainthread'
54
+ state.renderWorker = {
55
+ postMessage: (message) => renderer.handleMessage(message),
56
+ terminate: () => {}
57
+ }
58
+
59
+ if (rebindInput) {
60
+ setupInput()
61
+ }
62
+
63
+ renderer.handleMessage({
64
+ type: 'init',
65
+ canvas,
66
+ width: state.viewport.width,
67
+ height: state.viewport.height,
68
+ devicePixelRatio: state.viewport.ratio,
69
+ camera: state.camera,
70
+ theme: graphTheme
71
+ })
72
+ return true
73
+ } catch (error) {
74
+ console.error(error)
75
+ state.renderWorker = null
76
+ enterFallbackMode(rebindInput)
77
+ return false
78
+ }
79
+ }
80
+
81
+ // Canal único de mensagens do renderer, compartilhado pelo worker e pela thread
82
+ // principal. As saídas (ready/pick-result/frame-stats/fatal) chegam aqui igual
83
+ // nos dois modos.
84
+ const handleRendererMessage = (payload) => {
85
+ if (!payload || typeof payload !== 'object') {
86
+ return
87
+ }
88
+
89
+ if (payload.type === 'ready') {
90
+ state.workerReady = true
91
+ scheduleChunkFetch({ fit: true })
92
+ return
93
+ }
94
+
95
+ if (payload.type === 'pick-result') {
96
+ if (payload.node && typeof payload.node.id === 'string' && payload.node.id.length > 0) {
97
+ handlePickedNode(payload.node)
98
+ }
99
+ return
100
+ }
101
+
102
+ if (payload.type === 'frame-stats') {
103
+ state.lastVisibleNodes = Number.isFinite(payload.visibleNodes) ? payload.visibleNodes : state.lastVisibleNodes
104
+ state.lastVisibleEdges = Number.isFinite(payload.visibleEdges) ? payload.visibleEdges : state.lastVisibleEdges
105
+ return
106
+ }
107
+
108
+ if (payload.type === 'fatal') {
109
+ console.error(payload.message)
110
+ state.workerReady = false
111
+
112
+ // Já estamos no WebGL da thread principal: não há degrau acelerado
113
+ // adicional, então cai no canvas 2D.
114
+ if (state.rendererMode === 'mainthread') {
115
+ state.renderWorker = null
116
+ enterFallbackMode(true)
117
+ return
118
+ }
119
+
120
+ // O WebGL falhou dentro do worker. O canvas já foi transferido para ele,
121
+ // então troca por um novo e tenta WebGL na thread principal antes de
122
+ // desistir para o fallback 2D.
123
+ if (state.renderWorker && typeof state.renderWorker.terminate === 'function') {
124
+ state.renderWorker.terminate()
125
+ }
126
+ state.renderWorker = null
127
+ replaceGraphCanvas()
128
+ setupMainThreadWebGl(true)
129
+ }
130
+ }
131
+
34
132
  const setupRenderWorker = () => {
35
133
  const hasWorker = typeof Worker !== 'undefined'
36
134
  const canTransfer = typeof canvas.transferControlToOffscreen === 'function'
37
135
 
38
- if (!hasWorker || !canTransfer || !mainThreadWebGlAvailable()) {
136
+ // Sem WebGL em lugar nenhum: só resta o canvas 2D.
137
+ if (!mainThreadWebGlAvailable()) {
39
138
  enterFallbackMode(false)
40
139
  return
41
140
  }
42
141
 
142
+ // Tem WebGL, mas não dá para usar o worker (sem Worker ou sem OffscreenCanvas):
143
+ // renderiza WebGL na thread principal em vez de cair direto no 2D.
144
+ if (!hasWorker || !canTransfer) {
145
+ setupMainThreadWebGl(false)
146
+ return
147
+ }
148
+
43
149
  try {
44
150
  const offscreen = canvas.transferControlToOffscreen()
45
151
  const worker = new Worker('/render-worker.js')
152
+ state.rendererMode = 'worker'
46
153
  state.renderWorker = worker
47
154
 
48
- worker.onmessage = (event) => {
49
- const payload = event.data
50
- if (!payload || typeof payload !== 'object') {
51
- return
52
- }
53
-
54
- if (payload.type === 'ready') {
55
- state.workerReady = true
56
- scheduleChunkFetch({ fit: true })
57
- return
58
- }
59
-
60
- if (payload.type === 'pick-result') {
61
- if (payload.node && typeof payload.node.id === 'string' && payload.node.id.length > 0) {
62
- handlePickedNode(payload.node)
63
- }
64
- return
65
- }
66
-
67
- if (payload.type === 'frame-stats') {
68
- state.lastVisibleNodes = Number.isFinite(payload.visibleNodes) ? payload.visibleNodes : state.lastVisibleNodes
69
- state.lastVisibleEdges = Number.isFinite(payload.visibleEdges) ? payload.visibleEdges : state.lastVisibleEdges
70
- return
71
- }
72
-
73
- if (payload.type === 'fatal') {
74
- console.error(payload.message)
75
- state.workerReady = false
76
- if (state.renderWorker) {
77
- state.renderWorker.terminate()
78
- }
79
- state.renderWorker = null
80
- // O canvas já foi transferido para o worker: troca por um novo e
81
- // religa os eventos de interação antes de cair no fallback 2D.
82
- replaceGraphCanvas()
83
- enterFallbackMode(true)
84
- }
85
- }
155
+ worker.onmessage = (event) => handleRendererMessage(event && event.data)
86
156
 
87
157
  worker.postMessage({
88
158
  type: 'init',
@@ -95,8 +165,10 @@ const setupRenderWorker = () => {
95
165
  }, [offscreen])
96
166
  } catch (error) {
97
167
  console.error(error)
168
+ // O canvas pode já ter sido transferido: troca por um novo e tenta WebGL
169
+ // na thread principal antes do fallback 2D.
98
170
  replaceGraphCanvas()
99
- enterFallbackMode(false)
171
+ setupMainThreadWebGl(false)
100
172
  }
101
173
  }
102
174
 
@@ -8,6 +8,7 @@ import { createChunkFetchJs } from './client/chunk-fetch.js';
8
8
  import { createInputJs } from './client/input.js';
9
9
  import { createUploadJs } from './client/upload.js';
10
10
  import { createControlsJs } from './client/controls.js';
11
+ import { createGraphRendererJs } from './client/graph-renderer.js';
11
12
  import { createWorkerBootstrapJs } from './client/worker-bootstrap.js';
12
13
  export const createClientJs = () => [
13
14
  createElementsJs(),
@@ -20,5 +21,6 @@ export const createClientJs = () => [
20
21
  createInputJs(),
21
22
  createUploadJs(),
22
23
  createControlsJs(),
24
+ createGraphRendererJs(),
23
25
  createWorkerBootstrapJs()
24
26
  ].join('');