diff --git a/examples/vite/router.ts b/examples/vite/router.ts index a1f4180b3..c28dd5020 100644 --- a/examples/vite/router.ts +++ b/examples/vite/router.ts @@ -138,6 +138,14 @@ export const routes: RouterOptions['routes'] = [ path: '/confirm-delete', component: () => import('./src/ConfirmDelete/ConfirmDeleteExample.vue'), }, + { + path: '/nesting-flows', + component: () => import('./src/NestingFlows/NestingFlows.vue'), + }, + { + path: '/recursive-nesting-flows', + component: () => import('./src/RecursiveNestingFlows/RecursiveNestingFlows.vue'), + }, ] export const router = createRouter({ diff --git a/examples/vite/src/NestingFlows/NestedFlow.vue b/examples/vite/src/NestingFlows/NestedFlow.vue new file mode 100644 index 000000000..049d770f3 --- /dev/null +++ b/examples/vite/src/NestingFlows/NestedFlow.vue @@ -0,0 +1,55 @@ + + + + + diff --git a/examples/vite/src/NestingFlows/NestedFlowNode.vue b/examples/vite/src/NestingFlows/NestedFlowNode.vue new file mode 100644 index 000000000..7f0182f7b --- /dev/null +++ b/examples/vite/src/NestingFlows/NestedFlowNode.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/examples/vite/src/NestingFlows/NestingFlows.ts b/examples/vite/src/NestingFlows/NestingFlows.ts new file mode 100644 index 000000000..048d63c31 --- /dev/null +++ b/examples/vite/src/NestingFlows/NestingFlows.ts @@ -0,0 +1,4 @@ +let VueFlowInstanceId = 0 +export function newVueFlowInstanceID(): number { + return VueFlowInstanceId++ +} diff --git a/examples/vite/src/NestingFlows/NestingFlows.vue b/examples/vite/src/NestingFlows/NestingFlows.vue new file mode 100644 index 000000000..7e3bde9bb --- /dev/null +++ b/examples/vite/src/NestingFlows/NestingFlows.vue @@ -0,0 +1,45 @@ + + + diff --git a/examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue b/examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue new file mode 100644 index 000000000..3b27fbb07 --- /dev/null +++ b/examples/vite/src/RecursiveNestingFlows/NestedFlowNode.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/examples/vite/src/RecursiveNestingFlows/NestingFlows.ts b/examples/vite/src/RecursiveNestingFlows/NestingFlows.ts new file mode 100644 index 000000000..048d63c31 --- /dev/null +++ b/examples/vite/src/RecursiveNestingFlows/NestingFlows.ts @@ -0,0 +1,4 @@ +let VueFlowInstanceId = 0 +export function newVueFlowInstanceID(): number { + return VueFlowInstanceId++ +} diff --git a/examples/vite/src/RecursiveNestingFlows/RecursiveNestingFlows.vue b/examples/vite/src/RecursiveNestingFlows/RecursiveNestingFlows.vue new file mode 100644 index 000000000..f0a455438 --- /dev/null +++ b/examples/vite/src/RecursiveNestingFlows/RecursiveNestingFlows.vue @@ -0,0 +1,56 @@ + + + diff --git a/packages/core/src/components/ConnectionLine/index.ts b/packages/core/src/components/ConnectionLine/index.ts index 67227e593..b32576c93 100644 --- a/packages/core/src/components/ConnectionLine/index.ts +++ b/packages/core/src/components/ConnectionLine/index.ts @@ -21,6 +21,7 @@ const ConnectionLine = defineComponent({ connectionLineOptions, connectionStatus, viewport, + ancestorZoom, findNode, } = useVueFlow() @@ -32,8 +33,8 @@ const ConnectionLine = defineComponent({ const toXY = computed(() => { return { - x: (connectionPosition.value.x - viewport.value.x) / viewport.value.zoom, - y: (connectionPosition.value.y - viewport.value.y) / viewport.value.zoom, + x: (connectionPosition.value.x - viewport.value.x) / viewport.value.zoom / ancestorZoom.value, + y: (connectionPosition.value.y - viewport.value.y) / viewport.value.zoom / ancestorZoom.value, } }) diff --git a/packages/core/src/components/Nodes/NodeWrapper.ts b/packages/core/src/components/Nodes/NodeWrapper.ts index afcd8d8ed..9fa86ad1b 100644 --- a/packages/core/src/components/Nodes/NodeWrapper.ts +++ b/packages/core/src/components/Nodes/NodeWrapper.ts @@ -266,7 +266,7 @@ const NodeWrapper = defineComponent({ 'vue-flow__node', `vue-flow__node-${nodeCmp.value === false ? 'default' : node.type || 'default'}`, { - [noPanClassName.value]: isDraggable.value, + // [noPanClassName.value]: isDraggable.value, dragging: dragging?.value, draggable: isDraggable.value, selected: node.selected, diff --git a/packages/core/src/composables/useGetPointerPosition.ts b/packages/core/src/composables/useGetPointerPosition.ts index 4104befc9..6dbeca0ba 100644 --- a/packages/core/src/composables/useGetPointerPosition.ts +++ b/packages/core/src/composables/useGetPointerPosition.ts @@ -9,7 +9,7 @@ import type { UseDragEvent } from './useDrag' * @internal */ export function useGetPointerPosition() { - const { viewport, snapGrid, snapToGrid, vueFlowRef } = useVueFlow() + const { viewport, snapGrid, snapToGrid, vueFlowRef, ancestorZoom } = useVueFlow() // returns the pointer position projected to the VF coordinate system return (event: UseDragEvent | MouseTouchEvent) => { @@ -17,7 +17,7 @@ export function useGetPointerPosition() { const evt = isUseDragEvent(event) ? event.sourceEvent : event const { x, y } = getEventPosition(evt, containerBounds as DOMRect) - const pointerPos = pointToRendererPoint({ x, y }, viewport.value) + const pointerPos = pointToRendererPoint({ x, y }, viewport.value, undefined, undefined, ancestorZoom.value) const { x: xSnapped, y: ySnapped } = snapToGrid.value ? snapPosition(pointerPos, snapGrid.value) : pointerPos // we need the snapped position to be able to skip unnecessary drag events diff --git a/packages/core/src/composables/useHandle.ts b/packages/core/src/composables/useHandle.ts index 158b4f395..988188f7a 100644 --- a/packages/core/src/composables/useHandle.ts +++ b/packages/core/src/composables/useHandle.ts @@ -68,6 +68,7 @@ export function useHandle({ endConnection, emits, viewport, + ancestorZoom, edges, nodes, isValidConnection: isValidConnectionProp, @@ -116,6 +117,8 @@ export function useHandle({ let prevActiveHandle: Element let connectionPosition = getEventPosition(event, containerBounds) + connectionPosition.x += viewport.value.x * (1 - ancestorZoom.value) + connectionPosition.y += viewport.value.y * (1 - ancestorZoom.value) let autoPanStarted = false // when the user is moving the mouse close to the edge of the canvas while connecting we move the canvas @@ -177,9 +180,11 @@ export function useHandle({ function onPointerMove(event: MouseTouchEvent) { connectionPosition = getEventPosition(event, containerBounds) + connectionPosition.x += viewport.value.x * (1 - ancestorZoom.value) + connectionPosition.y += viewport.value.y * (1 - ancestorZoom.value) closestHandle = getClosestHandle( - pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1]), + pointToRendererPoint(connectionPosition, viewport.value, false, [1, 1], ancestorZoom.value), connectionRadius.value, nodeLookup.value, fromHandle, @@ -219,7 +224,7 @@ export function useHandle({ isValid, to: result.toHandle && isValid - ? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, viewport.value) + ? rendererPointToPoint({ x: result.toHandle.x, y: result.toHandle.y }, viewport.value, ancestorZoom.value) : connectionPosition, toHandle: result.toHandle, toPosition: isValid && result.toHandle ? result.toHandle.position : oppositePosition[fromHandle.position], @@ -250,6 +255,7 @@ export function useHandle({ y: closestHandle.y, }, viewport.value, + ancestorZoom.value, ) : connectionPosition, result.toHandle, diff --git a/packages/core/src/composables/useViewportHelper.ts b/packages/core/src/composables/useViewportHelper.ts index 417e95d4b..3b6fabe24 100644 --- a/packages/core/src/composables/useViewportHelper.ts +++ b/packages/core/src/composables/useViewportHelper.ts @@ -194,7 +194,7 @@ export function useViewportHelper(state: State) { y: position.y - domY, } - return pointToRendererPoint(correctedPosition, state.viewport, state.snapToGrid, state.snapGrid) + return pointToRendererPoint(correctedPosition, state.viewport, state.snapToGrid, state.snapGrid, state.ancestorZoom) } return { x: 0, y: 0 } @@ -208,7 +208,7 @@ export function useViewportHelper(state: State) { y: position.y + domY, } - return rendererPointToPoint(correctedPosition, state.viewport) + return rendererPointToPoint(correctedPosition, state.viewport, state.ancestorZoom) } return { x: 0, y: 0 } diff --git a/packages/core/src/container/Viewport/Viewport.vue b/packages/core/src/container/Viewport/Viewport.vue index 3d98d0fab..b05710572 100644 --- a/packages/core/src/container/Viewport/Viewport.vue +++ b/packages/core/src/container/Viewport/Viewport.vue @@ -38,6 +38,7 @@ const { d3Selection: storeD3Selection, d3ZoomHandler: storeD3ZoomHandler, viewport, + ancestorZoom, viewportRef, paneClickDistance, } = useVueFlow() @@ -245,6 +246,7 @@ onMounted(() => { return (!event.ctrlKey || panKeyPressed.value || event.type === 'wheel') && buttonAllowed }) + let prevEventTransform: ZoomTransform = zoomIdentity watch( [userSelectionActive, shouldPanOnDrag], () => { @@ -252,7 +254,12 @@ onMounted(() => { d3Zoom.on('zoom', null) } else if (!userSelectionActive.value) { d3Zoom.on('zoom', (event: D3ZoomEvent) => { - viewport.value = { x: event.transform.x, y: event.transform.y, zoom: event.transform.k } + viewport.value = { + x: viewport.value.x + (event.transform.x - prevEventTransform.x) / ancestorZoom.value, + y: viewport.value.y + (event.transform.y - prevEventTransform.y) / ancestorZoom.value, + zoom: event.transform.k, + } + prevEventTransform = event.transform const flowTransform = eventToFlowTransform(event.transform) diff --git a/packages/core/src/container/VueFlow/VueFlow.vue b/packages/core/src/container/VueFlow/VueFlow.vue index f71a823b3..e8064935e 100644 --- a/packages/core/src/container/VueFlow/VueFlow.vue +++ b/packages/core/src/container/VueFlow/VueFlow.vue @@ -27,6 +27,7 @@ const props = withDefaults(defineProps(), { zoomOnDoubleClick: undefined, panOnScroll: undefined, panOnDrag: undefined, + ancestorZoom: undefined, applyDefault: undefined, fitViewOnInit: undefined, connectOnClick: undefined, diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index ab8616d1a..f0b4ad378 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -157,8 +157,8 @@ export function useActions(state: State, nodeLookup: ComputedRef, ed if (doUpdate) { const nodeBounds = update.nodeElement.getBoundingClientRect() node.dimensions = dimensions - node.handleBounds.source = getHandleBounds('source', update.nodeElement, nodeBounds, zoom, node.id) - node.handleBounds.target = getHandleBounds('target', update.nodeElement, nodeBounds, zoom, node.id) + node.handleBounds.source = getHandleBounds('source', update.nodeElement, nodeBounds, zoom, state.ancestorZoom, node.id) + node.handleBounds.target = getHandleBounds('target', update.nodeElement, nodeBounds, zoom, state.ancestorZoom, node.id) changes.push({ id: node.id, diff --git a/packages/core/src/store/state.ts b/packages/core/src/store/state.ts index 2198b9465..a8582102d 100644 --- a/packages/core/src/store/state.ts +++ b/packages/core/src/store/state.ts @@ -21,6 +21,7 @@ export function useState(): State { height: 0, }, viewport: { x: 0, y: 0, zoom: 1 }, + ancestorZoom: 1, d3Zoom: null, d3Selection: null, diff --git a/packages/core/src/types/flow.ts b/packages/core/src/types/flow.ts index 4ea8134aa..734476242 100644 --- a/packages/core/src/types/flow.ts +++ b/packages/core/src/types/flow.ts @@ -179,6 +179,7 @@ export interface FlowProps { panOnDrag?: boolean | number[] minZoom?: number maxZoom?: number + ancestorZoom?: number defaultViewport?: Partial translateExtent?: CoordinateExtent nodeExtent?: CoordinateExtent | CoordinateExtentRange diff --git a/packages/core/src/types/store.ts b/packages/core/src/types/store.ts index 0fd614f82..f923cf7ed 100644 --- a/packages/core/src/types/store.ts +++ b/packages/core/src/types/store.ts @@ -67,6 +67,7 @@ export interface State extends Omit { minZoom: number /** use setMaxZoom action to change maxZoom */ maxZoom: number + ancestorZoom: number defaultViewport: Partial /** use setTranslateExtent action to change translateExtent */ translateExtent: CoordinateExtent diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index 209709b76..e50554d3f 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -289,10 +289,14 @@ export function updateEdge(oldEdge: Edge, newConnection: Connection, elements: E return elements.filter((e) => e.id !== oldEdge.id) } -export function rendererPointToPoint({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: ViewportTransform): XYPosition { +export function rendererPointToPoint( + { x, y }: XYPosition, + { x: tx, y: ty, zoom: tScale }: ViewportTransform, + ancestorZoom: number, +): XYPosition { return { - x: x * tScale + tx, - y: y * tScale + ty, + x: x * tScale * ancestorZoom + tx, + y: y * tScale * ancestorZoom + ty, } } export function pointToRendererPoint( @@ -300,10 +304,11 @@ export function pointToRendererPoint( { x: tx, y: ty, zoom: tScale }: ViewportTransform, snapToGrid: boolean = false, snapGrid: [snapX: number, snapY: number] = [1, 1], + ancestorZoom = 1, ): XYPosition { const position: XYPosition = { - x: (x - tx) / tScale, - y: (y - ty) / tScale, + x: (x - tx) / (tScale * ancestorZoom), + y: (y - ty) / (tScale * ancestorZoom), } return snapToGrid ? snapPosition(position, snapGrid) : position diff --git a/packages/core/src/utils/node.ts b/packages/core/src/utils/node.ts index 12855cf17..0961aaded 100644 --- a/packages/core/src/utils/node.ts +++ b/packages/core/src/utils/node.ts @@ -8,6 +8,7 @@ export function getHandleBounds( nodeElement: HTMLDivElement, nodeBounds: DOMRect, zoom: number, + ancestorZoom: number, nodeId: string, ): HandleElement[] | null { const handles = nodeElement.querySelectorAll(`.vue-flow__handle.${type}`) @@ -24,8 +25,8 @@ export function getHandleBounds( type, nodeId, position: handle.getAttribute('data-handlepos') as unknown as Position, - x: (handleBounds.left - nodeBounds.left) / zoom, - y: (handleBounds.top - nodeBounds.top) / zoom, + x: (handleBounds.left - nodeBounds.left) / zoom / ancestorZoom, + y: (handleBounds.top - nodeBounds.top) / zoom / ancestorZoom, ...getDimensions(handle as HTMLDivElement), } })