Canvas: Add "infinite" pan / zoom functionality (#84968)

* attempt at solving issue and resetting bounds to match panel content

* cleanup failed experiments

* add toggle for infinite pan - dont love naming but open to suggestions; clean up scene transform function

* add ability to "infinite" zoom (i.e. render more canvas via zooming out)

* slight refactor

* revert refactor as it broke things
This commit is contained in:
Nathan Marrs
2024-03-22 21:38:45 +01:00
committed by GitHub
parent 810c224039
commit 26473a0074
7 changed files with 76 additions and 5 deletions
@@ -111,6 +111,10 @@ export const defaultCanvasElementOptions: Partial<CanvasElementOptions> = {
};
export interface Options {
/**
* Enable infinite pan
*/
infinitePan: boolean;
/**
* Enable inline editing
*/
@@ -144,6 +148,7 @@ export interface Options {
}
export const defaultOptions: Partial<Options> = {
infinitePan: true,
inlineEditing: true,
panZoom: true,
showAdvancedTypes: true,
@@ -14,6 +14,15 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf
const onZoom = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
const scale = zoomPanPinchRef.state.scale;
scene.scale = scale;
if (scene.shouldInfinitePan) {
const isScaleZoomedOut = scale < 1;
if (isScaleZoomedOut) {
scene.updateSize(scene.width / scale, scene.height / scale);
scene.panel.forceUpdate();
}
}
};
const onZoomStop = (zoomPanPinchRef: ReactZoomPanPinchRef) => {
@@ -46,6 +55,25 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf
}
};
const onPanning = (_: ReactZoomPanPinchRef, event: MouseEvent | TouchEvent) => {
if (scene.shouldInfinitePan && event instanceof MouseEvent) {
// Get deltaX and deltaY from pan event and add it to current canvas dimensions
let deltaX = event.movementX;
let deltaY = event.movementY;
if (deltaX > 0) {
deltaX = 0;
}
if (deltaY > 0) {
deltaY = 0;
}
// TODO: Consider bounding to the scene elements instead of allowing "infinite" panning
// TODO: Consider making scene grow in all directions vs just down to the right / bottom
scene.updateSize(scene.width - deltaX, scene.height - deltaY);
scene.panel.forceUpdate();
}
};
const onSceneContainerMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
// If pan and zoom is disabled or context menu is visible, don't pan
if ((!scene.shouldPanZoom || scene.contextMenuVisible) && (e.button === 1 || (e.button === 2 && e.ctrlKey))) {
@@ -60,6 +88,9 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf
}
};
// Set panel content overflow to hidden to prevent canvas content from overflowing
scene.div?.parentElement?.parentElement?.parentElement?.parentElement?.setAttribute('style', `overflow: hidden`);
return (
<TransformWrapper
doubleClick={{ mode: 'reset' }}
@@ -67,9 +98,11 @@ export const SceneTransformWrapper = ({ scene, children: sceneDiv }: SceneTransf
onZoom={onZoom}
onZoomStop={onZoomStop}
onTransformed={onTransformed}
limitToBounds={true}
disabled={!config.featureToggles.canvasPanelPanZoom || !scene.shouldPanZoom}
panning={{ allowLeftClickPan: false }}
limitToBounds={!scene.shouldInfinitePan}
minScale={scene.shouldInfinitePan ? 0.1 : undefined}
onPanning={onPanning}
>
<TransformComponent>
{/* The <div> element has child elements that allow for mouse events, so we need to disable the linter rule */}
+11 -2
View File
@@ -75,6 +75,7 @@ export class Scene {
isEditingEnabled?: boolean;
shouldShowAdvancedTypes?: boolean;
shouldPanZoom?: boolean;
shouldInfinitePan?: boolean;
skipNextSelectionBroadcast = false;
ignoreDataUpdate = false;
panel: CanvasPanel;
@@ -112,10 +113,11 @@ export class Scene {
enableEditing: boolean,
showAdvancedTypes: boolean,
panZoom: boolean,
infinitePan: boolean,
public onSave: (cfg: CanvasFrameOptions) => void,
panel: CanvasPanel
) {
this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom);
this.root = this.load(cfg, enableEditing, showAdvancedTypes, panZoom, infinitePan);
this.subscription = this.editModeEnabled.subscribe((open) => {
if (!this.moveable || !this.isEditingEnabled) {
@@ -148,7 +150,13 @@ export class Scene {
return !this.byName.has(v);
};
load(cfg: CanvasFrameOptions, enableEditing: boolean, showAdvancedTypes: boolean, panZoom: boolean) {
load(
cfg: CanvasFrameOptions,
enableEditing: boolean,
showAdvancedTypes: boolean,
panZoom: boolean,
infinitePan: boolean
) {
this.root = new RootElement(
cfg ?? {
type: 'frame',
@@ -161,6 +169,7 @@ export class Scene {
this.isEditingEnabled = enableEditing;
this.shouldShowAdvancedTypes = showAdvancedTypes;
this.shouldPanZoom = panZoom;
this.shouldInfinitePan = infinitePan;
setTimeout(() => {
if (this.div) {
@@ -68,6 +68,7 @@ export class CanvasPanel extends Component<Props, State> {
this.props.options.inlineEditing,
this.props.options.showAdvancedTypes,
this.props.options.panZoom,
this.props.options.infinitePan,
this.onUpdateScene,
this
);
@@ -229,7 +230,14 @@ export class CanvasPanel extends Component<Props, State> {
const shouldShowAdvancedTypesSwitched =
this.props.options.showAdvancedTypes !== nextProps.options.showAdvancedTypes;
const panZoomSwitched = this.props.options.panZoom !== nextProps.options.panZoom;
if (this.needsReload || inlineEditingSwitched || shouldShowAdvancedTypesSwitched || panZoomSwitched) {
const infinitePanSwitched = this.props.options.infinitePan !== nextProps.options.infinitePan;
if (
this.needsReload ||
inlineEditingSwitched ||
shouldShowAdvancedTypesSwitched ||
panZoomSwitched ||
infinitePanSwitched
) {
if (inlineEditingSwitched) {
// Replace scene div to prevent selecto instance leaks
this.scene.revId++;
@@ -240,7 +248,8 @@ export class CanvasPanel extends Component<Props, State> {
nextProps.options.root,
nextProps.options.inlineEditing,
nextProps.options.showAdvancedTypes,
nextProps.options.panZoom
nextProps.options.panZoom,
nextProps.options.infinitePan
);
this.scene.updateSize(nextProps.width, nextProps.height);
this.scene.updateData(nextProps.data);
@@ -39,6 +39,14 @@ export const addStandardCanvasEditorOptions = (builder: PanelOptionsEditorBuilde
editor: PanZoomHelp,
showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom,
});
builder.addBooleanSwitch({
path: 'infinitePan',
name: 'Infinite panning',
description:
'Enable infinite panning - useful for expansive canvases. Warning: this an experimental feature and currently only works well with elements that are top / left constrained',
defaultValue: false,
showIf: (opts) => config.featureToggles.canvasPanelPanZoom && opts.panZoom,
});
};
export const plugin = new PanelPlugin<Options>(CanvasPanel)
@@ -94,6 +94,8 @@ composableKinds: PanelCfg: {
showAdvancedTypes: bool | *true
// Enable pan and zoom
panZoom: bool | *true
// Enable infinite pan
infinitePan: bool | *true
// The root element of canvas (frame), where all canvas elements are nested
// TODO: Figure out how to define a default value for this
root: {
@@ -109,6 +109,10 @@ export const defaultCanvasElementOptions: Partial<CanvasElementOptions> = {
};
export interface Options {
/**
* Enable infinite pan
*/
infinitePan: boolean;
/**
* Enable inline editing
*/
@@ -142,6 +146,7 @@ export interface Options {
}
export const defaultOptions: Partial<Options> = {
infinitePan: true,
inlineEditing: true,
panZoom: true,
showAdvancedTypes: true,