From b41e1a148b5208e8046ec4abc3aa2aa592385fc8 Mon Sep 17 00:00:00 2001 From: gkuzin13 Date: Sun, 25 Feb 2024 23:18:23 +0200 Subject: [PATCH] feat: arrow bend center snapping --- .../ArrowDrawable/ArrowTransformer.test.tsx | 66 +++++++++---------- .../Shapes/ArrowDrawable/ArrowTransformer.tsx | 23 ++++--- .../Canvas/Shapes/ArrowDrawable/helpers.ts | 10 +-- apps/client/src/constants/shape.ts | 1 + .../src/utils/__tests__/position.test.ts | 17 ++++- apps/client/src/utils/math.ts | 4 ++ apps/client/src/utils/position.ts | 4 ++ 7 files changed, 78 insertions(+), 47 deletions(-) diff --git a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.test.tsx b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.test.tsx index e07927a..0caab6d 100644 --- a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.test.tsx +++ b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.test.tsx @@ -4,38 +4,38 @@ import { findCanvas, renderWithProviders } from '@/test/test-utils'; import ArrowTransformer from './ArrowTransformer'; import { ARROW_TRANSFORMER } from '@/constants/shape'; -describe('ArrowTransformer', () => { - describe('cursor', () => { - it('grab on transformer anchor hover', async () => { - renderWithProviders( - - - - - , - ); - - const { container } = await findCanvas(); - - const arrowTransformerAnchor = Konva.stages[0].findOne( - `.${ARROW_TRANSFORMER.ANCHOR_NAME}`, - ); - - arrowTransformerAnchor?.fire('mouseenter'); - - expect(container.style.cursor).toBe('grab'); - - arrowTransformerAnchor?.fire('mouseleave'); - - expect(container.style.cursor).toBe(''); - }); +describe('cursor', () => { + it('grab on transformer anchor hover', async () => { + renderWithProviders( + + + + + , + ); + + const { container } = await findCanvas(); + + const arrowTransformerAnchor = Konva.stages[0].findOne( + `.${ARROW_TRANSFORMER.ANCHOR_NAME}`, + ); + + arrowTransformerAnchor?.fire('mouseenter'); + + expect(container.style.cursor).toBe('grab'); + + arrowTransformerAnchor?.fire('mouseleave'); + + expect(container.style.cursor).toBe(''); }); }); + +// [TODO] - test bend snap diff --git a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.tsx b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.tsx index 48008dc..df90674 100644 --- a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.tsx +++ b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowTransformer.tsx @@ -4,6 +4,8 @@ import { calculateClampedMidPoint, getAnchorType } from './helpers'; import { ARROW_TRANSFORMER } from '@/constants/shape'; import useDefaultThemeColors from '@/hooks/useThemeColors'; import { hexToRGBa } from '@/utils/string'; +import { calculateMidPointFromRange } from '@/utils/math'; +import { inRange } from '@/utils/position'; import { resetCursor, setCursor, @@ -150,7 +152,7 @@ const ArrowTransformer = ({ (event: Konva.KonvaEventObject) => { const node = event.target as Konva.Circle; const anchorType = getAnchorType(node); - + if (!anchorType) { return; } @@ -158,15 +160,20 @@ const ArrowTransformer = ({ const { x, y } = node.position(); if (anchorType === 'control') { - const { x: clampedX, y: clampedY } = calculateClampedMidPoint( - [x, y], - start, - end, - ); + let updatedPoint = calculateClampedMidPoint([x, y], start, end); + const mid = calculateMidPointFromRange(start, end); + const offset = ARROW_TRANSFORMER.SNAP_OFFSET; + + if ( + inRange(updatedPoint[0], mid[0] - offset, mid[0] + offset) && + inRange(updatedPoint[1], mid[1] - offset, mid[1] + offset) + ) { + updatedPoint = mid; + } - node.position({ x: clampedX, y: clampedY }); + node.position({ x: updatedPoint[0], y: updatedPoint[1] }); - onTransform({ anchorType, point: [clampedX, clampedY] }); + onTransform({ anchorType, point: updatedPoint }); return; } diff --git a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/helpers.ts b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/helpers.ts index 1a4d19a..670af2d 100644 --- a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/helpers.ts +++ b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/helpers.ts @@ -58,7 +58,7 @@ export function calculateClampedMidPoint( dragPosition: Point, start: Point, end: Point, -) { +): Point { const { mid, perp, length } = calculateMidPointAndPerp(start, end); // Calculate the distance of the drag from the midpoint along the perpendicular vector @@ -67,10 +67,10 @@ export function calculateClampedMidPoint( dragDist = Math.max(Math.min(dragDist, length / 2), -length / 2); - return { - x: mid.x + dragDist * perp.x, - y: mid.y + dragDist * perp.y, - }; + return [ + mid.x + dragDist * perp.x, + mid.y + dragDist * perp.y, + ]; } export function getDefaultPoints(node: NodeObject<'arrow'>) { diff --git a/apps/client/src/constants/shape.ts b/apps/client/src/constants/shape.ts index 8551673..65a7a19 100644 --- a/apps/client/src/constants/shape.ts +++ b/apps/client/src/constants/shape.ts @@ -21,6 +21,7 @@ export const ARROW_TRANSFORMER = { STROKE: colors.green300, NAME: 'arrow-transformer', ANCHOR_NAME: 'arrow-transformer-anchor', + SNAP_OFFSET: 8 } as const; export const ARROW = { diff --git a/apps/client/src/utils/__tests__/position.test.ts b/apps/client/src/utils/__tests__/position.test.ts index 033dcc4..6553400 100644 --- a/apps/client/src/utils/__tests__/position.test.ts +++ b/apps/client/src/utils/__tests__/position.test.ts @@ -1,7 +1,12 @@ import type { Point } from 'shared'; import { nodesGenerator } from '@/test/data-generators'; import { createNode } from '../node'; -import { getMiddleNode, getNodeRect, isNodeFullyInView } from '../position'; +import { + getMiddleNode, + getNodeRect, + inRange, + isNodeFullyInView, +} from '../position'; describe('getMiddleNode', () => { const nodes = nodesGenerator(3, 'rectangle').map((node) => ({ @@ -104,3 +109,13 @@ describe('isNodeFullyInView', () => { expect(isNodeFullyInView(node, stageRect, 1)).toBe(true); }); }); + +describe('inRange', () => { + it('returns true if value is in range', () => { + expect(inRange(5, 1, 10)).toBe(true); + }); + + it('returns false if value is not in range', () => { + expect(inRange(15, 1, 10)).toBe(false); + }); +}); diff --git a/apps/client/src/utils/math.ts b/apps/client/src/utils/math.ts index e342c18..9e63b7b 100644 --- a/apps/client/src/utils/math.ts +++ b/apps/client/src/utils/math.ts @@ -17,6 +17,10 @@ export function calculateMiddlePoint(rect: IRect): Vector2d { }; } +export function calculateMidPointFromRange(start: Point, end: Point): Point { + return [(start[0] + end[0]) / 2, (start[1] + end[1]) / 2]; +} + export function clamp(value: number, range: [min: number, max: number]) { return Math.min(Math.max(value, range[0]), range[1]); } diff --git a/apps/client/src/utils/position.ts b/apps/client/src/utils/position.ts index f19c016..6eddcb9 100644 --- a/apps/client/src/utils/position.ts +++ b/apps/client/src/utils/position.ts @@ -216,3 +216,7 @@ export function getNormalizedInvertedRect(rect: IRect, scale: number): IRect { export function calculateCenterPoint(width: number, height: number) { return { x: width / 2, y: height / 2 }; } + +export function inRange(value: number, start: number, end: number) { + return value > start && value < end; +}