fix: zoom pan helper

This commit is contained in:
Braks
2021-10-19 20:09:43 +02:00
parent a895853b64
commit c3fb3cd10a
38 changed files with 214 additions and 277 deletions

View File

@@ -60,13 +60,13 @@ const toggleClassnames = () => {
<Flow
class="revue-flow-basic-example"
:elements="elements"
:on-elements-remove="onElementsRemove"
:on-connect="onConnect"
:on-node-drag-stop="onNodeDragStop"
:on-node-click="onElementClick"
:default-zoom="1.5"
:min-zoom="0.2"
:max-zoom="4"
@elements-remove="onElementsRemove"
@connect="onConnect"
@node-drag-stop="onNodeDragStop"
@node-click="onElementClick"
@elementClick="onElementClick"
@load="onLoad"
>

View File

@@ -15,7 +15,7 @@ const onElementsRemove = (elementsToRemove: Elements) =>
const onConnect = (params: Connection | Edge) => (elements.value = addEdge(params, elements.value as Elements))
</script>
<template>
<Flow :elements="elements" :custom-connection-line="ConnectionLine" @elementsRemove="onElementsRemove" @connect="onConnect">
<Flow :elements="elements" :custom-connection-line="ConnectionLine" @elements-remove="onElementsRemove" @connect="onConnect">
<Background :variant="BackgroundVariant.Lines" />
</Flow>
</template>

View File

@@ -114,17 +114,17 @@ const onConnect = (params: Connection | Edge) =>
<template>
<Flow
:elements="elements"
:on-element-click="onElementClick"
:on-elements-remove="onElementsRemove"
:on-connect="onConnect"
:on-node-drag-stop="onNodeDragStop"
:style="`background: ${bgColor}`"
:on-load="onLoad"
:node-types="nodeTypes"
:connection-line-style="connectionLineStyle"
:snap-to-grid="true"
:snap-grid="snapGrid"
:default-zoom="1.5"
@element-click="onElementClick"
@elements-remove="onElementsRemove"
@connect="onConnect"
@node-drag-stop="onNodeDragStop"
@load="onLoad"
>
<MiniMap :node-stroke-color="nodeStroke" :node-color="nodeColor" />
<Controls />

View File

@@ -1,15 +1,5 @@
<script lang="ts" setup>
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
import {
getEdgeCenter,
getBezierPath,
getMarkerEnd,
ArrowHeadType,
EdgeProps,
ElementId,
Position,
RevueFlowStore,
} from '~/index'
import { getEdgeCenter, getBezierPath, getMarkerEnd, ArrowHeadType, EdgeProps, ElementId, Position, Hooks, Store } from '~/index'
interface CustomEdgeProps<T = any> extends EdgeProps<T> {
id: ElementId
@@ -25,8 +15,8 @@ interface CustomEdgeProps<T = any> extends EdgeProps<T> {
}
const props = defineProps<CustomEdgeProps>()
const store = inject<RevueFlowStore>('store')!
const hooks = inject<RevueFlowHooks>('hooks')!
const store = inject(Store)!
const hooks = inject(Hooks)!
const onEdgeClick = (evt: Event, id: string) => {
const edge = store.edges.find((edge) => edge.id === id)
if (edge) {

View File

@@ -1,20 +0,0 @@
<script lang="ts" setup>
import { ref } from 'vue'
import Flowy from '~/container/Flow/Flow.vue'
import { Elements } from '~/types'
const elements = ref<Elements>([
{ id: '1', type: 'input', data: { label: 'Node 1' }, position: { x: 250, y: 5 } },
{ id: '2', data: { label: 'Node 2' }, position: { x: 100, y: 100 } },
{ id: '3', data: { label: 'Node 3' }, position: { x: 400, y: 100 } },
{ id: '4', data: { label: 'Node 4' }, position: { x: 400, y: 200 } },
{ id: 'e1-2', source: '1', target: '2', animated: true },
{ id: 'e1-3', source: '1', target: '3' },
] as Elements)
</script>
<template>
<Flowy :elements="elements">
<div>Foobar!</div>
</Flowy>
</template>

View File

@@ -45,10 +45,6 @@ export const routes: RouterOptions['routes'] = [
path: '/interaction',
component: () => import('./Interaction/InteractionExample.vue'),
},
{
path: '/super-flow',
component: () => import('./Superflow/Superflow.vue'),
},
]
export const router = createRouter({

View File

@@ -1,6 +1,7 @@
<script lang="ts" setup>
import { HTMLAttributes } from 'vue'
import { BackgroundVariant, RevueFlowStore } from '~/types'
import { BackgroundVariant } from '~/types'
import { Store } from '~/context'
export interface BackgroundProps extends HTMLAttributes {
variant?: BackgroundVariant
@@ -20,14 +21,23 @@ const defaultColors: Record<BackgroundVariant, string> = {
[BackgroundVariant.Lines]: '#eee',
}
const store = inject<RevueFlowStore>('store')!
const store = inject(Store)!
// when there are multiple flows on a page we need to make sure that every background gets its own pattern.
const bgClasses = ['revue-flow__background']
const scaledGap = computed(() => props.gap && props.gap * store.transform[2])
const xOffset = computed(() => scaledGap.value && store.transform[0] % scaledGap.value)
const yOffset = computed(() => scaledGap.value && store.transform[1] % scaledGap.value)
const size = computed(() => props.size || 0.4 * store.transform[2])
const background = computed(() => {
const scaledGap = props.gap && props.gap * store.transform[2]
const xOffset = scaledGap && store.transform[0] % scaledGap
const yOffset = scaledGap && store.transform[1] % scaledGap
const size = props.size || 0.4 * store.transform[2]
return {
scaledGap,
xOffset,
yOffset,
size,
}
})
const isLines = props.variant === BackgroundVariant.Lines
const bgColor = props.color ? props.color : defaultColors[props.variant || BackgroundVariant.Dots]
@@ -41,16 +51,23 @@ const patternId = `pattern-${Math.floor(Math.random() * 100000)}`
height: '100%',
}"
>
<pattern :id="patternId" :x="xOffset" :y="yOffset" :width="scaledGap" :height="scaledGap" patternUnits="userSpaceOnUse">
<pattern
:id="patternId"
:x="background.xOffset"
:y="background.yOffset"
:width="background.scaledGap"
:height="background.scaledGap"
patternUnits="userSpaceOnUse"
>
<template v-if="isLines">
<path
:stroke="bgColor"
:stroke-width="props.size"
:d="`M${scaledGap / 2} 0 V${scaledGap} M0 ${scaledGap / 2} H${scaledGap}`"
:d="`M${background.scaledGap / 2} 0 V${background.scaledGap} M0 ${background.scaledGap / 2} H${background.scaledGap}`"
/>
</template>
<template v-else>
<circle :cx="size" :cy="size" :r="size" :fill="bgColor" />
<circle :cx="background.size" :cy="background.size" :r="background.size" :fill="bgColor" />
</template>
</pattern>
<rect x="0" y="0" width="100%" height="100%" :fill="`url(#${patternId})`" />

View File

@@ -6,8 +6,9 @@ import MinusIcon from '@/assets/icons/minus.svg'
import Fitview from '@/assets/icons/fitview.svg'
import Lock from '@/assets/icons/lock.svg'
import Unlock from '@/assets/icons/unlock.svg'
import { FitViewParams, RevueFlowStore, ZoomPanHelperFunctions } from '~/types'
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
import { FitViewParams } from '~/types'
import useZoomPanHelper from '~/composables/useZoomPanHelper'
import { Store } from '~/context'
export interface ControlProps extends HTMLAttributes {
showZoom?: boolean
@@ -26,25 +27,24 @@ const props = withDefaults(defineProps<ControlProps>(), {
showInteractive: true,
})
const store = inject<RevueFlowStore>('store')!
// const { onReady } = useZoomPanHelper()
const store = inject(Store)!
const { zoomIn, zoomOut, fitView } = useZoomPanHelper()
const isInteractive = store.nodesDraggable && store.nodesConnectable && store.elementsSelectable
const mapClasses = ['revue-flow__controls']
/*
* const onZoomInHandler = () => {
zoomHelper.value?.zoomIn?.()
const onZoomInHandler = () => {
zoomIn?.()
props.onZoomIn?.()
}
const onZoomOutHandler = () => {
zoomHelper.value?.zoomOut?.()
zoomOut?.()
props.onZoomOut?.()
}
const onFitViewHandler = () => {
zoomHelper.value?.fitView?.(props.fitViewParams)
fitView?.(props.fitViewParams)
props.onFitView?.()
}
@@ -52,7 +52,6 @@ const onInteractiveChangeHandler = () => {
store.setInteractive?.(!isInteractive)
props.onInteractiveChange?.(!isInteractive)
}
*/
</script>
<template>
<div :class="mapClasses">

View File

@@ -2,7 +2,8 @@
import { HTMLAttributes } from 'vue'
import MiniMapNode from './MiniMapNode.vue'
import { getBoundsofRects, getRectOfNodes } from '~/utils/graph'
import { Node, RevueFlowStore } from '~/types'
import { Node } from '~/types'
import { Store } from '~/context'
type StringFunc = (node: Node) => string
@@ -30,7 +31,7 @@ declare const window: any
const defaultWidth = 200
const defaultHeight = 150
const store = inject<RevueFlowStore>('store')!
const store = inject(Store)!
const elementWidth = attrs.style?.width ?? defaultWidth
const elementHeight = attrs.style?.height ?? defaultHeight

View File

@@ -1,5 +1,5 @@
<script lang="ts" setup>
import { computed, CSSProperties } from 'vue'
import { CSSProperties } from 'vue'
interface MiniMapNodeProps {
x?: number
@@ -8,11 +8,13 @@ interface MiniMapNodeProps {
height?: number
borderRadius?: number
color?: string
shapeRendering?: string
shapeRendering?: 'auto' | 'optimizeSpeed' | 'crispEdges' | 'geometricPrecision' | 'inherit'
strokeColor?: string
strokeWidth?: number
}
const props = defineProps<MiniMapNodeProps>()
const props = withDefaults(defineProps<MiniMapNodeProps>(), {
shapeRendering: 'geometricPrecision',
})
const attrs = useAttrs()
const styles = (attrs.style ?? {}) as CSSProperties

View File

@@ -2,7 +2,7 @@
import { CSSProperties } from 'vue'
import { ConnectionLineType, CustomConnectionLine, HandleElement, Node, Position } from '~/types'
import { getBezierPath, getSmoothStepPath } from '~/components/Edges/utils'
import { Hooks, Store } from '~/context/symbols'
import { Hooks, Store } from '~/context'
interface ConnectionLineProps {
sourceNode: Node

View File

@@ -4,7 +4,7 @@ import { getEdgePositions, getHandle, getSourceTargetNodes, isEdgeVisible } from
import { isEdge } from '~/utils/graph'
import { ConnectionMode, Dimensions, Edge, EdgeType, Elements, Position, Transform } from '~/types'
import { onMouseDown } from '~/components/Handle/utils'
import { Hooks, Store } from '~/context/symbols'
import { Hooks, Store } from '~/context'
interface EdgeProps {
type: EdgeType

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { ElementId, Position } from '~/types'
import { onMouseDown, ValidConnectionFunc } from '~/components/Handle/utils'
import { Hooks, Store } from '~/context/symbols'
import { Hooks, Store } from '~/context'
interface HandleProps {
id?: string

View File

@@ -1,6 +1,5 @@
import { getHostForElement } from '~/utils'
import { ElementId, ConnectionMode, Connection, HandleType, RevueFlowStore } from '~/types'
import { RevueFlowHooks } from '~/hooks/RevueFlowHooks'
import { ElementId, ConnectionMode, Connection, HandleType, FlowStore, FlowHooks } from '~/types'
export type ValidConnectionFunc = (connection: Connection) => boolean
@@ -71,13 +70,13 @@ function resetRecentHandle(hoveredHandle: Element): void {
export function onMouseDown(
event: MouseEvent,
store: RevueFlowStore,
store: FlowStore,
hooks: {
connectStart: RevueFlowHooks['connectStart']
connectStop: RevueFlowHooks['connectStop']
connectEnd: RevueFlowHooks['connectEnd']
connect: RevueFlowHooks['connect']
edgeUpdateEnd: RevueFlowHooks['edgeUpdateEnd']
connectStart: FlowHooks['connectStart']
connectStop: FlowHooks['connectStop']
connectEnd: FlowHooks['connectEnd']
connect: FlowHooks['connect']
edgeUpdateEnd: FlowHooks['edgeUpdateEnd']
},
handleId: ElementId,
nodeId: ElementId,

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import { DraggableEventListener } from '@braks/revue-draggable'
import { Node, NodeDimensionUpdate, NodeType } from '~/types'
import { Hooks, Store } from '~/context/symbols'
import { Hooks, Store } from '~/context'
interface NodeProps {
node: Node

View File

@@ -2,7 +2,7 @@
import { DraggableEventListener } from '@braks/revue-draggable'
import { Node } from '~/types'
import { isNode } from '~/utils/graph'
import { Hooks, Store } from '~/context/symbols'
import { Hooks, Store } from '~/context'
interface NodesSelectionProps {
onSelectionDragStart?: (event: MouseEvent, nodes: Node[]) => void

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup>
import SelectionRect from './SelectionRect.vue'
import { getMousePosition } from '~/components/UserSelection/utils'
import { Store } from '~/context/symbols'
import { Store } from '~/context'
const store = inject(Store)!
const el = templateRef('user-selection', null)

7
src/composables/index.ts Normal file
View File

@@ -0,0 +1,7 @@
export { default as useGlobalKeyHandler } from './useGlobalKeyHandler'
export { default as useHooks } from './useHooks'
export { default as useKeyPress } from './useKeyPress'
export { default as useResizeHandler } from './useResizeHandler'
export { default as useUpdateNodeInternals } from './useUpdateNodeInternals'
export { default as useZoom } from './useZoom'
export { default as useZoomPanHelper } from './useZoomPanHelper'

View File

@@ -1,7 +1,7 @@
import useKeyPress from './useKeyPress'
import { isNode, getConnectedEdges } from '~/utils/graph'
import { Elements, KeyCode, ElementId, FlowElement } from '~/types'
import { Store } from '~/context/symbols'
import { Store } from '~/context'
interface HookParams {
deleteKeyCode: KeyCode

View File

@@ -1,7 +1,7 @@
// global hooks
import { Connection, Edge, Elements, FlowHooks, FlowTransform, Node, OnConnectStartParams, OnLoadParams } from '~/types'
export const useHooks = (): FlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => FlowHooks } => {
export default (): FlowHooks & { bind: (emit: (event: string, ...args: any[]) => void) => FlowHooks } => {
const eventHooks: FlowHooks = {
elementClick: createEventHook<{ event: MouseEvent; element: Node | Edge }>(),
elementsRemove: createEventHook<Elements>(),

View File

@@ -2,20 +2,10 @@ import { D3ZoomEvent, zoom, zoomIdentity, ZoomTransform } from 'd3-zoom'
import { pointer, select } from 'd3-selection'
import { Ref } from 'vue'
import { get } from '@vueuse/core'
import {
D3Selection,
D3Zoom,
FlowTransform,
InitD3ZoomPayload,
KeyCode,
PanOnScrollMode,
Transform,
TranslateExtent,
} from '~/types'
import { D3Selection, D3Zoom, FlowTransform, KeyCode, PanOnScrollMode, Transform } from '~/types'
import { clamp } from '~/utils'
import useKeyPress from '~/hooks/useKeyPress'
import { Hooks } from '~/context/symbols'
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
import useKeyPress from '~/composables/useKeyPress'
import { Hooks, Store } from '~/context'
const viewChanged = (prevTransform: FlowTransform, eventTransform: ZoomTransform): boolean =>
prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k
@@ -30,11 +20,8 @@ interface UseZoomOptions {
selectionKeyCode?: KeyCode
zoomActivationKeyCode?: KeyCode
paneMoveable?: boolean
minZoom?: number
maxZoom?: number
defaultZoom?: number
defaultPosition?: [number, number]
translateExtent?: TranslateExtent
zoomOnScroll?: boolean
zoomOnPinch?: boolean
panOnScroll?: boolean
@@ -49,22 +36,12 @@ interface UseZoom {
d3Selection: Ref<D3Selection>
}
export default function (
el: Ref<HTMLDivElement>,
options: UseZoomOptions,
init: (initD3ZoomPayload: InitD3ZoomPayload) => void = () => {},
): UseZoom {
export default function (el: Ref<HTMLDivElement>, options: UseZoomOptions): UseZoom {
const {
selectionKeyCode = 'Shift',
zoomActivationKeyCode = 'Meta',
minZoom = 0.5,
maxZoom = 2,
defaultZoom = 1,
defaultPosition = [0, 0],
translateExtent = [
[Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY],
[Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY],
],
zoomOnScroll = true,
zoomOnPinch = true,
zoomOnDoubleClick = true,
@@ -74,13 +51,17 @@ export default function (
paneMoveable = true,
} = options
const hooks = inject(Hooks)!
const store = inject(Store)!
const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 })
const clampedX = clamp(defaultPosition[0], translateExtent[0][0], translateExtent[1][0])
const clampedY = clamp(defaultPosition[1], translateExtent[0][1], translateExtent[1][1])
const clampedZoom = clamp(defaultZoom, minZoom, maxZoom)
const clampedX = clamp(defaultPosition[0], store.translateExtent[0][0], store.translateExtent[1][0])
const clampedY = clamp(defaultPosition[1], store.translateExtent[0][1], store.translateExtent[1][1])
const clampedZoom = clamp(defaultZoom, store.minZoom, store.maxZoom)
const transform = ref<Transform>([clampedX, clampedY, clampedZoom])
const d3Zoom = ref(zoom<HTMLDivElement, any>().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent))
store.transform = transform.value
const d3Zoom = ref(
zoom<HTMLDivElement, any>().scaleExtent([store.minZoom, store.maxZoom]).translateExtent(store.translateExtent),
)
const d3Selection = ref()
until(el)
@@ -93,8 +74,7 @@ export default function (
const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom)
d3z.transform(d3s, updatedTransform)
init({ d3Zoom: d3z, d3ZoomHandler, d3Selection: d3s })
store.initD3Zoom({ d3Zoom: d3z, d3Selection: d3s, d3ZoomHandler })
const applyZoomHandlers = () => {
d3z.on('start', (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (viewChanged(prevTransform.value, event.transform)) {

View File

@@ -0,0 +1,62 @@
import { zoomIdentity } from 'd3-zoom'
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
import { FitViewParams, FlowTransform, Rect, UseZoomPanHelper, XYPosition } from '~/types'
import { Store } from '~/context'
const DEFAULT_PADDING = 0.1
export default function (): UseZoomPanHelper {
const store = inject(Store)!
return {
zoomIn: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1.2),
zoomOut: () => store.d3Selection && store.d3Zoom?.scaleBy(store.d3Selection, 1 / 1.2),
zoomTo: (zoomLevel: number) => store.d3Selection && store.d3Zoom?.scaleTo(store.d3Selection, zoomLevel),
transform: (transform: FlowTransform) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, nextTransform)
},
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
if (!store.nodes.length) {
return
}
const bounds = getRectOfNodes(options.includeHiddenNodes ? store.nodes : store.nodes.filter((node) => !node.isHidden))
const [x, y, zoom] = getTransformForBounds(
bounds,
store.dimensions.width,
store.dimensions.height,
options.minZoom || store.minZoom,
options.maxZoom || store.maxZoom,
options.padding || DEFAULT_PADDING,
)
const transform = zoomIdentity.translate(x, y).scale(zoom)
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, transform)
},
setCenter: (x: number, y: number, zoom?: number) => {
const nextZoom = typeof zoom !== 'undefined' ? zoom : store.maxZoom
const centerX = store.dimensions.width / 2 - x * nextZoom
const centerY = store.dimensions.height / 2 - y * nextZoom
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, transform)
},
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
const [x, y, zoom] = getTransformForBounds(
bounds,
store.dimensions.width,
store.dimensions.height,
store.minZoom,
store.maxZoom,
padding,
)
const transform = zoomIdentity.translate(x, y).scale(zoom)
store.d3Selection && store.d3Zoom?.transform(store.d3Selection, transform)
},
project: (position: XYPosition) => {
return pointToRendererPoint(position, store.transform, store.snapToGrid, store.snapGrid)
},
}
}

View File

@@ -5,7 +5,7 @@ import MarkerDefinitions from './MarkerDefinitions.vue'
import Edge from '~/components/Edges/Edge.vue'
import ConnectionLine from '~/components/ConnectionLine/ConnectionLine.vue'
import { ConnectionLineType, ConnectionMode, CustomConnectionLine, Dimensions, EdgeType, Transform } from '~/types'
import { Store } from '~/context/symbols'
import { Store } from '~/context'
interface EdgeRendererProps {
edgeTypes: Record<string, EdgeType>

View File

@@ -24,8 +24,8 @@ import { DefaultNode, InputNode, OutputNode } from '~/components/Nodes'
import { BezierEdge, SmoothStepEdge, StepEdge, StraightEdge } from '~/components/Edges'
import { createEdgeTypes } from '~/container/EdgeRenderer/utils'
import { createNodeTypes } from '~/container/NodeRenderer/utils'
import { useHooks } from '~/hooks/RevueFlowHooks'
import { Hooks, Store } from '~/context/symbols'
import { useHooks } from '~/composables'
import { Hooks, Store } from '~/context'
export interface FlowProps {
elements: Elements

View File

@@ -2,7 +2,7 @@
import Node from '~/components/Nodes/Node.vue'
import { NodeType, Node as TNode, Transform, Dimensions } from '~/types'
import { getNodesInside } from '~/utils/graph'
import { Store } from '~/context/symbols'
import { Store } from '~/context'
interface NodeRendererProps {
nodeTypes: Record<string, NodeType>

View File

@@ -1,10 +1,10 @@
<script lang="ts" setup>
import useKeyPress from '~/hooks/useKeyPress'
import useKeyPress from '~/composables/useKeyPress'
import { KeyCode } from '~/types'
import useGlobalKeyHandler from '~/hooks/useGlobalKeyHandler'
import useGlobalKeyHandler from '~/composables/useGlobalKeyHandler'
import NodesSelection from '~/components/NodesSelection/NodesSelection.vue'
import UserSelection from '~/components/UserSelection/UserSelection.vue'
import { Hooks, Store } from '~/context/symbols'
import { Hooks, Store } from '~/context'
interface SelectionPaneProps {
selectionKeyCode?: KeyCode

View File

@@ -1,9 +1,8 @@
<script lang="ts" setup>
import { KeyCode, PanOnScrollMode } from '~/types'
import useZoom from '~/composables/useZoom'
import useResizeHandler from '~/hooks/useResizeHandler'
import useZoomPanHelper from '~/hooks/useZoomPanHelper'
import { Store } from '~/context/symbols'
import { useZoom, useResizeHandler, useZoomPanHelper } from '~/composables'
import { Hooks, Store } from '~/context'
import { onLoadGetElements, onLoadProject, onLoadToObject } from '~/utils/graph'
interface ZoomPaneProps {
selectionKeyCode?: KeyCode
@@ -33,25 +32,25 @@ const props = withDefaults(defineProps<ZoomPaneProps>(), {
paneMoveable: true,
})
const store = inject(Store)!
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
const { transform, d3Selection, d3Zoom } = useZoom(zoomPaneEl, props, (initD3ZoomPayload) => store.initD3Zoom(initD3ZoomPayload))
const hooks = inject(Hooks)!
const zoomPaneEl = templateRef<HTMLDivElement>('zoom-pane', null)
const { transform } = useZoom(zoomPaneEl, props)
watch(transform, (val) => (store.transform = val), { flush: 'post' })
const dimensions = useResizeHandler(zoomPaneEl)
watch(dimensions, (val) => (store.dimensions = val), { flush: 'post' })
const getZoomPanHelper = reactify(useZoomPanHelper)
const zoomPanHelper = getZoomPanHelper(
d3Zoom,
d3Selection,
store.nodes,
transform,
dimensions,
store.minZoom,
store.maxZoom,
store.snapToGrid,
store.snapGrid,
)
const { zoomIn, zoomOut, zoomTo, transform: setTransform, fitView } = useZoomPanHelper()
hooks.load.trigger({
fitView: (params = { padding: 0.1 }) => fitView(params),
zoomIn,
zoomOut,
zoomTo,
setTransform,
project: onLoadProject(store),
getElements: onLoadGetElements(store),
toObject: onLoadToObject(store),
})
</script>
<template>
<div ref="zoom-pane" class="revue-flow__renderer revue-flow__zoompane">

View File

@@ -3,3 +3,4 @@ import { FlowHooks, FlowStore } from '~/types'
export const Store: InjectionKey<FlowStore> = Symbol('store')
export const Hooks: InjectionKey<FlowHooks> = Symbol('hooks')
export const NodeIdContextKey: InjectionKey<string> = Symbol('NodeIdContext')

View File

@@ -1,90 +0,0 @@
import { zoomIdentity } from 'd3-zoom'
import { getRectOfNodes, pointToRendererPoint, getTransformForBounds } from '~/utils/graph'
import {
FitViewParams,
FlowTransform,
ZoomPanHelperFunctions,
Rect,
XYPosition,
Node,
Transform,
SnapGrid,
Dimensions,
D3Zoom,
D3Selection,
} from '~/types'
const DEFAULT_PADDING = 0.1
type UseZoomPanHelper = (
d3Zoom: D3Zoom,
d3Selection: D3Selection,
nodes: Node[],
transform: Transform,
dimensions: Dimensions,
minZoom: number,
maxZoom: number,
snapToGrid: boolean,
snapGrid: SnapGrid,
) => ZoomPanHelperFunctions
export default (function (
d3Zoom,
d3Selection,
nodes = [],
transform = [0, 0, 0],
dimensions = {
width: 0,
height: 0,
},
minZoom = 0.5,
maxZoom = 2,
snapToGrid = false,
snapGrid = [15, 15],
): ZoomPanHelperFunctions {
return {
zoomIn: () => d3Zoom?.scaleBy(d3Selection, 1.2),
zoomOut: () => d3Zoom?.scaleBy(d3Selection, 1 / 1.2),
zoomTo: (zoomLevel: number) => d3Zoom.scaleTo(d3Selection, zoomLevel),
transform: (transform: FlowTransform) => {
const nextTransform = zoomIdentity.translate(transform.x, transform.y).scale(transform.zoom)
d3Zoom.transform(d3Selection, nextTransform)
},
fitView: (options: FitViewParams = { padding: DEFAULT_PADDING, includeHiddenNodes: false }) => {
if (!nodes.length) {
return
}
const bounds = getRectOfNodes(options.includeHiddenNodes ? nodes : nodes.filter((node) => !node.isHidden))
const [x, y, zoom] = getTransformForBounds(
bounds,
dimensions.width,
dimensions.height,
options.minZoom || minZoom,
options.maxZoom || maxZoom,
options.padding || DEFAULT_PADDING,
)
const transform = zoomIdentity.translate(x, y).scale(zoom)
d3Zoom?.transform(d3Selection, transform)
},
setCenter: (x: number, y: number, zoom?: number) => {
const nextZoom = typeof zoom !== 'undefined' ? zoom : maxZoom
const centerX = dimensions.width / 2 - x * nextZoom
const centerY = dimensions.height / 2 - y * nextZoom
const transform = zoomIdentity.translate(centerX, centerY).scale(nextZoom)
d3Zoom.transform(d3Selection, transform)
},
fitBounds: (bounds: Rect, padding = DEFAULT_PADDING) => {
const [x, y, zoom] = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, padding)
const transform = zoomIdentity.translate(x, y).scale(zoom)
d3Zoom.transform(d3Selection, transform)
},
project: (position: XYPosition) => {
return pointToRendererPoint(position, transform, snapToGrid, snapGrid)
},
}
} as UseZoomPanHelper)

View File

@@ -15,8 +15,9 @@ export {
getTransformForBounds,
getRectOfNodes,
} from './utils/graph'
export { default as useZoomPanHelper } from './hooks/useZoomPanHelper'
export { default as useUpdateNodeInternals } from './hooks/useUpdateNodeInternals'
export { default as useZoomPanHelper } from './composables/useZoomPanHelper'
export { default as useUpdateNodeInternals } from './composables/useUpdateNodeInternals'
export * from './additional-components'
export * from './types'
export * from './context'

View File

@@ -1,6 +1,6 @@
import { RevueFlowState, ConnectionMode } from '../types'
import { ConnectionMode, FlowState } from '~/types'
export const initialState: RevueFlowState = {
export const initialState: FlowState = {
dimensions: {
width: 0,
height: 0,

View File

@@ -13,7 +13,7 @@ export enum PanOnScrollMode {
Horizontal = 'horizontal',
}
export interface ZoomPanHelperFunctions {
export interface UseZoomPanHelper {
zoomIn: () => void
zoomOut: () => void
zoomTo: (zoomLevel: number) => void

View File

@@ -10,9 +10,32 @@ import {
Connection,
FlowExportObject,
NodeExtent,
RevueFlowStore,
} from '../types'
import { clampPosition, clamp } from './index'
Dimensions,
FlowStore,
} from '~/types'
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
const target = e.target as HTMLElement
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
}
export const getDimensions = (node: HTMLElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight,
})
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1]),
})
export const getHostForElement = (element: HTMLElement): Document => {
const doc = element.getRootNode() as Document
if ('getElementFromPoint' in doc) return doc
else return window.document
}
export const isEdge = (element: Node | Connection | Edge): element is Edge =>
'id' in element && 'source' in element && 'target' in element
@@ -136,7 +159,7 @@ export const pointToRendererPoint = (
}
export const onLoadProject =
(currentStore: RevueFlowStore) =>
(currentStore: FlowStore) =>
(position: XYPosition): XYPosition =>
pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid)
@@ -256,10 +279,10 @@ const parseElements = (nodes: Node[], edges: Edge[]): Elements => [
...edges.map((e) => ({ ...e })),
]
export const onLoadGetElements = (currentStore: RevueFlowStore) => (): Elements =>
export const onLoadGetElements = (currentStore: FlowStore) => (): Elements =>
parseElements(currentStore.nodes || [], currentStore.edges || [])
export const onLoadToObject = (currentStore: RevueFlowStore) => (): FlowExportObject => ({
export const onLoadToObject = (currentStore: FlowStore) => (): FlowExportObject => ({
elements: parseElements(currentStore.nodes || [], currentStore.edges || []),
position: [currentStore.transform[0], currentStore.transform[1]],
zoom: currentStore.transform[2],

View File

@@ -1,24 +1 @@
import { Dimensions, XYPosition, NodeExtent } from '../types'
export const isInputDOMNode = (e: KeyboardEvent | MouseEvent): boolean => {
const target = e.target as HTMLElement
return ['INPUT', 'SELECT', 'TEXTAREA', 'BUTTON'].includes(target.nodeName) || target.hasAttribute('contentEditable')
}
export const getDimensions = (node: HTMLElement): Dimensions => ({
width: node.offsetWidth,
height: node.offsetHeight,
})
export const clamp = (val: number, min = 0, max = 1): number => Math.min(Math.max(val, min), max)
export const clampPosition = (position: XYPosition, extent: NodeExtent): XYPosition => ({
x: clamp(position.x, extent[0][0], extent[1][0]),
y: clamp(position.y, extent[0][1], extent[1][1]),
})
export const getHostForElement = (element: HTMLElement): Document => {
const doc = element.getRootNode() as Document
if ('getElementFromPoint' in doc) return doc
else return window.document
}
export * from './graph'

View File

@@ -1,7 +0,0 @@
import { InjectionKey } from 'vue'
import { RevueFlowStore } from '../types'
import { RevueFlowHooks } from '../hooks/RevueFlowHooks'
export const StoreKey: InjectionKey<RevueFlowStore> = Symbol('store')
export const HooksKey: InjectionKey<RevueFlowHooks> = Symbol('hooks')
export const NodeIdContextKey: InjectionKey<string> = Symbol('NodeIdContext')