@pluot/react 0.1.0 → 0.1.1

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/src/Pluot.jsx CHANGED
@@ -7,9 +7,10 @@ import {
7
7
  initialize, getIsWasmReady,
8
8
  render_wasm, pick_wasm,
9
9
  setStore, getStore,
10
- create2dCamera, create3dCamera,
11
10
  getBounds, getCameraMatrixFromBounds,
12
11
  checkWebGpuFeatureDetection,
12
+ onMouseMove2d, onWheel2d,
13
+ onMouseMove3d, onWheel3d,
13
14
  } from '@pluot/core';
14
15
 
15
16
  // Needed due to "SyntaxError: Named export 'decompressFromUint8Array' not found.
@@ -67,8 +68,38 @@ export function Pluot(props) {
67
68
  maxTimeout = 32,
68
69
  allowSimultaneousRenders = true,
69
70
  debugMargins = false,
71
+ cameraMatrix: controlledCameraMatrix = null,
72
+ setCameraMatrix: setControlledCameraMatrix = null,
70
73
  } = props;
71
74
 
75
+ // If cameraMatrix is not provided, then we manage the camera matrix internally.
76
+ const [uncontrolledCameraMatrix, setUncontrolledCameraMatrix] = useState(
77
+ // Note: We use an initializer function here to avoid
78
+ // sharing the same Float32Array among multiple Pluot
79
+ // component instances that may be rendered on the same page.
80
+ () => Float32Array.from(
81
+ // If the cameraMatrix prop was provided, use that for the initial camera matrix;
82
+ // otherwise use the default matrix.
83
+ controlledCameraMatrix === null
84
+ ? (viewMode === "2d" ? DEFAULT_VIEW : DEFAULT_3D_VIEW)
85
+ : controlledCameraMatrix
86
+ )
87
+ );
88
+
89
+ // Decide which camera matrix and setter to use.
90
+ // If the user provides the cameraMatrix prop but NOT the setCameraMatrix setter,
91
+ // then interpret the prop as the "initial" camera settings, but still treat as uncontrolled.
92
+ const isControlledCamera = typeof setControlledCameraMatrix === "function";
93
+ // Alternatively, if the user provides the setCameraMatrix setter, but NOT
94
+ // the cameraMatrix, interpret this as they want to use the default camera
95
+ // value initially, but they still want a controlled camera matrix.
96
+ const cameraMatrix = isControlledCamera && controlledCameraMatrix !== null
97
+ ? controlledCameraMatrix
98
+ : uncontrolledCameraMatrix;
99
+ const setCameraMatrix = isControlledCamera
100
+ ? setControlledCameraMatrix
101
+ : setUncontrolledCameraMatrix;
102
+
72
103
  const width = Math.floor(widthProp);
73
104
  const height = Math.floor(heightProp);
74
105
 
@@ -92,7 +123,7 @@ export function Pluot(props) {
92
123
 
93
124
  const svgRef = useRef(null);
94
125
  const canvasRef = useRef(null);
95
- const cameraRef = useRef(null);
126
+ const cameraElementRef = useRef(null);
96
127
 
97
128
  const tempButtonRef = useRef(null);
98
129
 
@@ -112,232 +143,64 @@ export function Pluot(props) {
112
143
 
113
144
  const [pickingResult, setPickingResult] = useState(null);
114
145
 
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
146
 
125
147
  useLayoutEffect(() => {
126
148
  initialize().then(() => setIsWasmReady(getIsWasmReady()));
127
149
  }, []);
128
150
 
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
151
 
152
+ const wheelHandler = useEffectEvent((event) => {
153
+ const onWheel = viewMode === "3d" ? onWheel3d : onWheel2d;
154
+ const nextCameraMatrix = onWheel({
155
+ width,
156
+ height,
157
+ aspectRatioMode,
158
+ aspectRatioAlignmentMode,
159
+ margins: {
160
+ marginTop,
161
+ marginBottom,
162
+ marginLeft,
163
+ marginRight,
164
+ },
165
+ }, cameraMatrix, event);
166
+ setCameraMatrix(nextCameraMatrix);
167
+ });
168
+
169
+ const mouseMoveHandler = useEffectEvent((event) => {
170
+ const onMouseMove = viewMode === "3d" ? onMouseMove3d : onMouseMove2d;
171
+ const nextCameraMatrix = onMouseMove({
172
+ width,
173
+ height,
174
+ aspectRatioMode,
175
+ aspectRatioAlignmentMode,
176
+ margins: {
177
+ marginTop,
178
+ marginBottom,
179
+ marginLeft,
180
+ marginRight,
181
+ },
182
+ }, cameraMatrix, event);
183
+ setCameraMatrix(nextCameraMatrix);
184
+ });
137
185
 
138
- // Set up the camera.
186
+ // Set up FUNCTIONAL camera.
139
187
  useEffect(() => {
140
188
  // Set up the camera.
141
- const cameraEl = cameraRef.current;
189
+ const cameraEl = cameraElementRef.current;
190
+
142
191
  if (!cameraEl) {
143
192
  return () => {};
144
193
  }
145
194
 
146
- let dispose = () => {};
147
-
148
195
  // 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",
196
+ cameraEl.addEventListener("mousemove", mouseMoveHandler);
197
+ cameraEl.addEventListener("wheel", wheelHandler);
173
198
 
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]);
199
+ return () => {
200
+ cameraEl.removeEventListener("mousemove", mouseMoveHandler);
201
+ cameraEl.removeEventListener("wheel", wheelHandler);
202
+ };
203
+ }, [viewMode]);
341
204
 
342
205
  // The picking callback.
343
206
  const pickFrame = useEffectEvent(async (screenCoordX, screenCoordY) => {
@@ -355,7 +218,7 @@ export function Pluot(props) {
355
218
  view_mode: viewMode,
356
219
  pickable: false,
357
220
  // Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
358
- camera_view: viewMatrix,
221
+ camera_view: cameraMatrix,
359
222
  plot_id: plotId,
360
223
  plot_type: plotType,
361
224
  store_name: storeName,
@@ -401,7 +264,7 @@ export function Pluot(props) {
401
264
  view_mode: viewMode,
402
265
  pickable: false,
403
266
  // Should see the latest viewMatrix here, since renderFrame is wrapped in useEffectEvent.
404
- camera_view: viewMatrix,
267
+ camera_view: cameraMatrix,
405
268
  plot_id: plotId,
406
269
  plot_type: plotType,
407
270
  store_name: storeName,
@@ -509,7 +372,7 @@ export function Pluot(props) {
509
372
 
510
373
  // Render on the next animation frame.
511
374
  throttledRender();
512
- }, [isWasmReady, didFirstRender, viewMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
375
+ }, [isWasmReady, didFirstRender, cameraMatrix, backlogIteration, plotId, plotType, plotParams, storeName, format,
513
376
  width, height, aspectRatioMode, aspectRatioAlignmentMode, marginLeft, marginRight, marginTop, marginBottom]);
514
377
 
515
378
  return (
@@ -519,7 +382,7 @@ export function Pluot(props) {
519
382
  <p>{supportsWebGpuMessage}</p>
520
383
  ) : null}
521
384
  <div
522
- ref={cameraRef}
385
+ ref={cameraElementRef}
523
386
  style={{
524
387
  position: "absolute",
525
388
  top: marginTop,
package/src/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export * from '@pluot/core'; // Re-export everything from the vanilla JS package.
2
- export { Pluot } from "./Pluot.jsx";
2
+ export { Pluot } from "./Pluot.js";