feat(core): resolve promise from viewport actions on transition end

This commit is contained in:
braks
2023-07-19 18:48:12 +02:00
committed by Braks
parent fb569b461b
commit 7df2bfa146
3 changed files with 77 additions and 44 deletions

View File

@@ -64,6 +64,7 @@ export default defineComponent({
:min-zoom="0.2" :min-zoom="0.2"
:max-zoom="4" :max-zoom="4"
:zoom-on-scroll="false" :zoom-on-scroll="false"
fit-view-on-init
@connect="onConnect" @connect="onConnect"
@pane-ready="onPaneReady" @pane-ready="onPaneReady"
@node-drag-stop="onNodeDragStop" @node-drag-stop="onNodeDragStop"

View File

@@ -1,5 +1,5 @@
import { zoomIdentity } from 'd3-zoom' import { zoomIdentity } from 'd3-zoom'
import { computed, ref } from 'vue' import { computed } from 'vue'
import type { ComputedGetters, D3Selection, GraphNode, State, ViewportFunctions } from '~/types' import type { ComputedGetters, D3Selection, GraphNode, State, ViewportFunctions } from '~/types'
import { clampPosition, getRectOfNodes, getTransformForBounds, pointToRendererPoint } from '~/utils' import { clampPosition, getRectOfNodes, getTransformForBounds, pointToRendererPoint } from '~/utils'
@@ -9,7 +9,9 @@ interface ExtendedViewport extends ViewportFunctions {
const DEFAULT_PADDING = 0.1 const DEFAULT_PADDING = 0.1
function noop() {} function noop() {
return Promise.resolve(false)
}
const initialViewportHelper: ExtendedViewport = { const initialViewportHelper: ExtendedViewport = {
zoomIn: noop, zoomIn: noop,
@@ -25,59 +27,89 @@ const initialViewportHelper: ExtendedViewport = {
} }
export function useViewport(state: State, getters: ComputedGetters) { export function useViewport(state: State, getters: ComputedGetters) {
const { nodes, d3Zoom, d3Selection, dimensions, translateExtent, minZoom, maxZoom, viewport, snapToGrid, snapGrid, hooks } = const { nodes, d3Zoom, d3Selection, dimensions, translateExtent, minZoom, maxZoom, viewport, snapToGrid, snapGrid } = $(state)
$(state)
const { getNodes } = getters const { getNodes, getNodesInitialized } = getters
const nodesInitialized = ref(false) const isReady = computed(
() =>
hooks.nodesInitialized.on(() => { !!d3Zoom &&
nodesInitialized.value = true !!d3Selection &&
}) !!dimensions.width &&
!!dimensions.height &&
const isReady = computed(() => !!d3Zoom && !!d3Selection && !!dimensions.width && !!dimensions.height && nodesInitialized.value) getNodesInitialized.value.length === getNodes.value.length,
)
function zoom(scale: number, duration?: number) { function zoom(scale: number, duration?: number) {
if (d3Selection && d3Zoom) { return new Promise<boolean>((resolve) => {
d3Zoom.scaleBy(transition(d3Selection, duration), scale) if (d3Selection && d3Zoom) {
} d3Zoom.scaleBy(
transition(d3Selection, duration, () => {
resolve(true)
}),
scale,
)
} else {
resolve(false)
}
})
} }
function transformViewport(x: number, y: number, zoom: number, duration?: number) { function transformViewport(x: number, y: number, zoom: number, duration?: number) {
// enforce translate extent return new Promise<boolean>((resolve) => {
const { x: clampedX, y: clampedY } = clampPosition({ x: -x, y: -y }, translateExtent) // enforce translate extent
const { x: clampedX, y: clampedY } = clampPosition({ x: -x, y: -y }, translateExtent)
const nextTransform = zoomIdentity.translate(-clampedX, -clampedY).scale(zoom) const nextTransform = zoomIdentity.translate(-clampedX, -clampedY).scale(zoom)
if (d3Selection && d3Zoom) { if (d3Selection && d3Zoom) {
d3Zoom.transform(transition(d3Selection, duration), nextTransform) d3Zoom.transform(
} transition(d3Selection, duration, () => {
resolve(true)
}),
nextTransform,
)
} else {
resolve(false)
}
})
} }
return computed<ExtendedViewport>(() => { return computed<ExtendedViewport>(() => {
if (isReady.value) { if (isReady.value) {
return { return {
initialized: true, initialized: true,
// todo: allow passing scale as option
zoomIn: (options) => { zoomIn: (options) => {
zoom(1.2, options?.duration) return zoom(1.2, options?.duration)
}, },
zoomOut: (options) => { zoomOut: (options) => {
zoom(1 / 1.2, options?.duration) return zoom(1 / 1.2, options?.duration)
}, },
zoomTo: (zoomLevel, options) => { zoomTo: (zoomLevel, options) => {
if (d3Selection && d3Zoom) { return new Promise<boolean>((resolve) => {
d3Zoom.scaleTo(transition(d3Selection, options?.duration), zoomLevel) if (d3Selection && d3Zoom) {
} d3Zoom.scaleTo(
transition(d3Selection, options?.duration, () => {
resolve(true)
}),
zoomLevel,
)
} else {
resolve(false)
}
})
}, },
setTransform: (transform, options) => { setTransform: (transform, options) => {
transformViewport(transform.x, transform.y, transform.zoom, options?.duration) return transformViewport(transform.x, transform.y, transform.zoom, options?.duration)
},
getTransform: () => {
return {
x: viewport.x,
y: viewport.y,
zoom: viewport.zoom,
}
}, },
getTransform: () => ({
x: viewport.x,
y: viewport.y,
zoom: viewport.zoom,
}),
fitView: ( fitView: (
options = { options = {
padding: DEFAULT_PADDING, padding: DEFAULT_PADDING,
@@ -97,7 +129,7 @@ export function useViewport(state: State, getters: ComputedGetters) {
}) })
if (!nodesToFit.length) { if (!nodesToFit.length) {
return return Promise.resolve(false)
} }
const bounds = getRectOfNodes(nodesToFit) const bounds = getRectOfNodes(nodesToFit)
@@ -112,14 +144,14 @@ export function useViewport(state: State, getters: ComputedGetters) {
options.offset, options.offset,
) )
transformViewport(x, y, zoom, options?.duration) return transformViewport(x, y, zoom, options?.duration)
}, },
setCenter: (x, y, options) => { setCenter: (x, y, options) => {
const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom const nextZoom = typeof options?.zoom !== 'undefined' ? options.zoom : maxZoom
const centerX = dimensions.width / 2 - x * nextZoom const centerX = dimensions.width / 2 - x * nextZoom
const centerY = dimensions.height / 2 - y * nextZoom const centerY = dimensions.height / 2 - y * nextZoom
transformViewport(centerX, centerY, nextZoom, options?.duration) return transformViewport(centerX, centerY, nextZoom, options?.duration)
}, },
fitBounds: (bounds, options = { padding: DEFAULT_PADDING }) => { fitBounds: (bounds, options = { padding: DEFAULT_PADDING }) => {
const { x, y, zoom } = getTransformForBounds( const { x, y, zoom } = getTransformForBounds(
@@ -131,7 +163,7 @@ export function useViewport(state: State, getters: ComputedGetters) {
options.padding, options.padding,
) )
transformViewport(x, y, zoom, options?.duration) return transformViewport(x, y, zoom, options?.duration)
}, },
project: (position) => pointToRendererPoint(position, viewport, snapToGrid, snapGrid), project: (position) => pointToRendererPoint(position, viewport, snapToGrid, snapGrid),
} }
@@ -141,6 +173,6 @@ export function useViewport(state: State, getters: ComputedGetters) {
}) })
} }
function transition(selection: D3Selection, ms = 0) { function transition(selection: D3Selection, ms = 0, onEnd: () => void) {
return selection.transition().duration(ms) return selection.transition().duration(ms).on('end', onEnd)
} }

View File

@@ -42,28 +42,28 @@ export type FitBoundsOptions = TransitionOptions & {
} }
/** Fit the viewport around visible nodes */ /** Fit the viewport around visible nodes */
export type FitView = (fitViewOptions?: FitViewParams) => void export type FitView = (fitViewOptions?: FitViewParams) => Promise<boolean>
/** project a position onto the viewport, i.e. a mouse event clientX/clientY onto graph coordinates */ /** project a position onto the viewport, i.e. a mouse event clientX/clientY onto graph coordinates */
export type Project = (position: XYPosition) => XYPosition export type Project = (position: XYPosition) => XYPosition
/** set center of viewport */ /** set center of viewport */
export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => Promise<boolean>
/** fit the viewport around bounds */ /** fit the viewport around bounds */
export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => Promise<boolean>
/** zoom in/out */ /** zoom in/out */
export type ZoomInOut = (options?: TransitionOptions) => void export type ZoomInOut = (options?: TransitionOptions) => Promise<boolean>
/** zoom to a specific level */ /** zoom to a specific level */
export type ZoomTo = (zoomLevel: number, options?: TransitionOptions) => void export type ZoomTo = (zoomLevel: number, options?: TransitionOptions) => Promise<boolean>
/** get current viewport transform */ /** get current viewport transform */
export type GetTransform = () => ViewportTransform export type GetTransform = () => ViewportTransform
/** set current viewport transform */ /** set current viewport transform */
export type SetTransform = (transform: ViewportTransform, options?: TransitionOptions) => void export type SetTransform = (transform: ViewportTransform, options?: TransitionOptions) => Promise<boolean>
export interface ViewportFunctions { export interface ViewportFunctions {
zoomIn: ZoomInOut zoomIn: ZoomInOut