From da471175855be10d3df53b2760036a96b3f856c2 Mon Sep 17 00:00:00 2001 From: peterkogo Date: Mon, 8 Jan 2024 12:25:03 +0100 Subject: [PATCH] Extracted xyresizer functions to system --- .../NodeResizer/ResizeControl.tsx | 70 +++----- .../plugins/NodeResizer/NodeResizer.svelte | 2 - .../plugins/NodeResizer/ResizeControl.svelte | 87 +++------- packages/system/src/xyresizer/index.ts | 78 +-------- packages/system/src/xyresizer/types.ts | 34 ++++ packages/system/src/xyresizer/utils.ts | 154 ++++++++++++++++++ 6 files changed, 235 insertions(+), 190 deletions(-) create mode 100644 packages/system/src/xyresizer/types.ts create mode 100644 packages/system/src/xyresizer/utils.ts diff --git a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx index 7db2f778..14520c23 100644 --- a/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx +++ b/packages/react/src/additional-components/NodeResizer/ResizeControl.tsx @@ -2,7 +2,12 @@ import { useRef, useEffect, memo } from 'react'; import cc from 'classcat'; import { drag } from 'd3-drag'; import { select } from 'd3-selection'; -import { getPointerPosition, clamp } from '@xyflow/system'; +import { + getPointerPosition, + getControlDirection, + getDimensionsAfterResize, + getPositionAfterResize, +} from '@xyflow/system'; import { useStoreApi } from '../../hooks/useStore'; import { useNodeId } from '../../contexts/NodeIdContext'; @@ -53,10 +58,7 @@ function ResizeControl({ const selection = select(resizeControlRef.current); - const enableX = controlPosition.includes('right') || controlPosition.includes('left'); - const enableY = controlPosition.includes('bottom') || controlPosition.includes('top'); - const invertX = controlPosition.includes('left'); - const invertY = controlPosition.includes('top'); + const controlDirection = getControlDirection(controlPosition); const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { @@ -82,61 +84,27 @@ function ResizeControl({ }) .on('drag', (event: ResizeDragEvent) => { const { nodeLookup, transform, snapGrid, snapToGrid, triggerNodeChanges } = store.getState(); - const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); + const pointerPosition = getPointerPosition(event.sourceEvent, { transform, snapGrid, snapToGrid }); const node = nodeLookup.get(id); if (node) { const changes: NodeChange[] = []; - const { - pointerX: startX, - pointerY: startY, - width: startWidth, - height: startHeight, - x: startNodeX, - y: startNodeY, - aspectRatio, - } = startValues.current; const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues.current; - const distX = Math.floor(enableX ? xSnapped - startX : 0); - const distY = Math.floor(enableY ? ySnapped - startY : 0); - - let width = clamp(startWidth + (invertX ? -distX : distX), minWidth, maxWidth); - let height = clamp(startHeight + (invertY ? -distY : distY), minHeight, maxHeight); - - if (keepAspectRatio) { - const nextAspectRatio = width / height; - const isDiagonal = enableX && enableY; - const isHorizontal = enableX && !enableY; - const isVertical = enableY && !enableX; - - width = (nextAspectRatio <= aspectRatio && isDiagonal) || isVertical ? height * aspectRatio : width; - height = (nextAspectRatio > aspectRatio && isDiagonal) || isHorizontal ? width / aspectRatio : height; - - if (width >= maxWidth) { - width = maxWidth; - height = maxWidth / aspectRatio; - } else if (width <= minWidth) { - width = minWidth; - height = minWidth / aspectRatio; - } - - if (height >= maxHeight) { - height = maxHeight; - width = maxHeight * aspectRatio; - } else if (height <= minHeight) { - height = minHeight; - width = minHeight * aspectRatio; - } - } + const { width, height } = getDimensionsAfterResize( + startValues.current, + controlDirection, + pointerPosition, + { minWidth, minHeight, maxWidth, maxHeight }, + keepAspectRatio + ); const isWidthChange = width !== prevWidth; const isHeightChange = height !== prevHeight; - if (invertX || invertY) { - const x = invertX ? startNodeX - (width - startWidth) : startNodeX; - const y = invertY ? startNodeY - (height - startHeight) : startNodeY; + if (controlDirection.affectsX || controlDirection.affectsY) { + const { x, y } = getPositionAfterResize(startValues.current, controlDirection, width, height); // only transform the node if the width or height changes const isXPosChange = x !== prevX && isWidthChange; @@ -184,8 +152,8 @@ function ResizeControl({ prevWidth, height: prevValues.current.height, prevHeight, - invertX, - invertY, + affectsX: controlDirection.affectsX, + affectsY: controlDirection.affectsY, }); const nextValues = { ...prevValues.current, direction }; diff --git a/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte b/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte index f38d06c6..1738c70d 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/NodeResizer.svelte @@ -30,8 +30,6 @@ let _minHeight = minHeight || 10; let _maxWidth = maxWidth || Number.MAX_VALUE; let _maxHeight = maxHeight || Number.MAX_VALUE; - - $: console.log(isVisible); {#if isVisible} diff --git a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte index 822d77c5..621356e5 100644 --- a/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte +++ b/packages/svelte/src/lib/plugins/NodeResizer/ResizeControl.svelte @@ -7,8 +7,11 @@ import { ResizeControlVariant, getPointerPosition, - clamp, - getResizeDirection + getResizeDirection, + getControlDirection, + type XYResizeControlPosition, + getDimensionsAfterResize, + getPositionAfterResize } from '@xyflow/system'; import { useStore } from '$lib/store'; @@ -52,7 +55,9 @@ let startValues = { ...initStartValues }; let prevValues = { ...initPrevValues }; - $: defaultPosition = variant === ResizeControlVariant.Line ? 'right' : 'bottom-right'; + $: defaultPosition = ( + variant === ResizeControlVariant.Line ? 'right' : 'bottom-right' + ) as XYResizeControlPosition; $: controlPosition = position ?? defaultPosition; $: positionClassNames = controlPosition.split('-'); @@ -67,10 +72,7 @@ } const selection = select(resizeControlRef); - const enableX = controlPosition.includes('right') || controlPosition.includes('left'); - const enableY = controlPosition.includes('bottom') || controlPosition.includes('top'); - const invertX = controlPosition.includes('left'); - const invertY = controlPosition.includes('top'); + const controlDirection = getControlDirection(controlPosition); const dragHandler = drag() .on('start', (event: ResizeDragEvent) => { @@ -98,7 +100,7 @@ onResizeStart?.(event, { ...prevValues }); }) .on('drag', (event: ResizeDragEvent) => { - const { xSnapped, ySnapped } = getPointerPosition(event.sourceEvent, { + const pointerPosition = getPointerPosition(event.sourceEvent, { transform: [$viewport.x, $viewport.y, $viewport.zoom], snapGrid: $snapGrid ?? undefined, snapToGrid: !!$snapGrid @@ -106,64 +108,27 @@ const node = $nodeLookup.get(id); if (node) { - const { - pointerX: startX, - pointerY: startY, - width: startWidth, - height: startHeight, - x: startNodeX, - y: startNodeY, - aspectRatio - } = startValues; - const { x: prevX, y: prevY, width: prevWidth, height: prevHeight } = prevValues; - const distX = Math.floor(enableX ? xSnapped - startX : 0); - const distY = Math.floor(enableY ? ySnapped - startY : 0); - - let width = clamp(startWidth + (invertX ? -distX : distX), minWidth, maxWidth); - let height = clamp(startHeight + (invertY ? -distY : distY), minHeight, maxHeight); - - if (keepAspectRatio) { - const nextAspectRatio = width / height; - const isDiagonal = enableX && enableY; - const isHorizontal = enableX && !enableY; - const isVertical = enableY && !enableX; - - width = - (nextAspectRatio <= aspectRatio && isDiagonal) || isVertical - ? height * aspectRatio - : width; - height = - (nextAspectRatio > aspectRatio && isDiagonal) || isHorizontal - ? width / aspectRatio - : height; - - if (width >= _maxWidth) { - width = _maxWidth; - height = _maxWidth / aspectRatio; - } else if (width <= _minWidth) { - width = _minWidth; - height = _minWidth / aspectRatio; - } - - if (height >= _maxHeight) { - height = _maxHeight; - width = _maxHeight * aspectRatio; - } else if (height <= _minHeight) { - height = _minHeight; - width = _minHeight * aspectRatio; - } - } - + const { width, height } = getDimensionsAfterResize( + startValues, + controlDirection, + pointerPosition, + { + minWidth: _minWidth, + minHeight: _minHeight, + maxWidth: _maxWidth, + maxHeight: _maxHeight + }, + !!keepAspectRatio + ); const isWidthChange = width !== prevWidth; const isHeightChange = height !== prevHeight; let changes = false; - if (invertX || invertY) { - const x = invertX ? startNodeX - (width - startWidth) : startNodeX; - const y = invertY ? startNodeY - (height - startHeight) : startNodeY; + if (controlDirection.affectsX || controlDirection.affectsY) { + const { x, y } = getPositionAfterResize(startValues, controlDirection, width, height); // only transform the node if the width or height changes const isXPosChange = x !== prevX && isWidthChange; @@ -199,8 +164,8 @@ prevWidth, height: prevValues.height, prevHeight, - invertX, - invertY + affectsX: controlDirection.affectsX, + affectsY: controlDirection.affectsY }); const nextValues = { ...prevValues, direction }; diff --git a/packages/system/src/xyresizer/index.ts b/packages/system/src/xyresizer/index.ts index 1ebbe956..6d5a6ef4 100644 --- a/packages/system/src/xyresizer/index.ts +++ b/packages/system/src/xyresizer/index.ts @@ -1,76 +1,2 @@ -export type { D3DragEvent, SubjectPosition } from 'd3-drag'; - -export type XYResizeParams = { - x: number; - y: number; - width: number; - height: number; -}; - -export type XYResizeParamsWithDirection = XYResizeParams & { - direction: number[]; -}; - -export type XYResizeControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; - -export type XYResizeControlPosition = - | XYResizeControlLinePosition - | 'top-left' - | 'top-right' - | 'bottom-left' - | 'bottom-right'; - -export enum ResizeControlVariant { - Line = 'line', - Handle = 'handle', -} - -export const XY_RESIZER_HANDLE_CONTROLS: XYResizeControlPosition[] = [ - 'top-left', - 'top-right', - 'bottom-left', - 'bottom-right', -]; -export const XY_RESIZER_LINE_CONTROLS: XYResizeControlLinePosition[] = ['top', 'right', 'bottom', 'left']; - -type GetResizeDirectionParams = { - width: number; - prevWidth: number; - height: number; - prevHeight: number; - invertX: boolean; - invertY: boolean; -}; - -/** - * Get all connecting edges for a given set of nodes - * @param width - new width of the node - * @param prevWidth - previous width of the node - * @param height - new height of the node - * @param prevHeight - previous height of the node - * @param invertX - whether to invert the resize direction for the x axis - * @param invertY - whether to invert the resize direction for the y axis - * @returns array of two numbers representing the direction of the resize for each axis, 0 = no change, 1 = increase, -1 = decrease - */ -export function getResizeDirection({ - width, - prevWidth, - height, - prevHeight, - invertX, - invertY, -}: GetResizeDirectionParams) { - const deltaWidth = width - prevWidth; - const deltaHeight = height - prevHeight; - - const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0]; - - if (deltaWidth && invertX) { - direction[0] = direction[0] * -1; - } - - if (deltaHeight && invertY) { - direction[1] = direction[1] * -1; - } - return direction; -} +export * from './types'; +export * from './utils'; diff --git a/packages/system/src/xyresizer/types.ts b/packages/system/src/xyresizer/types.ts new file mode 100644 index 00000000..bf1ca556 --- /dev/null +++ b/packages/system/src/xyresizer/types.ts @@ -0,0 +1,34 @@ +export type { D3DragEvent, SubjectPosition } from 'd3-drag'; + +export type XYResizeParams = { + x: number; + y: number; + width: number; + height: number; +}; + +export type XYResizeParamsWithDirection = XYResizeParams & { + direction: number[]; +}; + +export type XYResizeControlLinePosition = 'top' | 'bottom' | 'left' | 'right'; + +export type XYResizeControlPosition = + | XYResizeControlLinePosition + | 'top-left' + | 'top-right' + | 'bottom-left' + | 'bottom-right'; + +export enum ResizeControlVariant { + Line = 'line', + Handle = 'handle', +} + +export const XY_RESIZER_HANDLE_CONTROLS: XYResizeControlPosition[] = [ + 'top-left', + 'top-right', + 'bottom-left', + 'bottom-right', +]; +export const XY_RESIZER_LINE_CONTROLS: XYResizeControlLinePosition[] = ['top', 'right', 'bottom', 'left']; diff --git a/packages/system/src/xyresizer/utils.ts b/packages/system/src/xyresizer/utils.ts new file mode 100644 index 00000000..15c3af06 --- /dev/null +++ b/packages/system/src/xyresizer/utils.ts @@ -0,0 +1,154 @@ +import { clamp, getPointerPosition } from '../utils'; +import { XYResizeControlPosition } from './types'; + +type GetResizeDirectionParams = { + width: number; + prevWidth: number; + height: number; + prevHeight: number; + affectsX: boolean; + affectsY: boolean; +}; + +/** + * Get all connecting edges for a given set of nodes + * @param width - new width of the node + * @param prevWidth - previous width of the node + * @param height - new height of the node + * @param prevHeight - previous height of the node + * @param affectsX - whether to invert the resize direction for the x axis + * @param affectsY - whether to invert the resize direction for the y axis + * @returns array of two numbers representing the direction of the resize for each axis, 0 = no change, 1 = increase, -1 = decrease + */ +export function getResizeDirection({ + width, + prevWidth, + height, + prevHeight, + affectsX, + affectsY, +}: GetResizeDirectionParams) { + const deltaWidth = width - prevWidth; + const deltaHeight = height - prevHeight; + + const direction = [deltaWidth > 0 ? 1 : deltaWidth < 0 ? -1 : 0, deltaHeight > 0 ? 1 : deltaHeight < 0 ? -1 : 0]; + + if (deltaWidth && affectsX) { + direction[0] = direction[0] * -1; + } + + if (deltaHeight && affectsY) { + direction[1] = direction[1] * -1; + } + return direction; +} + +/** + * Parses the control position that is being dragged to dimensions that are being resized + * @param controlPosition - position of the control that is being dragged + * @returns isHorizontal, isVertical, affectsX, affectsY, + */ +export function getControlDirection(controlPosition: XYResizeControlPosition) { + const isHorizontal = controlPosition.includes('right') || controlPosition.includes('left'); + const isVertical = controlPosition.includes('bottom') || controlPosition.includes('top'); + const affectsX = controlPosition.includes('left'); + const affectsY = controlPosition.includes('top'); + + return { + isHorizontal, + isVertical, + affectsX, + affectsY, + }; +} + +type PrevValues = { + width: number; + height: number; + x: number; + y: number; +}; + +type StartValues = PrevValues & { + pointerX: number; + pointerY: number; + aspectRatio: number; +}; + +/** + * Calculates new width & height of node after resize based on pointer position + * @param startValues - starting values of resize + * @param controlDirection - dimensions affected by the resize + * @param pointerPosition - the current pointer position corrected for snapping + * @param boundries - maximum and minimum dimensions of the node + * @param keepAspectRatio - prevent changes of asprect ratio + * @returns width: new width of node, height: new height of node + */ +export function getDimensionsAfterResize( + startValues: StartValues, + controlDirection: ReturnType, + pointerPosition: ReturnType, + boundries: { minWidth: number; maxWidth: number; minHeight: number; maxHeight: number }, + keepAspectRatio: boolean +) { + const { isHorizontal, isVertical, affectsX, affectsY } = controlDirection; + const { xSnapped, ySnapped } = pointerPosition; + const { minWidth, maxWidth, minHeight, maxHeight } = boundries; + + const { pointerX: startX, pointerY: startY, width: startWidth, height: startHeight, aspectRatio } = startValues; + const distX = Math.floor(isHorizontal ? xSnapped - startX : 0); + const distY = Math.floor(isVertical ? ySnapped - startY : 0); + + let width = clamp(startWidth + (affectsX ? -distX : distX), minWidth, maxWidth); + let height = clamp(startHeight + (affectsY ? -distY : distY), minHeight, maxHeight); + + if (keepAspectRatio) { + const nextAspectRatio = width / height; + const isDiagonal = isHorizontal && isVertical; + const isOnlyHorizontal = isHorizontal && !isVertical; + const isOnlyVertical = isVertical && !isHorizontal; + + width = (nextAspectRatio <= aspectRatio && isDiagonal) || isOnlyVertical ? height * aspectRatio : width; + height = (nextAspectRatio > aspectRatio && isDiagonal) || isOnlyHorizontal ? width / aspectRatio : height; + + if (width >= maxWidth) { + width = maxWidth; + height = maxWidth / aspectRatio; + } else if (width <= minWidth) { + width = minWidth; + height = minWidth / aspectRatio; + } + + if (height >= maxHeight) { + height = maxHeight; + width = maxHeight * aspectRatio; + } else if (height <= minHeight) { + height = minHeight; + width = minHeight * aspectRatio; + } + } + + return { + width, + height, + }; +} + +/** + * Determines new x & y position of node after resize based on new width & height + * @param startValues - starting values of resize + * @param controlDirection - dimensions affected by the resize + * @param width - new width of node + * @param height - new height of node + * @returns x: new x position of node, y: new y position of node + */ +export function getPositionAfterResize( + startValues: StartValues, + controlDirection: ReturnType, + width: number, + height: number +) { + const x = controlDirection.affectsX ? startValues.x - (width - startValues.width) : startValues.x; + const y = controlDirection.affectsY ? startValues.y - (height - startValues.height) : startValues.y; + return { x, y }; +}