diff --git a/packages/core/src/auto-imports.d.ts b/packages/core/src/auto-imports.d.ts index b867ed82..65b43fe1 100644 --- a/packages/core/src/auto-imports.d.ts +++ b/packages/core/src/auto-imports.d.ts @@ -347,6 +347,7 @@ declare global { const useVModel: typeof import('@vueuse/core')['useVModel'] const useVModels: typeof import('@vueuse/core')['useVModels'] const useVibrate: typeof import('@vueuse/core')['useVibrate'] + const useViewport: typeof import('./composables/useViewport')['default'] const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] const useVueFlow: typeof import('./composables/useVueFlow')['default'] const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] diff --git a/packages/core/src/composables/useViewport.ts b/packages/core/src/composables/useViewport.ts new file mode 100644 index 00000000..5b1d3a16 --- /dev/null +++ b/packages/core/src/composables/useViewport.ts @@ -0,0 +1,100 @@ +import { zoomIdentity } from 'd3-zoom' +import type { ComputedGetters, D3Selection, GraphNode, State, ViewportFunctions } from '~/types' + +const DEFAULT_PADDING = 0.1 + +export default (state: State, getters: ComputedGetters): ViewportFunctions => { + const { nodes, d3Zoom, d3Selection, dimensions, translateExtent, minZoom, maxZoom, viewport, snapToGrid, snapGrid } = $(state) + + const { getNodes } = $(getters) + + return { + zoomIn: async (options) => { + await zoom(1.2, options?.duration) + }, + zoomOut: async (options) => { + await zoom(1 / 1.2, options?.duration) + }, + zoomTo: async (zoomLevel, options) => { + if (d3Selection && d3Zoom) { + d3Zoom.scaleTo(transition(d3Selection, options?.duration), zoomLevel) + } + }, + setTransform: async (transform, options) => { + transformViewport(transform.x, transform.y, transform.zoom, options?.duration) + }, + getTransform: () => ({ + x: viewport.x, + y: viewport.y, + zoom: viewport.zoom, + }), + fitView: async ( + options = { + padding: DEFAULT_PADDING, + includeHiddenNodes: false, + duration: 0, + }, + ) => { + if (!nodes.length) return + + const nodesToFit: GraphNode[] = (options.includeHiddenNodes ? nodes : getNodes).filter((node) => { + const initialized = node.initialized && node.dimensions.width && node.dimensions.height + let shouldInclude = true + + if (options.nodes?.length) { + shouldInclude = options.nodes.includes(node.id) + } + + return initialized && shouldInclude + }) + + const bounds = getRectOfNodes(nodesToFit) + + const { x, y, zoom } = getTransformForBounds( + bounds, + dimensions.width, + dimensions.height, + options.minZoom ?? minZoom, + options.maxZoom ?? maxZoom, + options.padding ?? DEFAULT_PADDING, + options.offset, + ) + + transformViewport(x, y, zoom, options?.duration) + }, + setCenter: async (x, y, options) => { + const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom + const centerX = dimensions.width / 2 - x * nextZoom + const centerY = dimensions.height / 2 - y * nextZoom + + transformViewport(centerX, centerY, nextZoom, options?.duration) + }, + fitBounds: async (bounds, options = { padding: DEFAULT_PADDING }) => { + const { x, y, zoom } = getTransformForBounds(bounds, dimensions.width, dimensions.height, minZoom, maxZoom, options.padding) + + transformViewport(x, y, zoom, options?.duration) + }, + project: (position) => pointToRendererPoint(position, viewport, snapToGrid, snapGrid), + } + + async function zoom(scale: number, duration?: number) { + if (d3Selection && d3Zoom) { + d3Zoom.scaleBy(transition(d3Selection, duration), scale) + } + } + + function transformViewport(x: number, y: number, zoom: number, duration?: number) { + // enforce translate extent + const { x: clampedX, y: clampedY } = clampPosition({ x: -x, y: -y }, translateExtent) + + const nextTransform = zoomIdentity.translate(-clampedX, -clampedY).scale(zoom) + + if (d3Selection && d3Zoom) { + d3Zoom.transform(transition(d3Selection, duration), nextTransform) + } + } +} + +function transition(selection: D3Selection, ms = 0) { + return selection.transition().duration(ms) +} diff --git a/packages/core/src/store/actions.ts b/packages/core/src/store/actions.ts index 5a80c15b..e89676c8 100644 --- a/packages/core/src/store/actions.ts +++ b/packages/core/src/store/actions.ts @@ -27,23 +27,7 @@ export function useActions(state: State, getters: ComputedGetters): Actions { state.hooks.updateNodeInternals.trigger(ids) } - const zoomPanHelper = ref>() - - state.hooks.paneReady.on(({ id }) => { - zoomPanHelper.value = useZoomPanHelper(id) - }) - - const paneReady = async () => { - return new Promise>((resolve) => { - if (!zoomPanHelper.value) { - until(zoomPanHelper) - .not.toBeUndefined() - .then(() => resolve(zoomPanHelper.value!)) - } else { - resolve(zoomPanHelper.value) - } - }) - } + const viewportHelper = useViewport(state, getters) const nodeIds = $computed(() => state.nodes.map((n) => n.id)) const edgeIds = $computed(() => state.edges.map((e) => e.id)) @@ -280,10 +264,8 @@ export function useActions(state: State, getters: ComputedGetters): Actions { const setNodeExtent: Actions['setNodeExtent'] = async (nodeExtent) => { state.nodeExtent = nodeExtent - if (zoomPanHelper.value) { - const nodeIds = getters.getNodes.value.map((n) => n.id) - updateNodeInternals(nodeIds) - } + const nodeIds = getters.getNodes.value.map((n) => n.id) + updateNodeInternals(nodeIds) } const setInteractive: Actions['setInteractive'] = (isInteractive) => { @@ -628,38 +610,31 @@ export function useActions(state: State, getters: ComputedGetters): Actions { getIntersectingNodes, isNodeIntersecting, panBy, - fitView: async (params = { padding: 0.1 }) => { - const { fitView } = await paneReady() - fitView(params) + fitView: (params = { padding: 0.1 }) => { + viewportHelper.fitView(params) }, - zoomIn: async (options) => { - const { zoomIn } = await paneReady() - zoomIn(options) + zoomIn: (options) => { + viewportHelper.zoomIn(options) }, - zoomOut: async (options) => { - const { zoomOut } = await paneReady() - zoomOut(options) + zoomOut: (options) => { + viewportHelper.zoomOut(options) }, - zoomTo: async (zoomLevel, options) => { - const { zoomTo } = await paneReady() - zoomTo(zoomLevel, options) + zoomTo: (zoomLevel, options) => { + viewportHelper.zoomTo(zoomLevel, options) }, - setTransform: async (transform, options) => { - const { setTransform } = await paneReady() - setTransform(transform, options) + setTransform: (transform, options) => { + viewportHelper.setTransform(transform, options) }, getTransform: () => ({ x: state.viewport.x, y: state.viewport.y, zoom: state.viewport.zoom, }), - setCenter: async (x, y, options) => { - const { setCenter } = await paneReady() - setCenter(x, y, options) + setCenter: (x, y, options) => { + viewportHelper.setCenter(x, y, options) }, - fitBounds: async (bounds, options) => { - const { fitBounds } = await paneReady() - fitBounds(bounds, options) + fitBounds: (bounds, options) => { + viewportHelper.fitBounds(bounds, options) }, project: (position) => pointToRendererPoint(position, state.viewport, state.snapToGrid, state.snapGrid), toObject,