-
Notifications
You must be signed in to change notification settings - Fork 19
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
2a1b615
commit 1b60821
Showing
7 changed files
with
218 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,181 @@ | ||
import * as React from 'react'; | ||
import * as d3 from 'd3'; | ||
import { observer } from 'mobx-react'; | ||
import { action } from 'mobx'; | ||
import ElementContext from '../utils/ElementContext'; | ||
import useCallbackRef from '../utils/useCallbackRef'; | ||
import { Graph, isGraph } from '../types'; | ||
import { PanZoomRef } from './usePanZoom'; | ||
import Point from '../geom/Point'; | ||
// import Rect from '../geom/Rect'; | ||
|
||
export type DragZoomRef = (node: SVGGElement | null) => void; | ||
|
||
// Used to send events prevented by d3.zoom to the document allowing modals, dropdowns, etc, to close | ||
const propagateDragZoomMouseEvent = (e: Event): void => { | ||
document.dispatchEvent(new MouseEvent(e.type, e)); | ||
}; | ||
|
||
export const useDragZoom = (): WithDragZoomProps => { | ||
const element = React.useContext(ElementContext); | ||
const [draggingState, setDraggingState] = React.useState<Omit<WithDragZoomProps, 'dragZoomRef'>>({}); | ||
|
||
if (!isGraph(element)) { | ||
throw new Error('useDragZoom must be used within the scope of a Graph'); | ||
} | ||
const elementRef = React.useRef<Graph>(element); | ||
elementRef.current = element; | ||
|
||
const zoomToState = (dragStart: Point, dragEnd: Point) => { | ||
const currentScale = elementRef.current.getScale(); | ||
const graphPosition = elementRef.current.getPosition(); | ||
|
||
const start = dragStart.clone(); // .scale(elementRef.current.getScale()); | ||
const end = dragEnd.clone(); // .scale(currentScale); | ||
const x = -1 * graphPosition.x + Math.min(start.x, end.x); // graphPosition.x * currentScale + Math.min(start.x, end.x) / currentScale; | ||
const y = -1 * graphPosition.y + Math.min(start.y, end.y); // graphPosition.y * currentScale + Math.min(start.y, end.y) / currentScale; | ||
const width = Math.abs(end.x - start.x) / currentScale; | ||
const height = Math.abs(end.y - start.y) / currentScale; | ||
|
||
if (width < 10 || height < 0) { | ||
return; | ||
} | ||
|
||
// eslint-disable-next-line no-console | ||
console.log(`currentPosition: ${graphPosition.x},${graphPosition.y}`, currentScale); | ||
// eslint-disable-next-line no-console | ||
console.log(`Dimensions: ${elementRef.current.getDimensions().width},${elementRef.current.getDimensions().height}`); | ||
// eslint-disable-next-line no-console | ||
|
||
// eslint-disable-next-line no-console | ||
console.log(`Drag: ${dragStart.x},${dragStart.y} => ${dragEnd.x},${dragEnd.y}`); | ||
// eslint-disable-next-line no-console | ||
console.log(`Translate to: ${x},${y} => ${width}x${height}`); | ||
|
||
const { width: fullWidth, height: fullHeight } = elementRef.current.getDimensions(); | ||
// const midX = x + width / 2; | ||
// const midY = y + height / 2; | ||
|
||
// compute the scale | ||
const xScale = fullWidth / width; | ||
const yScale = fullHeight / height; | ||
const scale = Math.min(xScale, yScale); | ||
|
||
// const tx = fullWidth / 2 - midX * scale; | ||
// const ty = fullHeight / 2 - midY * scale; | ||
|
||
// eslint-disable-next-line no-console | ||
console.log(`Zoom to: ${x},${y} ${scale}`); | ||
// TODO should scale and bound be kept in a single geom Transform object instead of separately? | ||
elementRef.current.setScale(scale); | ||
elementRef.current.setPosition(new Point(x, y)); | ||
|
||
// const { width: fullWidth, height: fullHeight } = elementRef.current.getDimensions(); | ||
// // const midX = x + width / 2; | ||
// // const midY = y + height / 2; | ||
// | ||
// const xScale = fullWidth / width; | ||
// const yScale = fullHeight / height; | ||
// const zoomScale = Math.min(xScale, yScale); | ||
// // const zoomScale = 1 / Math.max(width / Math.max(1, fullWidth), height / Math.max(1, fullHeight)); | ||
// | ||
// // translate to center | ||
// // const tx = fullWidth / 2 - midX; | ||
// // const ty = fullHeight / 2 - midY; | ||
// | ||
// // eslint-disable-next-line no-console | ||
// console.log(`Zoom Scale: `, zoomScale); | ||
// // eslint-disable-next-line no-console | ||
// console.log(`Zoom Position: ${x},${y}`); | ||
// | ||
// elementRef.current.setScale(zoomScale); | ||
// elementRef.current.setPosition(new Point(x, y)); | ||
}; | ||
|
||
const dragZoomRef = useCallbackRef<PanZoomRef>((node: SVGGElement | null) => { | ||
if (node) { | ||
// TODO fix any type | ||
const $svg = d3.select(node.ownerSVGElement) as any; | ||
if (node && node.ownerSVGElement) { | ||
node.ownerSVGElement.addEventListener('mousedown', propagateDragZoomMouseEvent); | ||
node.ownerSVGElement.addEventListener('click', propagateDragZoomMouseEvent); | ||
} | ||
const drag = d3 | ||
.drag() | ||
.on( | ||
'start', | ||
action((event: d3.D3DragEvent<Element, any, any>) => { | ||
// eslint-disable-next-line no-console | ||
console.log(event); | ||
const { offsetX, offsetY } = | ||
event.sourceEvent instanceof MouseEvent ? event.sourceEvent : { offsetX: 0, offsetY: 0 }; | ||
const { width: maxX, height: maxY } = elementRef.current.getDimensions(); | ||
// eslint-disable-next-line no-console | ||
console.log(`Graph Position: ${elementRef.current.getPosition().x},${elementRef.current.getPosition().y}`); | ||
// eslint-disable-next-line no-console | ||
console.log(`Offset: ${offsetX},${offsetY}`); | ||
const startPoint = new Point(Math.min(Math.max(offsetX, 0), maxX), Math.min(Math.max(offsetY, 0), maxY)); | ||
setDraggingState({ | ||
isZoomDragging: true, | ||
zoomDragStart: startPoint, | ||
zoomDragEnd: startPoint | ||
}); | ||
}) | ||
) | ||
.on( | ||
'drag', | ||
action((event: d3.D3DragEvent<Element, any, any>) => { | ||
const { offsetX, offsetY } = | ||
event.sourceEvent instanceof MouseEvent ? event.sourceEvent : { offsetX: 0, offsetY: 0 }; | ||
const { width: maxX, height: maxY } = elementRef.current.getDimensions(); | ||
// eslint-disable-next-line no-console | ||
// console.log(`Offset: ${offsetX},${offsetY}`); | ||
setDraggingState((prev) => ({ | ||
...prev, | ||
zoomDragEnd: new Point(Math.min(Math.max(offsetX, 0), maxX), Math.min(Math.max(offsetY, 0), maxY)) | ||
})); | ||
}) | ||
) | ||
.on( | ||
'end', | ||
action(() => { | ||
setDraggingState((prev) => { | ||
zoomToState(prev.zoomDragStart, prev.zoomDragEnd); | ||
return { isZoomDragging: false }; | ||
}); | ||
}) | ||
) | ||
.filter((event: React.MouseEvent) => event.ctrlKey && !event.button); | ||
drag($svg); | ||
} | ||
|
||
return () => { | ||
if (node) { | ||
// remove all drag listeners | ||
d3.select(node.ownerSVGElement).on('.drag', null); | ||
if (node.ownerSVGElement) { | ||
node.ownerSVGElement.removeEventListener('mousedown', propagateDragZoomMouseEvent); | ||
node.ownerSVGElement.removeEventListener('click', propagateDragZoomMouseEvent); | ||
} | ||
} | ||
}; | ||
}); | ||
return { dragZoomRef, ...draggingState }; | ||
}; | ||
export interface WithDragZoomProps { | ||
dragZoomRef?: DragZoomRef; | ||
isZoomDragging?: boolean; | ||
zoomDragStart?: Point; | ||
zoomDragEnd?: Point; | ||
} | ||
|
||
export const withDragZoom = | ||
() => | ||
<P extends WithDragZoomProps>(WrappedComponent: React.ComponentType<P>) => { | ||
const Component: React.FunctionComponent<Omit<P, keyof WithDragZoomProps>> = (props) => { | ||
const dragZoomProps = useDragZoom(); | ||
return <WrappedComponent {...(props as any)} {...dragZoomProps} />; | ||
}; | ||
Component.displayName = `withPanZoom(${WrappedComponent.displayName || WrappedComponent.name})`; | ||
return observer(Component); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters