Working prototype with internal zooming/panning handlers.

This commit is contained in:
Filip Barl
2017-08-14 18:47:57 +02:00
parent 7abb05cb77
commit f11a9d3614

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { connect } from 'react-redux';
import { debounce, pick } from 'lodash';
import { clamp, debounce, pick } from 'lodash';
import { fromJS } from 'immutable';
import { drag } from 'd3-drag';
import { event as d3Event, select } from 'd3-selection';
import { zoom, zoomIdentity } from 'd3-zoom';
import Logo from '../components/logo';
import ZoomControl from '../components/zoom-control';
import { cacheZoomState } from '../actions/app-actions';
import { zoomFactor } from '../utils/zoom-utils';
import { transformToString } from '../utils/transform-utils';
import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
import {
@@ -19,6 +20,15 @@ import {
import { ZOOM_CACHE_DEBOUNCE_INTERVAL } from '../constants/timer';
const transformF = ({ x, y }, t) => ({
x: t.translateX + (t.scaleX * x),
y: t.translateY + (t.scaleY * y),
});
const inverseTransform = ({ x, y }, t) => ({
x: (x - t.translateX) / t.scaleX,
y: (y - t.translateY) / t.scaleY,
});
class ZoomableCanvas extends React.Component {
constructor(props, context) {
@@ -40,27 +50,26 @@ class ZoomableCanvas extends React.Component {
this.debouncedCacheZoom = debounce(this.cacheZoom.bind(this), ZOOM_CACHE_DEBOUNCE_INTERVAL);
this.handleZoomControlAction = this.handleZoomControlAction.bind(this);
this.canChangeZoom = this.canChangeZoom.bind(this);
this.zoomed = this.zoomed.bind(this);
this.handleZoom = this.handleZoom.bind(this);
this.handlePan = this.handlePan.bind(this);
}
componentDidMount() {
this.zoomRestored = false;
this.zoom = zoom().on('zoom', this.zoomed);
this.svg = select('svg#canvas');
this.drag = drag().on('drag', this.handlePan);
this.svg.call(this.drag);
this.setZoomTriggers(!this.props.disabled);
this.updateZoomLimits(this.props);
this.restoreZoomState(this.props);
}
componentWillUnmount() {
this.setZoomTriggers(false);
this.debouncedCacheZoom.cancel();
}
componentWillReceiveProps(nextProps) {
const layoutChanged = nextProps.layoutId !== this.props.layoutId;
const disabledChanged = nextProps.disabled !== this.props.disabled;
// If the layout has changed (either active topology or its options) or
// relayouting has been requested, stop pending zoom caching event and
@@ -70,11 +79,6 @@ class ZoomableCanvas extends React.Component {
this.zoomRestored = false;
}
// If the zooming has been enabled/disabled, update its triggers.
if (disabledChanged) {
this.setZoomTriggers(!nextProps.disabled);
}
this.updateZoomLimits(nextProps);
if (!this.zoomRestored) {
this.restoreZoomState(nextProps);
@@ -83,14 +87,12 @@ class ZoomableCanvas extends React.Component {
handleZoomControlAction(scale) {
// Update the canvas scale (not touching the translation).
this.svg.call(this.zoom.scaleTo, scale);
// Update the scale state and propagate to the global cache.
this.setState(this.cachableState({
scaleX: scale,
scaleY: scale,
}));
this.debouncedCacheZoom();
const { top, bottom, left, right } = this.svg.node().getBoundingClientRect();
const centerOfCanvas = {
x: (left + right) / 2,
y: (top + bottom) / 2,
};
this.zoomAtPosition(centerOfCanvas, scale / this.state.scaleX);
}
render() {
@@ -103,7 +105,9 @@ class ZoomableCanvas extends React.Component {
return (
<div className="zoomable-canvas">
<svg id="canvas" width="100%" height="100%" onClick={this.props.onClick}>
<svg
id="canvas" width="100%" height="100%"
onClick={this.props.onClick} onWheel={this.handleZoom}>
<Logo transform="translate(24,24) scale(0.25)" />
<g className="zoom-content" transform={transform}>
{forwardTransform ? children(this.state) : children}
@@ -119,16 +123,6 @@ class ZoomableCanvas extends React.Component {
);
}
setZoomTriggers(zoomingEnabled) {
if (zoomingEnabled) {
// use d3-zoom defaults but exclude double clicks
this.svg.call(this.zoom)
.on('dblclick.zoom', null);
} else {
this.svg.on('.zoom', null);
}
}
// Decides which part of the zoom state is cachable depending
// on the horizontal/vertical degrees of freedom.
cachableState(state = this.state) {
@@ -144,28 +138,7 @@ class ZoomableCanvas extends React.Component {
}
updateZoomLimits(props) {
const zoomLimits = props.layoutZoomLimits.toJS();
this.zoom = this.zoom.scaleExtent([zoomLimits.minScale, zoomLimits.maxScale]);
if (props.bounded) {
this.zoom = this.zoom
// Translation limits are only set if explicitly demanded (currently we are using them
// in the resource view, but not in the graph view, although I think the idea would be
// to use them everywhere).
.translateExtent([
[zoomLimits.minTranslateX, zoomLimits.minTranslateY],
[zoomLimits.maxTranslateX, zoomLimits.maxTranslateY],
])
// This is to ensure that the translation limits are properly
// centered, so that the canvas margins are respected.
.extent([
[props.canvasMargins.left, props.canvasMargins.top],
[props.canvasMargins.left + props.width, props.canvasMargins.top + props.height]
]);
}
this.setState(zoomLimits);
this.setState(props.layoutZoomLimits.toJS());
}
// Restore the zooming settings
@@ -173,11 +146,6 @@ class ZoomableCanvas extends React.Component {
if (!props.layoutZoomState.isEmpty()) {
const zoomState = props.layoutZoomState.toJS();
// After the limits have been set, update the zoom.
this.svg.call(this.zoom.transform, zoomIdentity
.translate(zoomState.translateX, zoomState.translateY)
.scale(zoomState.scaleX, zoomState.scaleY));
// Update the state variables.
this.setState(zoomState);
this.zoomRestored = true;
@@ -190,19 +158,83 @@ class ZoomableCanvas extends React.Component {
return !disabled && canvasHasContent;
}
zoomed() {
if (this.canChangeZoom()) {
const updatedState = this.cachableState({
scaleX: d3Event.transform.k,
scaleY: d3Event.transform.k,
translateX: d3Event.transform.x,
translateY: d3Event.transform.y,
});
handlePan() {
let state = { ...this.state };
state = this.clampedTranslation({ ...state,
translateX: this.state.translateX + d3Event.dx,
translateY: this.state.translateY + d3Event.dy,
});
this.updateState(state);
}
this.setState(updatedState);
this.debouncedCacheZoom();
handleZoom(ev) {
if (this.canChangeZoom()) {
const { top, left } = this.svg.node().getBoundingClientRect();
const mousePosition = {
x: ev.clientX - left,
y: ev.clientY - top,
};
this.zoomAtPosition(mousePosition, 1 / zoomFactor(ev));
}
}
clampedTranslation(state) {
if (this.props.bounded) {
const { width, height, canvasMargins } = this.props;
const { maxTranslateX, minTranslateX, maxTranslateY, minTranslateY }
= this.props.layoutZoomLimits.toJS();
const minPoint = transformF({ x: minTranslateX, y: minTranslateY }, state);
const maxPoint = transformF({ x: maxTranslateX, y: maxTranslateY }, state);
const viewportMinPoint = { x: canvasMargins.left, y: canvasMargins.top };
const viewportMaxPoint = { x: canvasMargins.left + width, y: canvasMargins.top + height };
if (true) {
if (maxPoint.x < viewportMaxPoint.x) {
state.translateX += viewportMaxPoint.x - maxPoint.x;
} else if (minPoint.x > viewportMinPoint.x) {
state.translateX -= minPoint.x - viewportMinPoint.x;
}
if (maxPoint.y < viewportMaxPoint.y) {
state.translateY += viewportMaxPoint.y - maxPoint.y;
} else if (minPoint.y > viewportMinPoint.y) {
state.translateY -= minPoint.y - viewportMinPoint.y;
}
} else {
if (minPoint.x > viewportMaxPoint.x) {
state.translateX -= minPoint.x - viewportMaxPoint.x;
} else if (maxPoint.x < viewportMinPoint.x) {
state.translateX += viewportMinPoint.x - maxPoint.x;
}
if (minPoint.y > viewportMaxPoint.y) {
state.translateY -= minPoint.y - viewportMaxPoint.y;
} else if (maxPoint.y < viewportMinPoint.y) {
state.translateY += viewportMinPoint.y - maxPoint.y;
}
}
}
return state;
}
zoomAtPosition(position, factor) {
const { minScale, maxScale } = this.state;
const scaleX = clamp(this.state.scaleX * factor, minScale, maxScale);
const scaleY = clamp(this.state.scaleY * factor, minScale, maxScale);
let state = { ...this.state, scaleX, scaleY };
const inversePosition = inverseTransform(position, this.state);
state = this.clampedTranslation({ ...state,
translateX: position.x - (inversePosition.x * scaleX),
translateY: position.y - (inversePosition.y * scaleY),
});
this.updateState(state);
}
updateState(state) {
this.setState(this.cachableState(state));
this.debouncedCacheZoom();
}
}