@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.
@@ -0,0 +1,17 @@
1
+ export default function createCamera(element: any, options: any): {
2
+ view: any;
3
+ element: any;
4
+ delay: any;
5
+ rotateSpeed: any;
6
+ zoomSpeed: any;
7
+ translateSpeed: any;
8
+ flipX: boolean;
9
+ flipY: boolean;
10
+ modes: any;
11
+ tick: () => boolean;
12
+ lookAt: (center: any, eye: any, up: any) => void;
13
+ rotate: (pitch: any, yaw: any, roll: any) => void;
14
+ pan: (dx: any, dy: any, dz: any) => void;
15
+ translate: (dx: any, dy: any, dz: any) => void;
16
+ };
17
+ //# sourceMappingURL=3d-view-controls.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"3d-view-controls.d.ts","sourceRoot":"","sources":["../src/3d-view-controls.js"],"names":[],"mappings":"AAmBA;;;;;;;;;;;;;;;EAgOC"}
@@ -0,0 +1,223 @@
1
+ // Reference: https://github.com/mikolalysenko/3d-view-controls/blob/0e59c2ae4a891ce3c7bb83aa4291f89d99366037/camera.js
2
+ import createView from '3d-view';
3
+ import mouseChange from 'mouse-change';
4
+ import mouseWheel from 'mouse-wheel';
5
+ import mouseOffset from 'mouse-event-offset';
6
+ import hasPassive from 'has-passive-events';
7
+ // Updated right-now implementation to avoid `global` usage.
8
+ // Reference: https://github.com/hughsk/right-now/blob/master/browser.js
9
+ const now = performance && performance.now
10
+ ? function now() {
11
+ return performance.now();
12
+ }
13
+ : Date.now ||
14
+ function now() {
15
+ return +new Date();
16
+ };
17
+ export default function createCamera(element, options) {
18
+ element = element || document.body;
19
+ options = options || {};
20
+ var limits = [0.01, Infinity];
21
+ if ('distanceLimits' in options) {
22
+ limits[0] = options.distanceLimits[0];
23
+ limits[1] = options.distanceLimits[1];
24
+ }
25
+ if ('zoomMin' in options) {
26
+ limits[0] = options.zoomMin;
27
+ }
28
+ if ('zoomMax' in options) {
29
+ limits[1] = options.zoomMax;
30
+ }
31
+ var view = createView({
32
+ center: options.center || [0, 0, 0],
33
+ up: options.up || [0, 1, 0],
34
+ eye: options.eye || [0, 0, 10],
35
+ mode: options.mode || 'orbit',
36
+ distanceLimits: limits
37
+ });
38
+ var pmatrix = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
39
+ var distance = 0.0;
40
+ var width = element.clientWidth;
41
+ var height = element.clientHeight;
42
+ var camera = {
43
+ view: view,
44
+ element: element,
45
+ delay: options.delay || 16,
46
+ rotateSpeed: options.rotateSpeed || 1,
47
+ zoomSpeed: options.zoomSpeed || 1,
48
+ translateSpeed: options.translateSpeed || 1,
49
+ flipX: !!options.flipX,
50
+ flipY: !!options.flipY,
51
+ modes: view.modes,
52
+ tick: function () {
53
+ var t = now();
54
+ var delay = this.delay;
55
+ view.idle(t - delay);
56
+ view.flush(t - (100 + delay * 2));
57
+ var ctime = t - 2 * delay;
58
+ view.recalcMatrix(ctime);
59
+ var allEqual = true;
60
+ var matrix = view.computedMatrix;
61
+ for (var i = 0; i < 16; ++i) {
62
+ allEqual = allEqual && (pmatrix[i] === matrix[i]);
63
+ pmatrix[i] = matrix[i];
64
+ }
65
+ var sizeChanged = element.clientWidth === width &&
66
+ element.clientHeight === height;
67
+ width = element.clientWidth;
68
+ height = element.clientHeight;
69
+ if (allEqual) {
70
+ return !sizeChanged;
71
+ }
72
+ distance = Math.exp(view.computedRadius[0]);
73
+ return true;
74
+ },
75
+ lookAt: function (center, eye, up) {
76
+ view.lookAt(view.lastT(), center, eye, up);
77
+ },
78
+ rotate: function (pitch, yaw, roll) {
79
+ view.rotate(view.lastT(), pitch, yaw, roll);
80
+ },
81
+ pan: function (dx, dy, dz) {
82
+ view.pan(view.lastT(), dx, dy, dz);
83
+ },
84
+ translate: function (dx, dy, dz) {
85
+ view.translate(view.lastT(), dx, dy, dz);
86
+ }
87
+ };
88
+ Object.defineProperties(camera, {
89
+ matrix: {
90
+ get: function () {
91
+ return view.computedMatrix;
92
+ },
93
+ set: function (mat) {
94
+ view.setMatrix(view.lastT(), mat);
95
+ return view.computedMatrix;
96
+ },
97
+ enumerable: true
98
+ },
99
+ mode: {
100
+ get: function () {
101
+ return view.getMode();
102
+ },
103
+ set: function (mode) {
104
+ view.setMode(mode);
105
+ return view.getMode();
106
+ },
107
+ enumerable: true
108
+ },
109
+ center: {
110
+ get: function () {
111
+ return view.computedCenter;
112
+ },
113
+ set: function (ncenter) {
114
+ view.lookAt(view.lastT(), ncenter);
115
+ return view.computedCenter;
116
+ },
117
+ enumerable: true
118
+ },
119
+ eye: {
120
+ get: function () {
121
+ return view.computedEye;
122
+ },
123
+ set: function (neye) {
124
+ view.lookAt(view.lastT(), null, neye);
125
+ return view.computedEye;
126
+ },
127
+ enumerable: true
128
+ },
129
+ up: {
130
+ get: function () {
131
+ return view.computedUp;
132
+ },
133
+ set: function (nup) {
134
+ view.lookAt(view.lastT(), null, null, nup);
135
+ return view.computedUp;
136
+ },
137
+ enumerable: true
138
+ },
139
+ distance: {
140
+ get: function () {
141
+ return distance;
142
+ },
143
+ set: function (d) {
144
+ view.setDistance(view.lastT(), d);
145
+ return d;
146
+ },
147
+ enumerable: true
148
+ },
149
+ distanceLimits: {
150
+ get: function () {
151
+ return view.getDistanceLimits(limits);
152
+ },
153
+ set: function (v) {
154
+ view.setDistanceLimits(v);
155
+ return v;
156
+ },
157
+ enumerable: true
158
+ }
159
+ });
160
+ element.addEventListener('contextmenu', function (ev) {
161
+ //ev.preventDefault()
162
+ //return false
163
+ });
164
+ var lastX = 0, lastY = 0, lastMods = { shift: false, control: false, alt: false, meta: false };
165
+ mouseChange(element, handleInteraction);
166
+ //enable simple touch interactions
167
+ element.addEventListener('touchstart', function (ev) {
168
+ var xy = mouseOffset(ev.changedTouches[0], element);
169
+ handleInteraction(0, xy[0], xy[1], lastMods);
170
+ handleInteraction(1, xy[0], xy[1], lastMods);
171
+ ev.preventDefault();
172
+ }, hasPassive ? { passive: false } : false);
173
+ element.addEventListener('touchmove', function (ev) {
174
+ var xy = mouseOffset(ev.changedTouches[0], element);
175
+ handleInteraction(1, xy[0], xy[1], lastMods);
176
+ ev.preventDefault();
177
+ }, hasPassive ? { passive: false } : false);
178
+ element.addEventListener('touchend', function (ev) {
179
+ var xy = mouseOffset(ev.changedTouches[0], element);
180
+ handleInteraction(0, lastX, lastY, lastMods);
181
+ ev.preventDefault();
182
+ }, hasPassive ? { passive: false } : false);
183
+ function handleInteraction(buttons, x, y, mods) {
184
+ var scale = 1.0 / element.clientHeight;
185
+ var dx = scale * (x - lastX);
186
+ var dy = scale * (y - lastY);
187
+ var flipX = camera.flipX ? 1 : -1;
188
+ var flipY = camera.flipY ? 1 : -1;
189
+ var drot = Math.PI * camera.rotateSpeed;
190
+ var t = now();
191
+ if (buttons & 1) {
192
+ if (mods.shift) {
193
+ view.rotate(t, 0, 0, -dx * drot);
194
+ }
195
+ else {
196
+ view.rotate(t, flipX * drot * dx, -flipY * drot * dy, 0);
197
+ }
198
+ }
199
+ else if (buttons & 2) {
200
+ view.pan(t, -camera.translateSpeed * dx * distance, camera.translateSpeed * dy * distance, 0);
201
+ }
202
+ else if (buttons & 4) {
203
+ var kzoom = camera.zoomSpeed * dy / window.innerHeight * (t - view.lastT()) * 50.0;
204
+ view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1));
205
+ }
206
+ lastX = x;
207
+ lastY = y;
208
+ lastMods = mods;
209
+ }
210
+ mouseWheel(element, function (dx, dy, dz) {
211
+ var flipX = camera.flipX ? 1 : -1;
212
+ var flipY = camera.flipY ? 1 : -1;
213
+ var t = now();
214
+ if (Math.abs(dx) > Math.abs(dy)) {
215
+ view.rotate(t, 0, 0, -dx * flipX * Math.PI * camera.rotateSpeed / window.innerWidth);
216
+ }
217
+ else {
218
+ var kzoom = camera.zoomSpeed * flipY * dy / window.innerHeight * (t - view.lastT()) / 100.0;
219
+ view.pan(t, 0, 0, distance * (Math.exp(kzoom) - 1));
220
+ }
221
+ }, true);
222
+ return camera;
223
+ }
@@ -0,0 +1,2 @@
1
+ export function Pluot(props: any): any;
2
+ //# sourceMappingURL=Pluot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Pluot.d.ts","sourceRoot":"","sources":["../src/Pluot.jsx"],"names":[],"mappings":"AAgDA,uCA+fC"}
@@ -0,0 +1,417 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import React, { useLayoutEffect, useEffect, useEffectEvent, useRef, useState, useMemo, useReducer, useCallback } from "react";
3
+ import { mat4, vec4 } from "gl-matrix";
4
+ import lzs from "lz-string";
5
+ import { isEqual, throttle } from "lodash-es";
6
+ import { FetchStore } from 'zarrita';
7
+ import { initialize, getIsWasmReady, render_wasm, pick_wasm, setStore, getStore, create2dCamera, create3dCamera, getBounds, getCameraMatrixFromBounds, checkWebGpuFeatureDetection, } from '@pluot/core';
8
+ // Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
9
+ // The requested module 'lz-string' is a CommonJS module,
10
+ // which may not support all module.exports as named exports."
11
+ const { decompressFromUint8Array } = lzs;
12
+ const DEFAULT_VIEW = new Float32Array([
13
+ 1, 0, 0, 0,
14
+ 0, 1, 0, 0,
15
+ 0, 0, 1 / 200, 0,
16
+ 0, 0, 0, 1,
17
+ ]);
18
+ const DEFAULT_3D_VIEW = new Float32Array([
19
+ 1, 0, 0, 0,
20
+ 0, 1, 0, 0,
21
+ 0, 0, 1, 0,
22
+ 0, 0, -10, 1,
23
+ ]);
24
+ function normalizePickingResult(data) {
25
+ const result = data;
26
+ if (data && Array.isArray(result.layer_results)) {
27
+ result.layer_results = result.layer_results.map(obj => ({
28
+ layer_id: obj.layer_id,
29
+ // This is needed because serde-wasm-bindgen
30
+ // converts Rust HashMap to JS Map.
31
+ info: Object.fromEntries(Array.from(obj.info)),
32
+ }));
33
+ }
34
+ return result;
35
+ }
36
+ export function Pluot(props) {
37
+ const { width: widthProp, height: heightProp, plotId, plotType, store, storeName: storeNameProp, plotParams, viewMode = "2d", marginBottom = 100.0, marginLeft = 100.0, marginTop = 100.0, marginRight = 100.0, aspectRatioMode = "Contain", // "Ignore", "Contain", "Cover"
38
+ aspectRatioAlignmentMode = "Start", // "Center", "Start", "End"
39
+ format = "Raster", // "Raster", "Vector"
40
+ minTimeout = 32, maxTimeout = 32, allowSimultaneousRenders = true, debugMargins = false, } = props;
41
+ const width = Math.floor(widthProp);
42
+ const height = Math.floor(heightProp);
43
+ const isVector = format === "Vector";
44
+ const storeName = useMemo(() => {
45
+ if (storeNameProp) {
46
+ return storeNameProp;
47
+ }
48
+ // If store is a string, assume it is a URL and initialize a FetchStore here.
49
+ if (store) {
50
+ if (typeof store === 'string') {
51
+ return setStore(new FetchStore(store), plotId);
52
+ }
53
+ return setStore(store, plotId);
54
+ }
55
+ throw new Error("Either storeName or store must be provided.");
56
+ }, [storeNameProp, store]);
57
+ const [supportsWebGpu, supportsWebGpuMessage] = useMemo(checkWebGpuFeatureDetection, []);
58
+ const svgRef = useRef(null);
59
+ const canvasRef = useRef(null);
60
+ const cameraRef = useRef(null);
61
+ const tempButtonRef = useRef(null);
62
+ // We may want to update these things without triggering a re-render.
63
+ const isRenderingRef = useRef(false);
64
+ const currentTimeout = useRef(maxTimeout);
65
+ // TODO: do we want to use the backlog approach or not?
66
+ // (Similar to the one used in the Vitessce heatmap)
67
+ // Reference: https://github.com/vitessce/vitessce/blob/71f17fb605768e0428fb15ed87b3ea34bcbb4803/packages/view-types/heatmap/src/Heatmap.js#L368
68
+ //const backlogRef = useRef([]);
69
+ const [backlogIteration, incBacklogIteration] = useReducer(i => i + 1, 0);
70
+ const [isWasmReady, setIsWasmReady] = useState(false);
71
+ const [didFirstRender, setDidFirstRender] = useState(false);
72
+ const [bailedEarly, setBailedEarly] = useState(true);
73
+ const [pickingResult, setPickingResult] = useState(null);
74
+ // TODO: handle a viewMatrix that is provided and set via props,
75
+ // to enable usage as a controlled component
76
+ // (e.g., for linked views with shared cameras).
77
+ const [viewMatrix, setViewMatrix] = useState(
78
+ // Note: We use an initializer function here to avoid
79
+ // sharing the same Float32Array among multiple Pluot
80
+ // component instances that may be rendered on the same page.
81
+ () => new Float32Array(DEFAULT_VIEW));
82
+ useLayoutEffect(() => {
83
+ initialize().then(() => setIsWasmReady(getIsWasmReady()));
84
+ }, []);
85
+ useEffect(() => {
86
+ // Reset view matrix on plot change.
87
+ // Create a new Float32Array to avoid sharing a mutable array
88
+ // among multiple Pluot component instances.
89
+ setViewMatrix(new Float32Array(viewMode === "2d" ? DEFAULT_VIEW : DEFAULT_3D_VIEW));
90
+ //viewMatrixRef.current = new Float32Array(DEFAULT_VIEW);
91
+ }, [plotId, viewMode]);
92
+ // Set up the camera.
93
+ useEffect(() => {
94
+ // Set up the camera.
95
+ const cameraEl = cameraRef.current;
96
+ if (!cameraEl) {
97
+ return () => { };
98
+ }
99
+ let dispose = () => { };
100
+ // Create a 2D camera for handling zoom and pan.
101
+ if (viewMode === "2d") {
102
+ function onCameraEvent(camera, event) {
103
+ camera.tick();
104
+ // Reference: https://github.com/flekschas/regl-scatterplot/blob/17a650c352fad313d1574472b2fdc5f58b9e1eca/src/index.js#L1648
105
+ setViewMatrix(prev => {
106
+ // Since camera events happen even on mousemove events that do not change the matrix,
107
+ // we check for equality here to avoid unnecessary state updates and plot re-renders.
108
+ if (isEqual(prev, camera.view)) {
109
+ return prev;
110
+ }
111
+ return mat4.clone(camera.view);
112
+ });
113
+ currentTimeout.current = minTimeout;
114
+ }
115
+ const camera = create2dCamera(cameraEl, {
116
+ isFixed: false,
117
+ distance: 0.0,
118
+ //target: [0.0, 0.0],
119
+ //viewCenter: [0.5, 0.5], // Should this be used when the coordinate system is (0 to 1) rather than (-1 to 1)?
120
+ viewCenter: [0.0, 0.0],
121
+ defaultMouseDownMoveAction: "pan",
122
+ onKeyDown: (event) => {
123
+ onCameraEvent(camera, event);
124
+ },
125
+ onKeyUp: (event) => {
126
+ onCameraEvent(camera, event);
127
+ },
128
+ onMouseDown: (event) => {
129
+ onCameraEvent(camera, event);
130
+ },
131
+ onMouseUp: (event) => {
132
+ onCameraEvent(camera, event);
133
+ },
134
+ onMouseMove: (event) => {
135
+ onCameraEvent(camera, event);
136
+ },
137
+ onWheel: (event) => {
138
+ onCameraEvent(camera, event);
139
+ },
140
+ aspectRatioMode: aspectRatioMode,
141
+ aspectRatioAlignmentMode: aspectRatioAlignmentMode,
142
+ });
143
+ // Set the initial view matrix.
144
+ // We need to ensure we create a new copy of the array.
145
+ camera.setView(new Float32Array(viewMatrix));
146
+ const tempHandler = e => {
147
+ // camera.setScaleBounds([[xScaleMin, xScaleMax], [yScaleMin, yScaleMax]])
148
+ //camera.lookAt([2.0, 2.0], 2.0);
149
+ //onCameraEvent(camera, null);
150
+ // Only zoom/pan the X axis; keep Y unchanged
151
+ const nextCameraMatrix = getCameraMatrixFromBounds({ yMin: 0.0, yMax: 100.0 }, new Float32Array(viewMatrix), {
152
+ width,
153
+ height,
154
+ aspectRatioMode,
155
+ aspectRatioAlignmentMode,
156
+ margins: {
157
+ marginTop,
158
+ marginBottom,
159
+ marginLeft,
160
+ marginRight
161
+ },
162
+ });
163
+ console.log("done", nextCameraMatrix);
164
+ camera.setView(nextCameraMatrix);
165
+ onCameraEvent(camera, null);
166
+ };
167
+ tempButtonRef.current.addEventListener('click', tempHandler);
168
+ // Set up an onClick handler.
169
+ //
170
+ const clickHandler = (event) => {
171
+ pickFrame(event.offsetX, event.offsetY);
172
+ };
173
+ cameraEl.addEventListener("click", clickHandler);
174
+ dispose = () => {
175
+ camera.dispose();
176
+ cameraEl.removeEventListener("click", clickHandler);
177
+ tempButtonRef.current.removeEventListener('click', tempHandler);
178
+ };
179
+ }
180
+ else if (viewMode === "3d") {
181
+ function onCameraEvent(camera, event) {
182
+ camera.tick();
183
+ // Note: the 3D camera stores the matrix in camera.matrix (not camera.view).
184
+ setViewMatrix(prev => {
185
+ // Since camera events happen even on mousemove events that do not change the matrix,
186
+ // we check for equality here to avoid unnecessary state updates and plot re-renders.
187
+ if (isEqual(prev, camera.matrix)) {
188
+ return prev;
189
+ }
190
+ return mat4.clone(camera.matrix);
191
+ });
192
+ }
193
+ const camera = create3dCamera(cameraEl, {
194
+ mode: "orbit",
195
+ zoomSpeed: -5,
196
+ });
197
+ // TODO:
198
+ // - fork 3d-view-controls and remove usage of "global" - then clean up vite config.
199
+ // - define a camera.dispsose option.
200
+ // Reference: https://github.com/flekschas/dom-2d-camera/blob/cd59ea035a0ea72c2c0535fa3721f8127946576c/src/index.js#L237C3-L315C71
201
+ const keyUpHandler = (event) => {
202
+ // TODO
203
+ };
204
+ const keyDownHandler = (event) => {
205
+ // TODO
206
+ };
207
+ const mouseUpHandler = (event) => {
208
+ // TODO
209
+ };
210
+ const mouseDownHandler = (event) => {
211
+ // TODO
212
+ };
213
+ // TODO: use react state?
214
+ var lastX = 0;
215
+ var lastY = 0;
216
+ // Reference: https://github.com/mikolalysenko/3d-view/blob/8269e02337bba1923173a750aa7f3f0f76c91ba5/example/minimal.js#L67
217
+ const mouseMoveHandler = (event) => {
218
+ /*
219
+ var dx = (event.clientX - lastX) / width;
220
+ var dy = -(event.clientY - lastY) / height;
221
+ if (event.which === 1) {
222
+ if (event.shiftKey) {
223
+ //zoom
224
+ camera.rotate(now(), 0, 0, dx);
225
+ } else {
226
+ //rotate
227
+ camera.rotate(now(), dx, dy);
228
+ }
229
+ } else if (event.which === 3) {
230
+ //pan
231
+ camera.pan(now(), dx, dy);
232
+ }
233
+ lastX = event.clientX;
234
+ lastY = event.clientY;
235
+ */
236
+ onCameraEvent(camera, event);
237
+ };
238
+ const wheelHandler = (event) => {
239
+ //camera.pan(now(), 0, 0, event.deltaY);
240
+ onCameraEvent(camera, event);
241
+ };
242
+ cameraEl.addEventListener("keydown", keyDownHandler);
243
+ cameraEl.addEventListener("keyup", keyUpHandler);
244
+ cameraEl.addEventListener("mousedown", mouseDownHandler);
245
+ cameraEl.addEventListener("mouseup", mouseUpHandler);
246
+ cameraEl.addEventListener("mousemove", mouseMoveHandler);
247
+ cameraEl.addEventListener("wheel", wheelHandler);
248
+ dispose = () => {
249
+ cameraEl.removeEventListener("keydown", keyDownHandler);
250
+ cameraEl.removeEventListener("keyup", keyUpHandler);
251
+ cameraEl.removeEventListener("mousedown", mouseDownHandler);
252
+ cameraEl.removeEventListener("mouseup", mouseUpHandler);
253
+ cameraEl.removeEventListener("mousemove", mouseMoveHandler);
254
+ cameraEl.removeEventListener("wheel", wheelHandler);
255
+ };
256
+ //camera.matrix = new Float32Array(viewMatrix);
257
+ }
258
+ else {
259
+ throw new Error("Unknown mode found.");
260
+ }
261
+ return dispose;
262
+ }, [cameraRef, viewMode, aspectRatioMode, aspectRatioAlignmentMode, width, height, marginLeft, marginRight, marginTop, marginBottom]);
263
+ // The picking callback.
264
+ const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
265
+ const renderParams = {
266
+ width,
267
+ height,
268
+ format: format,
269
+ margin_bottom: marginBottom,
270
+ margin_left: marginLeft,
271
+ margin_top: marginTop,
272
+ margin_right: marginRight,
273
+ device_pixel_ratio: window.devicePixelRatio,
274
+ aspect_ratio_mode: aspectRatioMode,
275
+ aspect_ratio_alignment_mode: aspectRatioAlignmentMode,
276
+ view_mode: viewMode,
277
+ pickable: false,
278
+ // Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
279
+ camera_view: viewMatrix,
280
+ plot_id: plotId,
281
+ plot_type: plotType,
282
+ store_name: storeName,
283
+ plot_params: plotParams,
284
+ // Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
285
+ timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
286
+ wait_for_store_gets: false, // TODO: lift this value up to pass/use it in the window.zarr_ functions as well?
287
+ cache_enabled: true,
288
+ svg_compression_enabled: true,
289
+ svg_include_document: false,
290
+ };
291
+ const layerHeight = height - marginTop - marginBottom;
292
+ setPickingResult(normalizePickingResult(await pick_wasm(renderParams,
293
+ // The coordinates are relative to the "layer" (the camera region), not the full width/height.
294
+ // We also need to flip the Y coordinate so that positive is up.
295
+ screenCoordX + marginLeft, marginBottom + (layerHeight - screenCoordY))));
296
+ });
297
+ // The renderFrame callback.
298
+ // We use useEffectEvent because we want to "see"
299
+ // the latest values of viewMatrix, plotProps, etc.
300
+ const renderFrame = useEffectEvent(async () => {
301
+ isRenderingRef.current = true;
302
+ console.log('wasm.render');
303
+ const renderParams = {
304
+ width,
305
+ height,
306
+ format: format,
307
+ margin_bottom: marginBottom,
308
+ margin_left: marginLeft,
309
+ margin_top: marginTop,
310
+ margin_right: marginRight,
311
+ device_pixel_ratio: window.devicePixelRatio,
312
+ aspect_ratio_mode: aspectRatioMode,
313
+ aspect_ratio_alignment_mode: aspectRatioAlignmentMode,
314
+ view_mode: viewMode,
315
+ pickable: false,
316
+ // Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
317
+ camera_view: viewMatrix,
318
+ plot_id: plotId,
319
+ plot_type: plotType,
320
+ store_name: storeName,
321
+ plot_params: plotParams,
322
+ // Reduce the timeout value to improve responsiveness during data loading (bailed-early renders)?
323
+ timeout: currentTimeout.current, // in ms // Note: will not have any effect when wait_for_store_gets is false.
324
+ wait_for_store_gets: false, // TODO: lift this value up to pass/use it in the window.zarr_ functions as well?
325
+ cache_enabled: true,
326
+ svg_compression_enabled: true,
327
+ svg_include_document: false,
328
+ };
329
+ // Wrap render_wasm in try/catch, to handle Rust panics.
330
+ let arr;
331
+ try {
332
+ arr = await render_wasm(renderParams);
333
+ isRenderingRef.current = false;
334
+ }
335
+ catch (error) {
336
+ console.error("Error during wasm.render_wasm:", error);
337
+ // Cleanup
338
+ isRenderingRef.current = false;
339
+ return;
340
+ }
341
+ if (isVector) {
342
+ // Format: Vector (render to SVG)
343
+ const gContents = decompressFromUint8Array(arr);
344
+ //console.log(gContents)
345
+ if (!svgRef.current) {
346
+ return;
347
+ }
348
+ svgRef.current.innerHTML = gContents;
349
+ // TODO: check for bailed early
350
+ }
351
+ else {
352
+ // Format: Raster (render to canvas)
353
+ const canvas = canvasRef.current;
354
+ if (!canvas) {
355
+ return;
356
+ }
357
+ const ctx = canvas.getContext("2d");
358
+ if (!ctx) {
359
+ return;
360
+ }
361
+ // TODO: is there a more efficient way to do this?
362
+ // E.g., write to a webgl texture? or is this fast enough already?
363
+ const imageData = new ImageData(new Uint8ClampedArray(arr.subarray(0, -1)), width, height);
364
+ ctx.putImageData(imageData, 0, 0);
365
+ const frameBailedEarly = arr.at(-1) === 1;
366
+ if (frameBailedEarly) {
367
+ currentTimeout.current = maxTimeout;
368
+ incBacklogIteration(); // Increment this to force a re-render.
369
+ setBailedEarly(true); // Update this to show the loading indicator.
370
+ }
371
+ else {
372
+ // Successful render.
373
+ setBailedEarly(false); // Update this to hide the loading indicator.
374
+ // Clear the LRU cache for the store (via its store_name) corresponding to the rendered plot.
375
+ const storeUsed = getStore(renderParams.store_name);
376
+ if (storeUsed && storeUsed.clearCache && typeof storeUsed.clearCache === 'function') {
377
+ storeUsed.clearCache();
378
+ }
379
+ }
380
+ }
381
+ setDidFirstRender(true);
382
+ });
383
+ const throttledRender = useMemo(() => throttle(renderFrame, 16, // ~60fps
384
+ // When both leading and trailing are true (the default):
385
+ // - First call -> executes immediately (leading edge)
386
+ // - Calls during the wait window -> ignored, but the most recent one is remembered.
387
+ // - After the wait period expires -> the last remembered call is executed (trailing edge).
388
+ { leading: true, trailing: true }), []);
389
+ useEffect(() => {
390
+ return () => throttledRender.cancel();
391
+ }, [throttledRender]);
392
+ // TODO: use react-query?
393
+ useEffect(() => {
394
+ if (!isWasmReady) {
395
+ return;
396
+ }
397
+ // We want to allow for simultaneous renders, as this makes user interactions feel
398
+ // much smoother. However, we allow for users to opt-out, and we also
399
+ // need to prevent simultaneous renders prior to the first render, as the first
400
+ // render initializes cached values and stuff.
401
+ if (isRenderingRef.current && (!didFirstRender || bailedEarly || !allowSimultaneousRenders)) {
402
+ // Prevent multiple render calls prior to the first successful render.
403
+ return;
404
+ }
405
+ // Render on the next animation frame.
406
+ throttledRender();
407
+ }, [isWasmReady, didFirstRender, viewMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
408
+ width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
409
+ return (_jsxs(_Fragment, { children: [_jsxs("div", { style: { width, height, position: "relative" }, children: [!supportsWebGpu ? (_jsx("p", { children: supportsWebGpuMessage })) : null, _jsx("div", { ref: cameraRef, style: {
410
+ position: "absolute",
411
+ top: marginTop,
412
+ left: marginLeft,
413
+ width: width - marginLeft - marginRight,
414
+ height: height - marginTop - marginBottom,
415
+ border: `${debugMargins ? 1 : 0}px solid red`,
416
+ } }), isVector ? (_jsx("svg", { ref: svgRef, style: { width, height, border: "1px solid black" }, width: width, height: height, viewBox: `0 0 ${width} ${height}`, xmlns: "http://www.w3.org/2000/svg" })) : (_jsx("canvas", { ref: canvasRef, style: { width, height, border: "1px solid black" }, width: width, height: height }))] }), bailedEarly ? (_jsx("p", { children: "Loading..." })) : null, _jsx("button", { ref: tempButtonRef, style: { display: 'none' }, children: "Try lookAt" }), pickingResult ? (_jsx("pre", { children: JSON.stringify(pickingResult, null, 2) })) : null] }));
417
+ }