@pluot/react 0.1.0
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/LICENSE +299 -0
- package/dist/index.js +8244 -0
- package/dist-tsc/3d-view-controls.d.ts +17 -0
- package/dist-tsc/3d-view-controls.d.ts.map +1 -0
- package/dist-tsc/3d-view-controls.js +223 -0
- package/dist-tsc/Pluot.d.ts +2 -0
- package/dist-tsc/Pluot.d.ts.map +1 -0
- package/dist-tsc/Pluot.js +417 -0
- package/dist-tsc/dom-2d-camera.d.ts +26 -0
- package/dist-tsc/dom-2d-camera.d.ts.map +1 -0
- package/dist-tsc/dom-2d-camera.js +303 -0
- package/dist-tsc/feature-detection.d.ts +5 -0
- package/dist-tsc/feature-detection.d.ts.map +1 -0
- package/dist-tsc/feature-detection.js +6 -0
- package/dist-tsc/index.d.ts +3 -0
- package/dist-tsc/index.d.ts.map +1 -0
- package/dist-tsc/index.js +2 -0
- package/dist-tsc/lru-store.d.ts +4 -0
- package/dist-tsc/lru-store.d.ts.map +1 -0
- package/dist-tsc/lru-store.js +137 -0
- package/package.json +59 -0
- package/src/Pluot.jsx +560 -0
- package/src/index.js +2 -0
package/src/Pluot.jsx
ADDED
|
@@ -0,0 +1,560 @@
|
|
|
1
|
+
import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback } from "react";
|
|
2
|
+
import { mat4, vec4 } from "gl-matrix";
|
|
3
|
+
import lzs from "lz-string";
|
|
4
|
+
import { isEqual, throttle } from "lodash-es";
|
|
5
|
+
import { FetchStore } from 'zarrita';
|
|
6
|
+
import {
|
|
7
|
+
initialize, getIsWasmReady,
|
|
8
|
+
render_wasm, pick_wasm,
|
|
9
|
+
setStore, getStore,
|
|
10
|
+
create2dCamera, create3dCamera,
|
|
11
|
+
getBounds, getCameraMatrixFromBounds,
|
|
12
|
+
checkWebGpuFeatureDetection,
|
|
13
|
+
} from '@pluot/core';
|
|
14
|
+
|
|
15
|
+
// Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
|
|
16
|
+
// The requested module 'lz-string' is a CommonJS module,
|
|
17
|
+
// which may not support all module.exports as named exports."
|
|
18
|
+
const { decompressFromUint8Array } = lzs;
|
|
19
|
+
|
|
20
|
+
const DEFAULT_VIEW = new Float32Array([
|
|
21
|
+
1, 0, 0, 0,
|
|
22
|
+
0, 1, 0, 0,
|
|
23
|
+
0, 0, 1/200, 0,
|
|
24
|
+
0, 0, 0, 1,
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
const DEFAULT_3D_VIEW = new Float32Array([
|
|
29
|
+
1, 0, 0, 0,
|
|
30
|
+
0, 1, 0, 0,
|
|
31
|
+
0, 0, 1, 0,
|
|
32
|
+
0, 0, -10, 1,
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
function normalizePickingResult(data) {
|
|
36
|
+
const result = data;
|
|
37
|
+
if (data && Array.isArray(result.layer_results)) {
|
|
38
|
+
result.layer_results = result.layer_results.map(obj => ({
|
|
39
|
+
layer_id: obj.layer_id,
|
|
40
|
+
// This is needed because serde-wasm-bindgen
|
|
41
|
+
// converts Rust HashMap to JS Map.
|
|
42
|
+
info: Object.fromEntries(Array.from(obj.info)),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
export function Pluot(props) {
|
|
50
|
+
const {
|
|
51
|
+
width: widthProp,
|
|
52
|
+
height: heightProp,
|
|
53
|
+
plotId,
|
|
54
|
+
plotType,
|
|
55
|
+
store,
|
|
56
|
+
storeName: storeNameProp,
|
|
57
|
+
plotParams,
|
|
58
|
+
viewMode = "2d",
|
|
59
|
+
marginBottom = 100.0,
|
|
60
|
+
marginLeft = 100.0,
|
|
61
|
+
marginTop = 100.0,
|
|
62
|
+
marginRight = 100.0,
|
|
63
|
+
aspectRatioMode = "Contain", // "Ignore", "Contain", "Cover"
|
|
64
|
+
aspectRatioAlignmentMode = "Start", // "Center", "Start", "End"
|
|
65
|
+
format = "Raster", // "Raster", "Vector"
|
|
66
|
+
minTimeout = 32,
|
|
67
|
+
maxTimeout = 32,
|
|
68
|
+
allowSimultaneousRenders = true,
|
|
69
|
+
debugMargins = false,
|
|
70
|
+
} = props;
|
|
71
|
+
|
|
72
|
+
const width = Math.floor(widthProp);
|
|
73
|
+
const height = Math.floor(heightProp);
|
|
74
|
+
|
|
75
|
+
const isVector = format === "Vector";
|
|
76
|
+
|
|
77
|
+
const storeName = useMemo(() => {
|
|
78
|
+
if (storeNameProp) {
|
|
79
|
+
return storeNameProp;
|
|
80
|
+
}
|
|
81
|
+
// If store is a string, assume it is a URL and initialize a FetchStore here.
|
|
82
|
+
if (store) {
|
|
83
|
+
if (typeof store === 'string') {
|
|
84
|
+
return setStore(new FetchStore(store), plotId);
|
|
85
|
+
}
|
|
86
|
+
return setStore(store, plotId);
|
|
87
|
+
}
|
|
88
|
+
throw new Error("Either storeName or store must be provided.");
|
|
89
|
+
}, [storeNameProp, store]);
|
|
90
|
+
|
|
91
|
+
const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
|
|
92
|
+
|
|
93
|
+
const svgRef = useRef(null);
|
|
94
|
+
const canvasRef = useRef(null);
|
|
95
|
+
const cameraRef = useRef(null);
|
|
96
|
+
|
|
97
|
+
const tempButtonRef = useRef(null);
|
|
98
|
+
|
|
99
|
+
// We may want to update these things without triggering a re-render.
|
|
100
|
+
const isRenderingRef = useRef(false);
|
|
101
|
+
const currentTimeout = useRef(maxTimeout);
|
|
102
|
+
|
|
103
|
+
// TODO: do we want to use the backlog approach or not?
|
|
104
|
+
// (Similar to the one used in the Vitessce heatmap)
|
|
105
|
+
// Reference: https://github.com/vitessce/vitessce/blob/71f17fb605768e0428fb15ed87b3ea34bcbb4803/packages/view-types/heatmap/src/Heatmap.js#L368
|
|
106
|
+
//const backlogRef = useRef([]);
|
|
107
|
+
const [backlogIteration, incBacklogIteration] = useReducer(i => i + 1, 0);
|
|
108
|
+
|
|
109
|
+
const [isWasmReady, setIsWasmReady] = useState(false);
|
|
110
|
+
const [didFirstRender, setDidFirstRender] = useState(false);
|
|
111
|
+
const [bailedEarly, setBailedEarly] = useState(true);
|
|
112
|
+
|
|
113
|
+
const [pickingResult, setPickingResult] = useState(null);
|
|
114
|
+
|
|
115
|
+
// TODO: handle a viewMatrix that is provided and set via props,
|
|
116
|
+
// to enable usage as a controlled component
|
|
117
|
+
// (e.g., for linked views with shared cameras).
|
|
118
|
+
const [viewMatrix, setViewMatrix] = useState(
|
|
119
|
+
// Note: We use an initializer function here to avoid
|
|
120
|
+
// sharing the same Float32Array among multiple Pluot
|
|
121
|
+
// component instances that may be rendered on the same page.
|
|
122
|
+
() => new Float32Array(DEFAULT_VIEW)
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
useLayoutEffect(() => {
|
|
126
|
+
initialize().then(() => setIsWasmReady(getIsWasmReady()));
|
|
127
|
+
}, []);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
// Reset view matrix on plot change.
|
|
131
|
+
// Create a new Float32Array to avoid sharing a mutable array
|
|
132
|
+
// among multiple Pluot component instances.
|
|
133
|
+
setViewMatrix(new Float32Array(viewMode === "2d" ? DEFAULT_VIEW : DEFAULT_3D_VIEW));
|
|
134
|
+
//viewMatrixRef.current = new Float32Array(DEFAULT_VIEW);
|
|
135
|
+
}, [plotId, viewMode]);
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// Set up the camera.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
// Set up the camera.
|
|
141
|
+
const cameraEl = cameraRef.current;
|
|
142
|
+
if (!cameraEl) {
|
|
143
|
+
return () => {};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
let dispose = () => {};
|
|
147
|
+
|
|
148
|
+
// Create a 2D camera for handling zoom and pan.
|
|
149
|
+
if (viewMode === "2d") {
|
|
150
|
+
function onCameraEvent(camera, event) {
|
|
151
|
+
camera.tick();
|
|
152
|
+
// Reference: https://github.com/flekschas/regl-scatterplot/blob/17a650c352fad313d1574472b2fdc5f58b9e1eca/src/index.js#L1648
|
|
153
|
+
|
|
154
|
+
setViewMatrix(prev => {
|
|
155
|
+
// Since camera events happen even on mousemove events that do not change the matrix,
|
|
156
|
+
// we check for equality here to avoid unnecessary state updates and plot re-renders.
|
|
157
|
+
if (isEqual(prev, camera.view)) {
|
|
158
|
+
return prev;
|
|
159
|
+
}
|
|
160
|
+
return mat4.clone(camera.view)
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
currentTimeout.current = minTimeout;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const camera = create2dCamera(cameraEl, {
|
|
167
|
+
isFixed: false,
|
|
168
|
+
distance: 0.0,
|
|
169
|
+
//target: [0.0, 0.0],
|
|
170
|
+
//viewCenter: [0.5, 0.5], // Should this be used when the coordinate system is (0 to 1) rather than (-1 to 1)?
|
|
171
|
+
viewCenter: [0.0, 0.0],
|
|
172
|
+
defaultMouseDownMoveAction: "pan",
|
|
173
|
+
|
|
174
|
+
onKeyDown: (event) => {
|
|
175
|
+
onCameraEvent(camera, event);
|
|
176
|
+
},
|
|
177
|
+
onKeyUp: (event) => {
|
|
178
|
+
onCameraEvent(camera, event);
|
|
179
|
+
},
|
|
180
|
+
onMouseDown: (event) => {
|
|
181
|
+
onCameraEvent(camera, event);
|
|
182
|
+
},
|
|
183
|
+
onMouseUp: (event) => {
|
|
184
|
+
onCameraEvent(camera, event);
|
|
185
|
+
},
|
|
186
|
+
onMouseMove: (event) => {
|
|
187
|
+
onCameraEvent(camera, event);
|
|
188
|
+
},
|
|
189
|
+
onWheel: (event) => {
|
|
190
|
+
onCameraEvent(camera, event);
|
|
191
|
+
},
|
|
192
|
+
aspectRatioMode: aspectRatioMode,
|
|
193
|
+
aspectRatioAlignmentMode: aspectRatioAlignmentMode,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
// Set the initial view matrix.
|
|
198
|
+
// We need to ensure we create a new copy of the array.
|
|
199
|
+
camera.setView(new Float32Array(viewMatrix));
|
|
200
|
+
|
|
201
|
+
const tempHandler = e => {
|
|
202
|
+
// camera.setScaleBounds([[xScaleMin, xScaleMax], [yScaleMin, yScaleMax]])
|
|
203
|
+
//camera.lookAt([2.0, 2.0], 2.0);
|
|
204
|
+
//onCameraEvent(camera, null);
|
|
205
|
+
|
|
206
|
+
// Only zoom/pan the X axis; keep Y unchanged
|
|
207
|
+
const nextCameraMatrix = getCameraMatrixFromBounds(
|
|
208
|
+
{ yMin: 0.0, yMax: 100.0 },
|
|
209
|
+
new Float32Array(viewMatrix),
|
|
210
|
+
{
|
|
211
|
+
width,
|
|
212
|
+
height,
|
|
213
|
+
aspectRatioMode,
|
|
214
|
+
aspectRatioAlignmentMode,
|
|
215
|
+
margins: {
|
|
216
|
+
marginTop,
|
|
217
|
+
marginBottom,
|
|
218
|
+
marginLeft,
|
|
219
|
+
marginRight
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
console.log("done", nextCameraMatrix)
|
|
225
|
+
|
|
226
|
+
camera.setView(nextCameraMatrix);
|
|
227
|
+
onCameraEvent(camera, null);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
tempButtonRef.current.addEventListener('click', tempHandler);
|
|
231
|
+
|
|
232
|
+
// Set up an onClick handler.
|
|
233
|
+
//
|
|
234
|
+
const clickHandler = (event) => {
|
|
235
|
+
pickFrame(event.offsetX, event.offsetY);
|
|
236
|
+
};
|
|
237
|
+
cameraEl.addEventListener("click", clickHandler);
|
|
238
|
+
|
|
239
|
+
dispose = () => {
|
|
240
|
+
camera.dispose();
|
|
241
|
+
cameraEl.removeEventListener("click", clickHandler);
|
|
242
|
+
|
|
243
|
+
tempButtonRef.current.removeEventListener('click', tempHandler);
|
|
244
|
+
};
|
|
245
|
+
} else if (viewMode === "3d") {
|
|
246
|
+
function onCameraEvent(camera, event) {
|
|
247
|
+
camera.tick();
|
|
248
|
+
// Note: the 3D camera stores the matrix in camera.matrix (not camera.view).
|
|
249
|
+
setViewMatrix(prev => {
|
|
250
|
+
// Since camera events happen even on mousemove events that do not change the matrix,
|
|
251
|
+
// we check for equality here to avoid unnecessary state updates and plot re-renders.
|
|
252
|
+
if (isEqual(prev, camera.matrix)) {
|
|
253
|
+
return prev;
|
|
254
|
+
}
|
|
255
|
+
return mat4.clone(camera.matrix)
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const camera = create3dCamera(cameraEl, {
|
|
260
|
+
mode: "orbit",
|
|
261
|
+
zoomSpeed: -5,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// TODO:
|
|
265
|
+
// - fork 3d-view-controls and remove usage of "global" - then clean up vite config.
|
|
266
|
+
// - define a camera.dispsose option.
|
|
267
|
+
|
|
268
|
+
// Reference: https://github.com/flekschas/dom-2d-camera/blob/cd59ea035a0ea72c2c0535fa3721f8127946576c/src/index.js#L237C3-L315C71
|
|
269
|
+
const keyUpHandler = (event) => {
|
|
270
|
+
// TODO
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
const keyDownHandler = (event) => {
|
|
274
|
+
// TODO
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const mouseUpHandler = (event) => {
|
|
278
|
+
// TODO
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const mouseDownHandler = (event) => {
|
|
282
|
+
// TODO
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
// TODO: use react state?
|
|
286
|
+
var lastX = 0;
|
|
287
|
+
var lastY = 0;
|
|
288
|
+
|
|
289
|
+
// Reference: https://github.com/mikolalysenko/3d-view/blob/8269e02337bba1923173a750aa7f3f0f76c91ba5/example/minimal.js#L67
|
|
290
|
+
const mouseMoveHandler = (event) => {
|
|
291
|
+
/*
|
|
292
|
+
var dx = (event.clientX - lastX) / width;
|
|
293
|
+
var dy = -(event.clientY - lastY) / height;
|
|
294
|
+
if (event.which === 1) {
|
|
295
|
+
if (event.shiftKey) {
|
|
296
|
+
//zoom
|
|
297
|
+
camera.rotate(now(), 0, 0, dx);
|
|
298
|
+
} else {
|
|
299
|
+
//rotate
|
|
300
|
+
camera.rotate(now(), dx, dy);
|
|
301
|
+
}
|
|
302
|
+
} else if (event.which === 3) {
|
|
303
|
+
//pan
|
|
304
|
+
camera.pan(now(), dx, dy);
|
|
305
|
+
}
|
|
306
|
+
lastX = event.clientX;
|
|
307
|
+
lastY = event.clientY;
|
|
308
|
+
*/
|
|
309
|
+
onCameraEvent(camera, event);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const wheelHandler = (event) => {
|
|
313
|
+
//camera.pan(now(), 0, 0, event.deltaY);
|
|
314
|
+
onCameraEvent(camera, event);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
cameraEl.addEventListener("keydown", keyDownHandler);
|
|
318
|
+
cameraEl.addEventListener("keyup", keyUpHandler);
|
|
319
|
+
cameraEl.addEventListener("mousedown", mouseDownHandler);
|
|
320
|
+
cameraEl.addEventListener("mouseup", mouseUpHandler);
|
|
321
|
+
cameraEl.addEventListener("mousemove", mouseMoveHandler);
|
|
322
|
+
cameraEl.addEventListener("wheel", wheelHandler);
|
|
323
|
+
|
|
324
|
+
dispose = () => {
|
|
325
|
+
cameraEl.removeEventListener("keydown", keyDownHandler);
|
|
326
|
+
cameraEl.removeEventListener("keyup", keyUpHandler);
|
|
327
|
+
cameraEl.removeEventListener("mousedown", mouseDownHandler);
|
|
328
|
+
cameraEl.removeEventListener("mouseup", mouseUpHandler);
|
|
329
|
+
cameraEl.removeEventListener("mousemove", mouseMoveHandler);
|
|
330
|
+
cameraEl.removeEventListener("wheel", wheelHandler);
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
//camera.matrix = new Float32Array(viewMatrix);
|
|
334
|
+
|
|
335
|
+
} else {
|
|
336
|
+
throw new Error("Unknown mode found.");
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return dispose;
|
|
340
|
+
}, [cameraRef, viewMode, aspectRatioMode, aspectRatioAlignmentMode, width, height, marginLeft, marginRight, marginTop, marginBottom]);
|
|
341
|
+
|
|
342
|
+
// The picking callback.
|
|
343
|
+
const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
|
|
344
|
+
const renderParams = {
|
|
345
|
+
width,
|
|
346
|
+
height,
|
|
347
|
+
format: format,
|
|
348
|
+
margin_bottom: marginBottom,
|
|
349
|
+
margin_left: marginLeft,
|
|
350
|
+
margin_top: marginTop,
|
|
351
|
+
margin_right: marginRight,
|
|
352
|
+
device_pixel_ratio: window.devicePixelRatio,
|
|
353
|
+
aspect_ratio_mode: aspectRatioMode,
|
|
354
|
+
aspect_ratio_alignment_mode: aspectRatioAlignmentMode,
|
|
355
|
+
view_mode: viewMode,
|
|
356
|
+
pickable: false,
|
|
357
|
+
// Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
|
|
358
|
+
camera_view: viewMatrix,
|
|
359
|
+
plot_id: plotId,
|
|
360
|
+
plot_type: plotType,
|
|
361
|
+
store_name: storeName,
|
|
362
|
+
plot_params: plotParams,
|
|
363
|
+
// Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
|
|
364
|
+
timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
|
|
365
|
+
wait_for_store_gets: false, // TODO: lift this value up to pass/use it in the window.zarr_ functions as well?
|
|
366
|
+
cache_enabled: true,
|
|
367
|
+
svg_compression_enabled: true,
|
|
368
|
+
svg_include_document: false,
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const layerHeight = height - marginTop - marginBottom;
|
|
372
|
+
|
|
373
|
+
setPickingResult(normalizePickingResult(await pick_wasm(
|
|
374
|
+
renderParams,
|
|
375
|
+
// The coordinates are relative to the "layer" (the camera region), not the full width/height.
|
|
376
|
+
// We also need to flip the Y coordinate so that positive is up.
|
|
377
|
+
screenCoordX + marginLeft,
|
|
378
|
+
marginBottom + (layerHeight - screenCoordY)
|
|
379
|
+
)));
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
// The renderFrame callback.
|
|
384
|
+
// We use useEffectEvent because we want to "see"
|
|
385
|
+
// the latest values of viewMatrix, plotProps, etc.
|
|
386
|
+
const renderFrame = useEffectEvent(async () => {
|
|
387
|
+
isRenderingRef.current = true;
|
|
388
|
+
console.log('wasm.render');
|
|
389
|
+
|
|
390
|
+
const renderParams = {
|
|
391
|
+
width,
|
|
392
|
+
height,
|
|
393
|
+
format: format,
|
|
394
|
+
margin_bottom: marginBottom,
|
|
395
|
+
margin_left: marginLeft,
|
|
396
|
+
margin_top: marginTop,
|
|
397
|
+
margin_right: marginRight,
|
|
398
|
+
device_pixel_ratio: window.devicePixelRatio,
|
|
399
|
+
aspect_ratio_mode: aspectRatioMode,
|
|
400
|
+
aspect_ratio_alignment_mode: aspectRatioAlignmentMode,
|
|
401
|
+
view_mode: viewMode,
|
|
402
|
+
pickable: false,
|
|
403
|
+
// Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
|
|
404
|
+
camera_view: viewMatrix,
|
|
405
|
+
plot_id: plotId,
|
|
406
|
+
plot_type: plotType,
|
|
407
|
+
store_name: storeName,
|
|
408
|
+
plot_params: plotParams,
|
|
409
|
+
// Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
|
|
410
|
+
timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
|
|
411
|
+
wait_for_store_gets: false, // TODO: lift this value up to pass/use it in the window.zarr_ functions as well?
|
|
412
|
+
cache_enabled: true,
|
|
413
|
+
svg_compression_enabled: true,
|
|
414
|
+
svg_include_document: false,
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
// Wrap render_wasm in try/catch, to handle Rust panics.
|
|
418
|
+
let arr;
|
|
419
|
+
try {
|
|
420
|
+
arr = await render_wasm(renderParams);
|
|
421
|
+
|
|
422
|
+
isRenderingRef.current = false;
|
|
423
|
+
} catch (error) {
|
|
424
|
+
console.error("Error during wasm.render_wasm:", error);
|
|
425
|
+
// Cleanup
|
|
426
|
+
isRenderingRef.current = false;
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (isVector) {
|
|
431
|
+
// Format: Vector (render to SVG)
|
|
432
|
+
const gContents = decompressFromUint8Array(arr);
|
|
433
|
+
|
|
434
|
+
//console.log(gContents)
|
|
435
|
+
|
|
436
|
+
if (!svgRef.current) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
svgRef.current.innerHTML = gContents;
|
|
440
|
+
|
|
441
|
+
// TODO: check for bailed early
|
|
442
|
+
} else {
|
|
443
|
+
// Format: Raster (render to canvas)
|
|
444
|
+
const canvas = canvasRef.current;
|
|
445
|
+
if (!canvas) {
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const ctx = canvas.getContext("2d");
|
|
449
|
+
if (!ctx) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// TODO: is there a more efficient way to do this?
|
|
453
|
+
// E.g., write to a webgl texture? or is this fast enough already?
|
|
454
|
+
const imageData = new ImageData(
|
|
455
|
+
new Uint8ClampedArray(arr.subarray(0, -1)),
|
|
456
|
+
width,
|
|
457
|
+
height,
|
|
458
|
+
);
|
|
459
|
+
ctx.putImageData(imageData, 0, 0);
|
|
460
|
+
|
|
461
|
+
const frameBailedEarly = arr.at(-1) === 1;
|
|
462
|
+
if (frameBailedEarly) {
|
|
463
|
+
currentTimeout.current = maxTimeout;
|
|
464
|
+
incBacklogIteration(); // Increment this to force a re-render.
|
|
465
|
+
setBailedEarly(true); // Update this to show the loading indicator.
|
|
466
|
+
} else {
|
|
467
|
+
// Successful render.
|
|
468
|
+
setBailedEarly(false); // Update this to hide the loading indicator.
|
|
469
|
+
|
|
470
|
+
// Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
|
|
471
|
+
const storeUsed = getStore(renderParams.store_name);
|
|
472
|
+
if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
|
|
473
|
+
storeUsed.clearCache();
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
setDidFirstRender(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const throttledRender = useMemo(
|
|
481
|
+
() => throttle(
|
|
482
|
+
renderFrame,
|
|
483
|
+
16, // ~60fps
|
|
484
|
+
// When both leading and trailing are true (the default):
|
|
485
|
+
// - First call -> executes immediately (leading edge)
|
|
486
|
+
// - Calls during the wait window -> ignored, but the most recent one is remembered.
|
|
487
|
+
// - After the wait period expires -> the last remembered call is executed (trailing edge).
|
|
488
|
+
{ leading: true, trailing: true }
|
|
489
|
+
), []);
|
|
490
|
+
|
|
491
|
+
useEffect(() => {
|
|
492
|
+
return () => throttledRender.cancel();
|
|
493
|
+
}, [throttledRender]);
|
|
494
|
+
|
|
495
|
+
// TODO: use react-query?
|
|
496
|
+
useEffect(() => {
|
|
497
|
+
if (!isWasmReady) {
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// We want to allow for simultaneous renders, as this makes user interactions feel
|
|
502
|
+
// much smoother. However, we allow for users to opt-out, and we also
|
|
503
|
+
// need to prevent simultaneous renders prior to the first render, as the first
|
|
504
|
+
// render initializes cached values and stuff.
|
|
505
|
+
if (isRenderingRef.current && (!didFirstRender || bailedEarly || !allowSimultaneousRenders)) {
|
|
506
|
+
// Prevent multiple render calls prior to the first successful render.
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Render on the next animation frame.
|
|
511
|
+
throttledRender();
|
|
512
|
+
}, [isWasmReady, didFirstRender, viewMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
|
|
513
|
+
width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
|
|
514
|
+
|
|
515
|
+
return (
|
|
516
|
+
<>
|
|
517
|
+
<div style={{ width, height, position: "relative" }}>
|
|
518
|
+
{!supportsWebGpu ? (
|
|
519
|
+
<p>{supportsWebGpuMessage}</p>
|
|
520
|
+
) : null}
|
|
521
|
+
<div
|
|
522
|
+
ref={cameraRef}
|
|
523
|
+
style={{
|
|
524
|
+
position: "absolute",
|
|
525
|
+
top: marginTop,
|
|
526
|
+
left: marginLeft,
|
|
527
|
+
width: width - marginLeft - marginRight,
|
|
528
|
+
height: height - marginTop - marginBottom,
|
|
529
|
+
border: `${debugMargins ? 1 : 0}px solid red`,
|
|
530
|
+
}}
|
|
531
|
+
/>
|
|
532
|
+
{isVector ? (
|
|
533
|
+
<svg
|
|
534
|
+
ref={svgRef}
|
|
535
|
+
style={{ width, height, border: "1px solid black" }}
|
|
536
|
+
width={width}
|
|
537
|
+
height={height}
|
|
538
|
+
viewBox={`0 0 ${width} ${height}`}
|
|
539
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
540
|
+
>
|
|
541
|
+
</svg>
|
|
542
|
+
) : (
|
|
543
|
+
<canvas
|
|
544
|
+
ref={canvasRef}
|
|
545
|
+
style={{ width, height, border: "1px solid black" }}
|
|
546
|
+
width={width}
|
|
547
|
+
height={height}
|
|
548
|
+
/>
|
|
549
|
+
)}
|
|
550
|
+
</div>
|
|
551
|
+
{bailedEarly ? (
|
|
552
|
+
<p>Loading...</p>
|
|
553
|
+
) : null}
|
|
554
|
+
<button ref={tempButtonRef} style={{ display: 'none' }}>Try lookAt</button>
|
|
555
|
+
{pickingResult ? (
|
|
556
|
+
<pre>{JSON.stringify(pickingResult, null, 2)}</pre>
|
|
557
|
+
) : null}
|
|
558
|
+
</>
|
|
559
|
+
);
|
|
560
|
+
}
|
package/src/index.js
ADDED