diff --git a/CHANGELOG.md b/CHANGELOG.md index 23ad6dd..3ddb14e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.11.2 + +- Fix: handle camera fixing better by adding a dedicated prop called `cameraIsFixed`. Previously, the lasso end interaction would unset the camera fixing. ([#94](https://github.com/flekschas/regl-scatterplot/issues/94)) + ## 1.11.1 - Fix: ensure that the drawing order of points cannot be manipulated via `scatterplot.filter()` ([#197](https://github.com/flekschas/regl-scatterplot/issues/197)) diff --git a/README.md b/README.md index 1c58216..8fa38b1 100644 --- a/README.md +++ b/README.md @@ -756,7 +756,8 @@ can be read and written via [`scatterplot.get()`](#scatterplot.get) and [`scatte | cameraTarget | tuple | `[0, 0]` | | `true` | `false` | | cameraDistance | float | `1` | > 0 | `true` | `false` | | cameraRotation | float | `0` | | `true` | `false` | -| cameraView | Float32Array | `[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1`] | | `true` | `false` | +| cameraView | Float32Array | `[1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1]` | | `true` | `false` | +| cameraIsFixed | boolean | `false` | | `true` | `false` | | colorBy | string | `null` | See [data encoding](#property-by) | `true` | `true` | | sizeBy | string | `null` | See [data encoding](#property-by) | `true` | `true` | | opacityBy | string | `null` | See [data encoding](#property-by) | `true` | `true` | diff --git a/src/constants.js b/src/constants.js index 8c22444..5f004f5 100644 --- a/src/constants.js +++ b/src/constants.js @@ -166,6 +166,7 @@ export const Z_NAMES = new Set(['z', 'valueZ', 'valueA', 'value1', 'category']); export const W_NAMES = new Set(['w', 'valueW', 'valueB', 'value2', 'value']); export const DEFAULT_IMAGE_LOAD_TIMEOUT = 15000; export const DEFAULT_SPATIAL_INDEX_USE_WORKER = undefined; +export const DEFAULT_CAMERA_IS_FIXED = false; // Error messages export const ERROR_POINTS_NOT_DRAWN = 'Points have not been drawn'; diff --git a/src/index.js b/src/index.js index 0765f41..05852b0 100644 --- a/src/index.js +++ b/src/index.js @@ -38,6 +38,7 @@ import { DEFAULT_ANNOTATION_LINE_COLOR, DEFAULT_ANNOTATION_LINE_WIDTH, DEFAULT_BACKGROUND_IMAGE, + DEFAULT_CAMERA_IS_FIXED, DEFAULT_COLOR_ACTIVE, DEFAULT_COLOR_BG, DEFAULT_COLOR_BY, @@ -287,6 +288,7 @@ const createScatterplot = ( annotationLineColor = DEFAULT_ANNOTATION_LINE_COLOR, annotationLineWidth = DEFAULT_ANNOTATION_LINE_WIDTH, annotationHVLineLimit = DEFAULT_ANNOTATION_HVLINE_LIMIT, + cameraIsFixed = DEFAULT_CAMERA_IS_FIXED, } = initialProperties; let currentWidth = width === AUTO ? 1 : width; @@ -888,7 +890,7 @@ const createScatterplot = ( }; const lassoEnd = (lassoPoints, lassoPointsFlat, { merge = false } = {}) => { - camera.config({ isFixed: false }); + camera.config({ isFixed: cameraIsFixed }); lassoPointsCurr = [...lassoPoints]; const pointsInLasso = findPointsInLasso(lassoPointsFlat); select(pointsInLasso, { merge }); @@ -2722,7 +2724,7 @@ const createScatterplot = ( 'transitionEnd', () => { resolve(); - camera.config({ isFixed: false }); + camera.config({ isFixed: cameraIsFixed }); }, 1, ); @@ -2921,6 +2923,11 @@ const createScatterplot = ( } }; + const setCameraIsFixed = (isFixed) => { + cameraIsFixed = Boolean(isFixed); + camera.config({ isFixed: cameraIsFixed }); + }; + const setLassoColor = (newLassoColor) => { if (!newLassoColor) { return; @@ -3296,6 +3303,10 @@ const createScatterplot = ( return camera.view; } + if (property === 'cameraIsFixed') { + return cameraIsFixed; + } + if (property === 'canvas') { return canvas; } @@ -3616,6 +3627,10 @@ const createScatterplot = ( setCameraView(properties.cameraView); } + if (properties.cameraIsFixed !== undefined) { + setCameraIsFixed(properties.cameraIsFixed); + } + if (properties.colorBy !== undefined) { setColorBy(properties.colorBy); } @@ -3873,6 +3888,7 @@ const createScatterplot = ( const initCamera = () => { if (!camera) { camera = createDom2dCamera(canvas, { + isFixed: cameraIsFixed, isPanInverted: [false, true], defaultMouseDownMoveAction: mouseMode === MOUSE_MODE_ROTATE ? 'rotate' : 'pan', @@ -4083,7 +4099,7 @@ const createScatterplot = ( }; const cancelFrameListener = renderer.onFrame(() => { - // Update camera: this needs to happen on every + // Update camera: this needs to happen on every frame isViewChanged = camera.tick(); if (!((isPointsDrawn || isAnnotationsDrawn) && (draw || isTransitioning))) { diff --git a/src/types.d.ts b/src/types.d.ts index da49586..81807a0 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -164,6 +164,7 @@ interface BaseOptions { xScale: null | Scale; yScale: null | Scale; pointScaleMode: PointScaleMode; + cameraIsFixed: boolean; } // biome-ignore lint/style/useNamingConvention: KDBush is a library name diff --git a/tests/constructor.test.js b/tests/constructor.test.js index b39d004..6510362 100644 --- a/tests/constructor.test.js +++ b/tests/constructor.test.js @@ -10,6 +10,7 @@ import createScatterplot, { } from '../src'; import { + DEFAULT_CAMERA_IS_FIXED, DEFAULT_COLOR_NORMAL, DEFAULT_COLOR_ACTIVE, DEFAULT_COLOR_HOVER, @@ -73,6 +74,7 @@ test('createScatterplot()', () => { expect(scatterplot.get('opacityInactiveScale')).toBe(DEFAULT_OPACITY_INACTIVE_SCALE); expect(scatterplot.get('width')).toBe(DEFAULT_WIDTH); expect(scatterplot.get('height')).toBe(DEFAULT_HEIGHT); + expect(scatterplot.get('cameraIsFixed')).toBe(DEFAULT_CAMERA_IS_FIXED); scatterplot.destroy(); }); diff --git a/tests/get-set.test.js b/tests/get-set.test.js index 0cf6073..fb27a44 100644 --- a/tests/get-set.test.js +++ b/tests/get-set.test.js @@ -1,4 +1,5 @@ import '@babel/polyfill'; +import { nextAnimationFrame } from '@flekschas/utils'; import { assert, expect, test } from 'vitest'; import { version } from '../package.json'; @@ -451,7 +452,7 @@ test( ); expect(scatterplot.get('pointConnectionOpacityActive')).toBe( - scatterplot.get('pointConnectionOpacityActive'), + DEFAULT_POINT_CONNECTION_OPACITY_ACTIVE, ); scatterplot.set({ @@ -630,3 +631,63 @@ test( scatterplot.destroy(); } ); + +test('set({ cameraIsFixed })', async () => { + const canvas = createCanvas(); + const scatterplot = createScatterplot({ canvas }); + + await scatterplot.draw([ + [-1, 1], + [1, 1], + [0, 0], + [-1, -1], + [1, -1], + ]); + + canvas.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 })); + + await nextAnimationFrame(); + + console.log('1. camera distance', scatterplot.get('camera').distance[0]); + + // We expect the distance to be less than one because we zoomed into the plot + // via wheeling + expect(scatterplot.get('camera').distance[0]).toBeLessThan(1); + + await scatterplot.zoomToOrigin(); + + expect(scatterplot.get('camera').distance[0]).toBe(1); + + scatterplot.set({ cameraIsFixed: true }); + expect(scatterplot.get('cameraIsFixed')).toBe(true); + + canvas.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 })); + + await nextAnimationFrame(); + + // We expect the distance to be one because we fixed the camera + expect(scatterplot.get('camera').distance[0]).toBe(1); + + scatterplot.set({ cameraIsFixed: false }); + expect(scatterplot.get('cameraIsFixed')).toBe(false); + + canvas.dispatchEvent(new WheelEvent('wheel', { deltaY: -100 })); + + await nextAnimationFrame(); + + // We expect the distance to be less than one because we unfixed the camera + expect(scatterplot.get('camera').distance[0]).toBeLessThan(1); + + await scatterplot.zoomToOrigin(); + expect(scatterplot.get('camera').distance[0]).toBe(1); + + scatterplot.set({ cameraIsFixed: true }); + await scatterplot.zoomToPoints([2]); + + // Even though the camera is fixed, programmatic zooming still works. Only + // mouse wheel interactions are prevented + expect(scatterplot.get('cameraIsFixed')).toBe(true); + expect(scatterplot.get('camera').distance[0]).toBeLessThan(1); + + scatterplot.destroy(); +});