From fcdbc21d341b06ac35a240d2f384aa31133e8c7e Mon Sep 17 00:00:00 2001 From: Braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Sat, 9 Apr 2022 21:41:02 +0200 Subject: [PATCH] refactor(flow!): Change transform to viewport * Change array type to object type and rename transform type to viewport * Update examples & docs --- docs/package.json | 2 +- docs/src/guide/config.md | 8 +- docs/src/guide/index.md | 2 +- docs/src/guide/utilities.md | 349 ------------------ docs/src/guide/utils/graph.md | 2 +- examples/package.json | 2 +- examples/src/Nesting/GroupNode.vue | 4 +- examples/src/Provider/Sidebar.vue | 4 +- .../Background/Background.vue | 8 +- .../additional-components/MiniMap/MiniMap.vue | 11 +- package/src/auto-imports.d.ts | 22 +- .../ConnectionLine/ConnectionLine.vue | 4 +- package/src/components/Nodes/NodeWrapper.vue | 6 +- .../NodesSelection/NodesSelection.vue | 8 +- .../UserSelection/UserSelection.vue | 2 +- package/src/composables/useZoomPanHelper.ts | 16 +- package/src/container/Viewport/Transform.vue | 4 +- package/src/container/Viewport/Viewport.vue | 177 +++++++-- package/src/container/Viewport/Zoom.vue | 151 -------- package/src/store/actions.ts | 2 +- package/src/store/getters.ts | 4 +- package/src/store/state.ts | 2 +- package/src/types/flow.ts | 7 +- package/src/types/store.ts | 17 +- package/src/types/zoom.ts | 19 +- package/src/utils/edge.ts | 14 +- package/src/utils/graph.ts | 23 +- tests/package.json | 2 +- yarn.lock | 16 +- 29 files changed, 255 insertions(+), 633 deletions(-) delete mode 100644 docs/src/guide/utilities.md delete mode 100644 package/src/container/Viewport/Zoom.vue diff --git a/docs/package.json b/docs/package.json index a90d7545..4aa39776 100644 --- a/docs/package.json +++ b/docs/package.json @@ -9,7 +9,7 @@ "build": "vuepress build src" }, "dependencies": { - "@braks/vue-flow": "^0.4.2-0", + "@braks/vue-flow": "^0.4.2-3", "blobity": "^0.1.7" }, "devDependencies": { diff --git a/docs/src/guide/config.md b/docs/src/guide/config.md index b1bc92fd..27c7923b 100644 --- a/docs/src/guide/config.md +++ b/docs/src/guide/config.md @@ -331,9 +331,9 @@ const edges = ref([ - Details: - Trigger fit view when transformation-pane is mounted. + Trigger fit view when viewport is mounted. -## Zoom & Pan Options +## Viewport Options ### zoom-activation-key-code @@ -475,7 +475,7 @@ const edges = ref([ - Details: - Default zoom pane position on initial load. + Default viewport position on initial load. ### translate-extent @@ -492,7 +492,7 @@ const edges = ref([ - Details: - The area in which the zoom pane can be moved around. + The area in which the viewport can be moved around. ## Selection Pane Options diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index da6771f1..4fb0735b 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -5,7 +5,7 @@ title: Introduction # Introduction Vue Flow is a library for building node-based applications. These can be simple static diagrams or complex node-based -editors. You can implement custom nodes and edges. It also comes with components like a mini-map, zoom pane controls and +editors. You can implement custom nodes and edges. It also comes with components like a mini-map, viewport controls and a background. Check out the [examples](/examples/) if you want to dive directly into the code. diff --git a/docs/src/guide/utilities.md b/docs/src/guide/utilities.md deleted file mode 100644 index ff5ef6c0..00000000 --- a/docs/src/guide/utilities.md +++ /dev/null @@ -1,349 +0,0 @@ -# Utilities - -Vue Flow comes with built-in utilities to control the zoom-pane, state and more. - -## Graph Utilities - -### isEdge - -- Details: - - Confirms if an element is an edge. - -- Example: - -```vue:no-line-numbers{13} - - -``` - -### isNode - -- Details: - - Confirms if an element is a node. - -- Example: - -```vue:no-line-numbers{13} - - -``` - -### addEdge - -::: warning -When using composition you should access `addEdges` from `useVueFlow` -::: - -- Details: - - Confirms if an element is a node. - -- Example: - -```vue:no-line-numbers{12} - - -``` - -### updateEdge - -::: warning -When using composition you should access `updateEdge` from `useVueFlow` -::: - -- Details: - - Updates an edge and applies new source/target. - -- Example: - -```vue:no-line-numbers{12} - - -``` - -### [getOutgoers](https://types.vueflow.dev/modules.html#getOutgoers) - -- Details: - - Returns all target elements of a node. - -### [getIncomers](https://types.vueflow.dev/modules.html#getIncomers) - -- Details: - - Returns all source elements of a node. - -### [getConnectedEdges](https://types.vueflow.dev/modules.html#getConnectedEdges) - -- Details: - - Returns all connected edges of a node. - -### [getTransformForBounds](https://types.vueflow.dev/modules.html#getTransformForBounds) - -- Details: - - Returns a transformation for the zoom pane according to input bounds. - -### [getRectOfNodes](https://types.vueflow.dev/modules.html#getRectOfNodes) - -- Details: - - Returns a rect of node elements. - - Useful when you need to know the boundaries of a set of nodes. - -### [getNodesInside](https://types.vueflow.dev/modules.html#getNodesInside) - -- Details: - - Returns node elements that are inside a specified rect. - -### [getMarkerId](https://types.vueflow.dev/modules.html#getMarkerId) - -- Details: - - Returns a marker id for a marker definition. - -## Instance - -The Vue Flow instance provides easy access to zoom-pan-helper functions. -It can be accessed either directly from the state using `useVueFlow` or -you can receive the instance with a `onPaneReady` event handler. - - - - - -```vue:no-line-numbers - -``` - - - - - - -```vue:no-line-numbers - - -``` - - - -### [project](https://types.vueflow.dev/modules.html#Project) - -- Details: - - Transforms pixel coordinates to the internal VueFlow coordinate system. - - This can be used when you drag nodes (from a sidebar for example) and need the internal position on the pane. - -- Example: - -```ts:no-line-numbers -vueFlowInstance.project({ x: 100, y: 100 }) -``` - -### [fitView](https://types.vueflow.dev/modules.html#FitView) - -- Details: - - Fits the view port so that all nodes are visible. - - Padding is 0.1 and includeHiddenNodes is false by default. - -- Example: - -```ts:no-line-numbers -vueFlowInstance.fitView({ padding: 0.25, includeHiddenNodes: true }) -``` - -### [fitBounds](https://types.vueflow.dev/modules.html#FitBounds) - -- Details: - - Fits the view port according to the bounds' rect input. - -- Example: - -```ts:no-line-numbers -vueFlowInstance.fitBounds(getRectOfNodes(nodes.value)) -``` - -### [setTransform](https://types.vueflow.dev/modules.html#SetTransform) - -- Details: - - Sets position and zoom of the pane. - -- Example: - -```ts:no-line-numbers -vueFlowInstance.setTransform({ x: 100, y: 100, zoom: 1.5 }) -``` - -### [getTransform](https://types.vueflow.dev/modules.html#GetTransform) - -- Details: - - Gets position and zoom of the pane. - -### [zoomIn](https://types.vueflow.dev/modules.html#ZoomInOut) - -- Details: - - Zooms in. - - -### [zoomOut](https://types.vueflow.dev/modules.html#ZoomInOut) - -- Details: - - Zooms out. - -### [zoomTo](https://types.vueflow.dev/modules.html#ZoomTo) - -- Details: - - Zooms to specific level. - -### getElements - -- Details: - - Returns currently stored elements (nodes + edges). - -### getNodes - -- Details: - - Returns currently stored nodes. - -### getEdges - -- Details: - - Returns currently stored edges. - -### toObject - -- Details: - - Returns elements, position and zoom of the current flow state. - -- Example: - -```ts:no-line-numbers -toObject = (): { - elements: FlowElements, - position: [x, y], - zoom: scale, -} -``` diff --git a/docs/src/guide/utils/graph.md b/docs/src/guide/utils/graph.md index 18bbdd61..a4027da7 100644 --- a/docs/src/guide/utils/graph.md +++ b/docs/src/guide/utils/graph.md @@ -154,7 +154,7 @@ const onEdgeUpdate = ({ edge, connection }) => { - Details: - Returns a transformation for the zoom pane according to input bounds. + Returns a transformation for the viewport according to input bounds. ### getRectOfNodes diff --git a/examples/package.json b/examples/package.json index d6297ed3..2f8b0def 100644 --- a/examples/package.json +++ b/examples/package.json @@ -10,7 +10,7 @@ "serve": "vite serve" }, "dependencies": { - "@braks/vue-flow": "^0.4.2-0" + "@braks/vue-flow": "^0.4.2-3" }, "devDependencies": { "@types/dagre": "^0.7.46", diff --git a/examples/src/Nesting/GroupNode.vue b/examples/src/Nesting/GroupNode.vue index b8063535..c98573ae 100644 --- a/examples/src/Nesting/GroupNode.vue +++ b/examples/src/Nesting/GroupNode.vue @@ -3,7 +3,7 @@ import { Handle, Position, getNodesInside, useVueFlow } from '@braks/vue-flow' import type { NodeProps } from '@braks/vue-flow' const props = defineProps() -const { onNodeDragStop, getNodes, transform } = useVueFlow() +const { onNodeDragStop, getNodes, viewport } = useVueFlow() onNodeDragStop(({ node }) => { const nodes = getNodesInside( getNodes.value, @@ -12,7 +12,7 @@ onNodeDragStop(({ node }) => { x: props.computedPosition.x, y: props.computedPosition.y, }, - transform.value, + viewport.value, ) if (nodes.some((n) => n.id === node.id && n.id !== props.id)) { node.label = `In ${props.id}` diff --git a/examples/src/Provider/Sidebar.vue b/examples/src/Provider/Sidebar.vue index d15e515a..90594c64 100644 --- a/examples/src/Provider/Sidebar.vue +++ b/examples/src/Provider/Sidebar.vue @@ -1,6 +1,6 @@ @@ -37,17 +163,10 @@ export default { } diff --git a/package/src/container/Viewport/Zoom.vue b/package/src/container/Viewport/Zoom.vue deleted file mode 100644 index ad6382eb..00000000 --- a/package/src/container/Viewport/Zoom.vue +++ /dev/null @@ -1,151 +0,0 @@ - - - diff --git a/package/src/store/actions.ts b/package/src/store/actions.ts index e9541359..93076db0 100644 --- a/package/src/store/actions.ts +++ b/package/src/store/actions.ts @@ -152,7 +152,7 @@ export default (state: State, getters: ComputedGetters): Actions => { dimensions.height && (node.dimensions.width !== dimensions.width || node.dimensions.height !== dimensions.height || update.forceUpdate) ) - node.handleBounds = getHandleBounds(update.nodeElement, state.transform[2]) + node.handleBounds = getHandleBounds(update.nodeElement, state.viewport.zoom) if (doUpdate) { node.dimensions = dimensions diff --git a/package/src/store/getters.ts b/package/src/store/getters.ts index c2daba67..20e4178c 100644 --- a/package/src/store/getters.ts +++ b/package/src/store/getters.ts @@ -35,7 +35,7 @@ export default (state: State): ComputedGetters => { width: state.dimensions.width, height: state.dimensions.height, }, - state.transform, + state.viewport, true, ) : nodes ?? [] @@ -63,7 +63,7 @@ export default (state: State): ComputedGetters => { targetHeight: e.targetNode.dimensions.height, width: state.dimensions.width, height: state.dimensions.height, - transform: state.transform, + viewport: state.viewport, }), ) }) diff --git a/package/src/store/state.ts b/package/src/store/state.ts index 4f752294..878a0faf 100644 --- a/package/src/store/state.ts +++ b/package/src/store/state.ts @@ -46,7 +46,7 @@ const defaultState = (): State => ({ width: 0, height: 0, }, - transform: [0, 0, 1], + viewport: { x: 0, y: 0, zoom: 1 }, d3Zoom: null, d3Selection: null, diff --git a/package/src/types/flow.ts b/package/src/types/flow.ts index 7a8e238f..d7fc2947 100644 --- a/package/src/types/flow.ts +++ b/package/src/types/flow.ts @@ -2,7 +2,7 @@ import { CSSProperties, VNode } from 'vue' import { GraphEdge, Edge, DefaultEdgeOptions } from './edge' import { GraphNode, CoordinateExtent, Node } from './node' import { ConnectionLineType, ConnectionMode } from './connection' -import { KeyCode, PanOnScrollMode, Viewport } from './zoom' +import { KeyCode, PanOnScrollMode, ViewportFuncs } from './zoom' import { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } from './components' export type ElementData = any @@ -36,9 +36,6 @@ export interface Element { } export type Elements = (Node | Edge)[] -/** Transform x, y, z */ -export type Transform = [number, number, number] - /** Handle Positions */ export enum Position { Left = 'left', @@ -93,7 +90,7 @@ interface Exports { toObject: () => FlowExportObject } -export type FlowInstance = Exports & Viewport +export type FlowInstance = Exports & ViewportFuncs export interface FlowProps { id?: string diff --git a/package/src/types/store.ts b/package/src/types/store.ts index 2023b2a3..0ae78186 100644 --- a/package/src/types/store.ts +++ b/package/src/types/store.ts @@ -1,21 +1,10 @@ import { ComputedRef, CSSProperties, ToRefs } from 'vue' -import { - Dimensions, - ElementData, - Elements, - FlowElements, - FlowInstance, - FlowOptions, - Rect, - SnapGrid, - Transform, - XYPosition, -} from './flow' +import { Dimensions, ElementData, Elements, FlowElements, FlowInstance, FlowOptions, Rect, SnapGrid, XYPosition } from './flow' import { EdgeComponent, NodeComponent, DefaultNodeTypes, DefaultEdgeTypes } from './components' import { Connection, ConnectionLineType, ConnectionMode } from './connection' import { DefaultEdgeOptions, Edge, GraphEdge } from './edge' import { GraphNode, CoordinateExtent, Node } from './node' -import { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode } from './zoom' +import { D3Selection, D3Zoom, D3ZoomHandler, KeyCode, PanOnScrollMode, Viewport } from './zoom' import { FlowHooks, FlowHooksOn } from './hooks' import { NodeChange, EdgeChange } from './changes' import { StartHandle, HandleType } from './handle' @@ -44,7 +33,7 @@ export interface State /** viewport dimensions */ dimensions: Dimensions /** viewport transform x, y, z */ - transform: Transform + viewport: Viewport onlyRenderVisibleElements: boolean defaultPosition: [number, number] diff --git a/package/src/types/zoom.ts b/package/src/types/zoom.ts index e0f55f3b..0ee675d9 100644 --- a/package/src/types/zoom.ts +++ b/package/src/types/zoom.ts @@ -5,6 +5,9 @@ export type D3Zoom = ZoomBehavior export type D3Selection = Selection export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void +/** Transform x, y, z */ +export type Viewport = { x: number; y: number; zoom: number } + export type KeyCode = number | string export enum PanOnScrollMode { @@ -13,7 +16,7 @@ export enum PanOnScrollMode { Horizontal = 'horizontal', } -export type UseZoomPanHelperOptions = { +export type ViewportFuncsOptions = { duration?: number } @@ -27,7 +30,7 @@ export type FitViewParams = { y?: number } nodes?: string[] -} & UseZoomPanHelperOptions +} & ViewportFuncsOptions export type FlowTransform = { x: number @@ -35,11 +38,11 @@ export type FlowTransform = { zoom: number } -export type SetCenterOptions = UseZoomPanHelperOptions & { +export type SetCenterOptions = ViewportFuncsOptions & { zoom?: number } -export type FitBoundsOptions = UseZoomPanHelperOptions & { +export type FitBoundsOptions = ViewportFuncsOptions & { padding?: number } @@ -47,12 +50,12 @@ export type FitView = (fitViewOptions?: FitViewParams) => void export type Project = (position: XYPosition) => XYPosition export type SetCenter = (x: number, y: number, options?: SetCenterOptions) => void export type FitBounds = (bounds: Rect, options?: FitBoundsOptions) => void -export type ZoomInOut = (options?: UseZoomPanHelperOptions) => void -export type ZoomTo = (zoomLevel: number, options?: UseZoomPanHelperOptions) => void +export type ZoomInOut = (options?: ViewportFuncsOptions) => void +export type ZoomTo = (zoomLevel: number, options?: ViewportFuncsOptions) => void export type GetTransform = () => FlowTransform -export type SetTransform = (transform: FlowTransform, options?: UseZoomPanHelperOptions) => void +export type SetTransform = (transform: FlowTransform, options?: ViewportFuncsOptions) => void -export interface Viewport { +export interface ViewportFuncs { zoomIn: ZoomInOut zoomOut: ZoomInOut zoomTo: ZoomTo diff --git a/package/src/utils/edge.ts b/package/src/utils/edge.ts index 40a59be0..231d53da 100644 --- a/package/src/utils/edge.ts +++ b/package/src/utils/edge.ts @@ -1,5 +1,5 @@ import { rectToBox } from './graph' -import { EdgePositions, GraphEdge, GraphNode, HandleElement, Position, Rect, Transform, XYPosition } from '~/types' +import { EdgePositions, GraphEdge, GraphNode, HandleElement, Position, Rect, Viewport, XYPosition } from '~/types' export const getHandlePosition = (position: Position, rect: Rect, handle?: HandleElement): XYPosition => { const x = (handle?.x ?? 0) + rect.x @@ -83,7 +83,7 @@ interface IsEdgeVisibleParams { targetHeight: number width: number height: number - transform: Transform + viewport: Viewport } export function isEdgeVisible({ @@ -95,7 +95,7 @@ export function isEdgeVisible({ targetHeight, width, height, - transform, + viewport, }: IsEdgeVisibleParams): boolean { const edgeBox = { x: Math.min(sourcePos.x, targetPos.x), @@ -113,10 +113,10 @@ export function isEdgeVisible({ } const viewBox = rectToBox({ - x: (0 - transform[0]) / transform[2], - y: (0 - transform[1]) / transform[2], - width: width / transform[2], - height: height / transform[2], + x: (0 - viewport.x) / viewport.zoom, + y: (0 - viewport.y) / viewport.zoom, + width: width / viewport.zoom, + height: height / viewport.zoom, }) const xOverlap = Math.max(0, Math.min(viewBox.x2, edgeBox.x2) - Math.max(viewBox.x, edgeBox.x)) diff --git a/package/src/utils/graph.ts b/package/src/utils/graph.ts index 9b988b24..e1661ed7 100644 --- a/package/src/utils/graph.ts +++ b/package/src/utils/graph.ts @@ -16,7 +16,7 @@ import { Rect, State, Store, - Transform, + Viewport, XYPosition, XYZPosition, } from '~/types' @@ -200,7 +200,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E export const pointToRendererPoint = ( { x, y }: XYPosition, - [tx, ty, tScale]: Transform, + { x: tx, y: ty, zoom: tScale }: Viewport, snapToGrid: boolean, [snapX, snapY]: [number, number], ) => { @@ -220,7 +220,7 @@ export const pointToRendererPoint = ( } export const onLoadProject = (currentStore: Store) => (position: XYPosition) => - pointToRendererPoint(position, currentStore.transform, currentStore.snapToGrid, currentStore.snapGrid) + pointToRendererPoint(position, currentStore.viewport, currentStore.snapToGrid, currentStore.snapGrid) const getBoundsOfBoxes = (box1: Box, box2: Box): Box => ({ x: Math.min(box1.x, box2.x), @@ -261,12 +261,17 @@ export const getRectOfNodes = (nodes: GraphNode[]) => { return boxToRect(box) } -export const graphPosToZoomedPos = ({ x, y }: XYPosition, [tx, ty, tScale]: Transform): XYPosition => ({ +export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: Viewport): XYPosition => ({ x: x * tScale + tx, y: y * tScale + ty, }) -export const getNodesInside = (nodes: GraphNode[], rect: Rect, [tx, ty, tScale]: Transform = [0, 0, 1], partially = false) => { +export const getNodesInside = ( + nodes: GraphNode[], + rect: Rect, + { x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 }, + partially = false, +) => { const rBox = rectToBox({ x: (rect.x - tx) / tScale, y: (rect.y - ty) / tScale, @@ -309,8 +314,8 @@ export const onLoadToObject = (store: State) => (): FlowExportObject => { JSON.stringify({ nodes: store.nodes, edges: store.edges, - position: [store.transform[0], store.transform[1]], - zoom: store.transform[2], + position: [store.viewport.x, store.viewport.y], + zoom: store.viewport.zoom, } as FlowExportObject), ) } @@ -326,7 +331,7 @@ export const getTransformForBounds = ( x?: number y?: number } = { x: 0, y: 0 }, -): Transform => { +): Viewport => { const xZoom = width / (bounds.width * (1 + padding)) const yZoom = height / (bounds.height * (1 + padding)) const zoom = Math.min(xZoom, yZoom) @@ -336,7 +341,7 @@ export const getTransformForBounds = ( const x = width / 2 - boundsCenterX * clampedZoom + (offset.x ?? 0) const y = height / 2 - boundsCenterY * clampedZoom + (offset.y ?? 0) - return [x, y, clampedZoom] + return { x, y, zoom: clampedZoom } } export const getXYZPos = (parentPos: XYZPosition, computedPosition: XYZPosition): XYZPosition => { diff --git a/tests/package.json b/tests/package.json index dd1c8d0e..c369e67d 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,7 +7,7 @@ "open": "cypress open-ct" }, "dependencies": { - "@braks/vue-flow": "^0.4.0-17" + "@braks/vue-flow": "^0.4.2-3" }, "devDependencies": { "@cypress/vite-dev-server": "^2.2.1", diff --git a/yarn.lock b/yarn.lock index dae3422e..e72ac672 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1196,18 +1196,6 @@ "@vueuse/core" "^7.0.1" vue-demi latest -"@braks/vue-flow@^0.4.0-17": - version "0.4.1" - resolved "https://registry.yarnpkg.com/@braks/vue-flow/-/vue-flow-0.4.1.tgz#81ce05e6a47f7232f09ec78f93ea371105b85b43" - integrity sha512-EOTo/zAvtnP7ZtXVJ8DSKJrQWMoajVVaFgzx5/ghVTn2KWU846S2AIaahy2L95x8gyzob0YmsJ73tkgoPgmuJg== - dependencies: - "@braks/revue-draggable" "^0.4.2" - "@types/d3" "^7.1.0" - "@vueuse/core" "^7.5.5" - d3 "^7.1.1" - d3-selection "^3.0.0" - d3-zoom "^3.0.0" - "@cypress/mount-utils@1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@cypress/mount-utils/-/mount-utils-1.0.2.tgz#afbc4f8c350b7cd86edc5ad0db0cbe1e0181edc8" @@ -2352,7 +2340,7 @@ ora "^5.4.1" upath "^2.0.1" -"@vueuse/core@^7.0.1", "@vueuse/core@^7.5.5": +"@vueuse/core@^7.0.1": version "7.7.1" resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-7.7.1.tgz#fc284f4103de73c7fb79bc06579d8066790db511" integrity sha512-PRRgbATMpoeUmkCEBtUeJgOwtew8s+4UsEd+Pm7MhkjL2ihCNrSqxNVtM6NFE4uP2sWnkGcZpCjPuNSxowJ1Ow== @@ -3550,7 +3538,7 @@ d3-zoom@3, d3-zoom@^3.0.0: d3-selection "2 - 3" d3-transition "2 - 3" -d3@^7.1.1, d3@^7.4.2: +d3@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/d3/-/d3-7.4.2.tgz#c5d1001459dc9517bc774949f71b95e8735b3b4e" integrity sha512-7VK+QBAWtNDbP2EU/ThkXgjd0u1MsXYYgCK2ElQ4BBWh0usE75tHVVeYx47m2pqQEy4isYKAA0tAFSln0l+9EQ==