From eadbc0360bddc756702e99de0d3f0197e08ec793 Mon Sep 17 00:00:00 2001 From: gkuzin13 Date: Thu, 22 Feb 2024 18:56:01 +0200 Subject: [PATCH 1/2] fix: flashing arrow transformer --- apps/client/src/components/Canvas/DrawingCanvas/Nodes.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/client/src/components/Canvas/DrawingCanvas/Nodes.tsx b/apps/client/src/components/Canvas/DrawingCanvas/Nodes.tsx index 15e3fae..24fc2d7 100644 --- a/apps/client/src/components/Canvas/DrawingCanvas/Nodes.tsx +++ b/apps/client/src/components/Canvas/DrawingCanvas/Nodes.tsx @@ -29,6 +29,8 @@ const Nodes = ({ return selectedSingleNode ? selectedNodes[0].nodeProps.id : null; }, [selectedNodes]); + const hasSelectedNodes = selectedNodes.length > 0; + const handleNodeChange = useCallback( (node: NodeObject) => { onNodesChange([node]); @@ -51,7 +53,7 @@ const Nodes = ({ /> ); })} - {!editingNodeId && ( + {hasSelectedNodes && !editingNodeId && ( Date: Thu, 22 Feb 2024 18:56:53 +0200 Subject: [PATCH 2/2] feat: implement arrow heads --- .../Shapes/ArrowDrawable/ArrowDrawable.tsx | 78 +++++----- .../ArrowDrawable/ArrowTransformer.test.tsx | 3 +- .../Shapes/ArrowDrawable/ArrowTransformer.tsx | 143 ++++++++++-------- .../Canvas/Shapes/ArrowDrawable/heads.ts | 40 +++++ .../Canvas/Shapes/ArrowDrawable/helpers.ts | 31 +++- .../src/components/Elements/Icon/Icon.tsx | 11 +- .../src/components/Panels/Panels.test.tsx | 79 ++++++++++ .../Panels/StylePanel/ArrowHeadsSection.tsx | 106 +++++++++++++ .../Panels/StylePanel/StylePanel.styled.ts | 18 +++ .../Panels/StylePanel/StylePanel.tsx | 33 +++- .../Panels/__snapshots__/Panels.test.tsx.snap | 79 ++++++++++ apps/client/src/constants/panels.ts | 39 ++++- packages/shared/src/schemas/node.ts | 4 + packages/shared/src/types/index.ts | 5 + 14 files changed, 552 insertions(+), 117 deletions(-) create mode 100644 apps/client/src/components/Canvas/Shapes/ArrowDrawable/heads.ts create mode 100644 apps/client/src/components/Panels/StylePanel/ArrowHeadsSection.tsx create mode 100644 apps/client/src/components/Panels/__snapshots__/Panels.test.tsx.snap diff --git a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowDrawable.tsx b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowDrawable.tsx index 0a0e102..5e6efee 100644 --- a/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowDrawable.tsx +++ b/apps/client/src/components/Canvas/Shapes/ArrowDrawable/ArrowDrawable.tsx @@ -2,17 +2,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Line } from 'react-konva'; import useAnimatedDash from '@/hooks/useAnimatedDash/useAnimatedDash'; import useNode from '@/hooks/useNode/useNode'; -import ArrowTransformer from './ArrowTransformer'; +import ArrowTransformer, { type OnTransformFnParams } from './ArrowTransformer'; import { calculateLengthFromPoints, getValueFromRatio } from '@/utils/math'; import { getDashValue, getSizeValue, getTotalDashLength } from '@/utils/shape'; -import { ARROW } from '@/constants/shape'; import { calculateMinMaxMovementPoints, getBendValue, - getPoints, + getDefaultBend, + getDefaultPoints, } from './helpers'; +import { drawArrowHead } from './heads'; import type Konva from 'konva'; -import type { Point, NodeProps } from 'shared'; +import type { Point } from 'shared'; import type { NodeComponentProps } from '@/components/Canvas/Node/Node'; const ArrowDrawable = ({ @@ -21,8 +22,8 @@ const ArrowDrawable = ({ stageScale, onNodeChange, }: NodeComponentProps<'arrow'>) => { - const [points, setPoints] = useState(getPoints(node)); - const [bendValue, setBendValue] = useState(getBendValue(node)); + const [points, setPoints] = useState(getDefaultPoints(node)); + const [bendValue, setBendValue] = useState(getDefaultBend(node)); const [dragging, setDragging] = useState(false); const { config } = useNode(node, stageScale); @@ -35,6 +36,9 @@ const ArrowDrawable = ({ totalDashLength: getTotalDashLength(config.dash), }); + const arrowStartHead = node.style.arrowStartHead ?? 'arrow'; + const arrowEndHead = node.style.arrowEndHead; + const [start, end] = points; const bendMovement = useMemo(() => { @@ -50,15 +54,17 @@ const ArrowDrawable = ({ ]; }, [bendValue, bendMovement]); - const pointsWithControl = [start, control, end]; + const flattenedPoints = useMemo(() => { + return [start, control, end].flat(); + }, [start, control, end]); const shouldTransformerRender = useMemo(() => { return selected && node.nodeProps.visible && !dragging; }, [selected, node.nodeProps.visible, dragging]); useEffect(() => { - setPoints(getPoints(node)); - setBendValue(getBendValue(node)); + setPoints(getDefaultPoints(node)); + setBendValue(getDefaultBend(node)); }, [node]); const handleDragStart = useCallback(() => setDragging(true), []); @@ -71,14 +77,17 @@ const ArrowDrawable = ({ }, [node.style.animated, animation]); const handleTransform = useCallback( - (updatedPoints: Point[], bend?: NodeProps['bend']) => { - setPoints(updatedPoints); - - if (bend) { + (event: OnTransformFnParams) => { + if (event.anchorType === 'control') { + const bend = getBendValue(event.point, bendMovement); setBendValue(bend); + } else if (event.anchorType === 'start') { + setPoints((prevPoints) => [event.point, prevPoints[1]]); + } else { + setPoints((prevPoints) => [prevPoints[0], event.point]); } - const lineLength = calculateLengthFromPoints(updatedPoints); + const lineLength = calculateLengthFromPoints(points); const dash = getDashValue( lineLength, @@ -88,7 +97,7 @@ const ArrowDrawable = ({ lineRef.current?.dash(dash.map((d) => d * stageScale)); }, - [node.style.line, node.style.size, stageScale], + [points, bendMovement, stageScale, node.style.size, node.style.line], ); const handleTransformEnd = useCallback(() => { @@ -112,51 +121,32 @@ const ArrowDrawable = ({ { - // draw arrow line + ctx.save(); ctx.beginPath(); - ctx.setLineDash(shape.dash()); - ctx.moveTo(start[0], start[1]); ctx.quadraticCurveTo(control[0], control[1], end[0], end[1]); + ctx.restore(); ctx.fillStrokeShape(shape); - // draw arrow head - const dx = end[0] - control[0]; - const dy = end[1] - control[1]; - - const PI2 = Math.PI * 2; - const radians = (Math.atan2(dy, dx) + PI2) % PI2; - const length = (ARROW.HEAD_LENGTH / stageScale) * shape.strokeWidth(); - const width = (ARROW.HEAD_WIDTH / stageScale) * shape.strokeWidth(); - - ctx.beginPath(); + if (arrowStartHead === 'arrow') { + drawArrowHead(ctx, shape, [end, control]); + } - ctx.translate(end[0], end[1]); - ctx.rotate(radians); - - ctx.moveTo(0, 0); - ctx.lineTo(-length, width / 2); - - ctx.moveTo(0, 0); - ctx.lineTo(-length, -width / 2); - - ctx.restore(); - - ctx.fillStrokeShape(shape); + if (arrowEndHead === 'arrow') { + drawArrowHead(ctx, shape, [start, control]); + } }} /> {shouldTransformerRender && ( { void; type ArrowTransformerProps = { start: Point; + control: Point; end: Point; - bendPoint: Point; - bendMovement: BendMovement; stageScale: number; - onTranformStart: () => void; - onTransform: (updatedPoints: Point[], bend?: number) => void; - onTransformEnd: () => void; -}; - -type AnchorProps = { - x: number; - y: number; - scale: number; - onDragStart: (e: Konva.KonvaEventObject) => void; - onDragMove: (e: Konva.KonvaEventObject) => void; - onDragEnd: (e: Konva.KonvaEventObject) => void; - dragBoundFunc?: (position: Konva.Vector2d) => void; -}; - -const getBendValue = (dragPosition: Point, bendMovement: BendMovement) => { - const bendX = getRatioFromValue( - dragPosition[0], - bendMovement.min.x, - bendMovement.max.x, - ); - const bendY = getRatioFromValue( - dragPosition[1], - bendMovement.min.y, - bendMovement.max.y, - ); - - return +((bendX + bendY) / 2).toFixed(2); + onTranformStart: OnTransformFn; + onTransform: OnTransformFn; + onTransformEnd: OnTransformFn; }; - -const bendPointIndex = 2; +type AnchorProps = Konva.CircleConfig & { type: AnchorType }; const Anchor = ({ x, y, + type, scale, onDragStart, onDragMove, onDragEnd, + ...restProps }: AnchorProps) => { const themeColors = useDefaultThemeColors(); @@ -99,14 +78,14 @@ const Anchor = ({ ); }; const ArrowTransformer = ({ start, + control, end, - bendPoint, - bendMovement, stageScale, onTranformStart, onTransform, @@ -134,22 +113,51 @@ const ArrowTransformer = ({ const normalizedScale = 1 / stageScale; + const anchors: AnchorPoint[] = [ + { + type: 'start', + point: start, + }, + { + type: 'control', + point: control, + }, + { + type: 'end', + point: end, + }, + ]; + useEffect(() => { transformerRef.current?.moveToTop(); }, []); - const handleAnchorDragStart = useCallback(() => { - onTranformStart(); - }, [onTranformStart]); + const handleAnchorDragStart = useCallback( + (event: Konva.KonvaEventObject) => { + const node = event.target as Konva.Circle; + const anchorType = getAnchorType(node); + + if (anchorType) { + const { x, y } = node.position(); + + onTranformStart({ anchorType, point: [x, y] }); + } + }, + [onTranformStart], + ); const handleAnchorDragMove = useCallback( (event: Konva.KonvaEventObject) => { const node = event.target as Konva.Circle; - const stage = node.getStage() as Konva.Stage; + const anchorType = getAnchorType(node); + + if (!anchorType) { + return; + } - const { x, y } = node.getAbsolutePosition(stage); + const { x, y } = node.position(); - if (node.index === bendPointIndex) { + if (anchorType === 'control') { const { x: clampedX, y: clampedY } = calculateClampedMidPoint( [x, y], start, @@ -158,35 +166,40 @@ const ArrowTransformer = ({ node.position({ x: clampedX, y: clampedY }); - const updatedBend = getBendValue([clampedX, clampedY], bendMovement); - - onTransform([start, end], updatedBend); - + onTransform({ anchorType, point: [clampedX, clampedY] }); return; } - const updatedPoints = [...[start, end]]; + onTransform({ anchorType, point: [x, y] }); + }, + [start, end, onTransform], + ); + + const handleAnchorDragEnd = useCallback( + (event: Konva.KonvaEventObject) => { + const node = event.target as Konva.Circle; + const anchorType = getAnchorType(node); - updatedPoints[node.index] = [x, y]; + if (anchorType) { + const { x, y } = node.position(); - onTransform(updatedPoints); + onTransformEnd({ anchorType, point: [x, y] }); + } }, - [start, end, bendMovement, onTransform], + [onTransformEnd], ); - const handleAnchorDragEnd = useCallback(() => { - onTransformEnd(); - }, [onTransformEnd]); - return ( - {[start, end, bendPoint].map(([x, y], index) => { + {anchors.map((anchor) => { return ( ) { +export function getDefaultPoints(node: NodeObject<'arrow'>) { const { point, points } = node.nodeProps; return [point, ...(points || [point])]; } -export function getBendValue(node: NodeObject<'arrow'>) { +export function getDefaultBend(node: NodeObject<'arrow'>) { return node.nodeProps.bend ?? ARROW.DEFAULT_BEND; } + +export function getBendValue(dragPosition: Point, bendMovement: BendMovement) { + const bendX = getRatioFromValue( + dragPosition[0], + bendMovement.min.x, + bendMovement.max.x, + ); + const bendY = getRatioFromValue( + dragPosition[1], + bendMovement.min.y, + bendMovement.max.y, + ); + + return +((bendX + bendY) / 2).toFixed(2); +} + +export function getAnchorType(anchorNode: Konva.Circle): AnchorType | null { + return anchorNode.getAttrs().type; +} diff --git a/apps/client/src/components/Elements/Icon/Icon.tsx b/apps/client/src/components/Elements/Icon/Icon.tsx index ba8b2c8..25250d7 100644 --- a/apps/client/src/components/Elements/Icon/Icon.tsx +++ b/apps/client/src/components/Elements/Icon/Icon.tsx @@ -54,6 +54,8 @@ const ICONS = { letterM: Icons.TbLetterM, letterL: Icons.TbLetterL, extraLarge: ExtraLarge, + arrowNarrowRight: Icons.TbArrowNarrowRight, + arrowNarrowLeft: Icons.TbArrowNarrowLeft, } as const; const Icon = (props: IconProps) => { @@ -64,7 +66,14 @@ const Icon = (props: IconProps) => { const Component = ICONS[name]; - return ; + return ( + + ); }; export default Icon; diff --git a/apps/client/src/components/Panels/Panels.test.tsx b/apps/client/src/components/Panels/Panels.test.tsx index a47a6c1..d6c19d3 100644 --- a/apps/client/src/components/Panels/Panels.test.tsx +++ b/apps/client/src/components/Panels/Panels.test.tsx @@ -245,6 +245,85 @@ describe('style panel', () => { ); }); }); + + describe('arrow heads', () => { + const arrow = nodesGenerator(1, 'arrow')[0]; + arrow.style.arrowStartHead = 'none'; + arrow.style.arrowEndHead = 'arrow'; + + const preloadedArrow = stateGenerator({ + canvas: { + present: { + nodes: [arrow], + selectedNodeIds: { [arrow.nodeProps.id]: true }, + }, + }, + }); + + it('displays correct values', async () => { + const { user } = renderWithProviders( + , + { preloadedState: preloadedArrow }, + ); + + // open start head options + await user.click(screen.getByTestId(/arrow-start-head-trigger/)); + + expect(screen.getByTestId(/arrow-start-head-trigger/)).toMatchSnapshot(); + expect(screen.getByTestId(/start-none-button/i)).toHaveAttribute( + 'data-state', + 'checked', + ); + + // open end head options + await user.click(screen.getByTestId(/arrow-end-head-trigger/)); + + expect(screen.getByTestId(/arrow-end-head-trigger/)).toMatchSnapshot(); + expect(screen.getByTestId(/end-arrow-button/i)).toHaveAttribute( + 'data-state', + 'checked', + ); + }); + + it('updates arrow head icon button', async () => { + const { user } = renderWithProviders( + , + { preloadedState: preloadedArrow }, + ); + + const startHeadTrigger = screen.getByTestId(/arrow-start-head-trigger/); + + // open start head options + await user.click(startHeadTrigger); + + // select 'arrow' head + await user.click(screen.getByTestId(/start-arrow-button/i)); + + expect(screen.getByTestId(/start-arrow-button/i)).toHaveAttribute( + 'data-state', + 'checked', + ); + expect( + within(startHeadTrigger).getByTestId('arrowNarrowLeft-icon'), + ).toBeInTheDocument(); + + const endHeadTrigger = screen.getByTestId(/arrow-end-head-trigger/); + + // open end head options + await user.click(endHeadTrigger); + + // select 'none' head + await user.click(screen.getByTestId(/end-none-button/i)); + + expect(screen.getByTestId(/end-none-button/i)).toHaveAttribute( + 'data-state', + 'checked', + ); + expect( + within(endHeadTrigger).getByTestId('minus-icon'), + ).toBeInTheDocument(); + }); + }); }); describe('delete nodes button', () => { diff --git a/apps/client/src/components/Panels/StylePanel/ArrowHeadsSection.tsx b/apps/client/src/components/Panels/StylePanel/ArrowHeadsSection.tsx new file mode 100644 index 0000000..e315296 --- /dev/null +++ b/apps/client/src/components/Panels/StylePanel/ArrowHeadsSection.tsx @@ -0,0 +1,106 @@ +import { memo } from 'react'; +import { ARROW_HEADS } from '@/constants/panels'; +import { capitalizeFirstLetter, createTitle } from '@/utils/string'; +import Popover from '@/components/Elements/Popover/Popover'; +import Icon from '@/components/Elements/Icon/Icon'; +import { Button } from '@/components/Elements/Button/Button.styled'; +import * as Styled from './StylePanel.styled'; +import type { ArrowEndHead, ArrowHeadDirection, ArrowStartHead } from 'shared'; + +export type ArrowHead = NonNullable; +export type ArrowHeadEntity = (typeof ARROW_HEADS)[keyof typeof ARROW_HEADS]; + +type Props = { + startHead: ArrowStartHead; + endHead: ArrowEndHead; + onArrowHeadChange: (direction: ArrowHeadDirection, head: ArrowHead) => void; +}; + +const getCurrentIcon = (heads: ArrowHeadEntity, value: ArrowHead) => { + return heads.find((head) => head.value === value)?.icon ?? 'minus'; +}; + +const ArrowHeadsSection = ({ + startHead, + endHead, + onArrowHeadChange, +}: Props) => { + const getCurrentHead = (direction: ArrowHeadDirection) => { + if (direction === 'start') { + return startHead ?? 'arrow'; + } + return endHead ?? 'none'; + }; + + return ( + + Arrow Heads + + {Object.entries(ARROW_HEADS).map(([key, heads]) => { + const direction = key as ArrowHeadDirection; + const currentHead = getCurrentHead(direction); + const directionTitle = capitalizeFirstLetter(direction); + + return ( + + + + + + + { + onArrowHeadChange(direction, value); + }} + > + + {heads.map((head) => { + return ( + + + + ); + })} + + + + + + ); + })} + + + ); +}; + +export default memo(ArrowHeadsSection); diff --git a/apps/client/src/components/Panels/StylePanel/StylePanel.styled.ts b/apps/client/src/components/Panels/StylePanel/StylePanel.styled.ts index b1bab6e..fda69db 100644 --- a/apps/client/src/components/Panels/StylePanel/StylePanel.styled.ts +++ b/apps/client/src/components/Panels/StylePanel/StylePanel.styled.ts @@ -39,3 +39,21 @@ export const Item = styled(RadioGroup.Item, Button, { size: 'xs', }, }); + +export const ArrowHeadsContainer = styled('div', { + display: 'flex', + flexDirection: 'column', +}); + +export const ArrowHeadsTriggers = styled('div', { + display: 'flex', + alignItems: 'center', + gap: '$1', +}); + +export const ArrowHeadsPopoverContent = styled('div', { + display: 'flex', + alignItems: 'center', + gap: '$1', + padding: '$1', +}); diff --git a/apps/client/src/components/Panels/StylePanel/StylePanel.tsx b/apps/client/src/components/Panels/StylePanel/StylePanel.tsx index d33a1af..b848604 100644 --- a/apps/client/src/components/Panels/StylePanel/StylePanel.tsx +++ b/apps/client/src/components/Panels/StylePanel/StylePanel.tsx @@ -5,10 +5,12 @@ import AnimatedSection from './AnimatedSection'; import ColorSection from './ColorSection'; import LineSection from './LineSection'; import OpacitySection from './OpacitySection'; +import ArrowHeadsSection from './ArrowHeadsSection'; import FillSection from './FillSection'; import SizeSection from './SizeSection'; import * as Styled from './StylePanel.styled'; import type { + ArrowHeadDirection, NodeColor, NodeFill, NodeLine, @@ -16,6 +18,7 @@ import type { NodeSize, NodeStyle, } from 'shared'; +import type { ArrowHead } from './ArrowHeadsSection'; type Props = { selectedNodes: NodeObject[]; @@ -31,7 +34,7 @@ const getValueIfAllIdentical = < }; const StylePanel = ({ selectedNodes, onStyleChange }: Props) => { - const mergedStyle = useMemo(() => { + const mergedStyle = useMemo((): Partial => { const styles: NodeStyle[] = selectedNodes.map(({ style }) => style); const colors = new Set(styles.map(({ color }) => color)); @@ -40,6 +43,10 @@ const StylePanel = ({ selectedNodes, onStyleChange }: Props) => { const sizes = new Set(styles.map(({ size }) => size)); const opacities = new Set(styles.map(({ opacity }) => opacity)); const allShapesAnimated = styles.every(({ animated }) => animated); + const arrowStartHeads = new Set( + styles.map((style) => style.arrowStartHead), + ); + const arrowEndHeads = new Set(styles.map((style) => style.arrowEndHead)); return { color: getValueIfAllIdentical(colors), @@ -47,6 +54,8 @@ const StylePanel = ({ selectedNodes, onStyleChange }: Props) => { fill: getValueIfAllIdentical(fills), size: getValueIfAllIdentical(sizes), opacity: getValueIfAllIdentical(opacities), + arrowStartHead: getValueIfAllIdentical(arrowStartHeads), + arrowEndHead: getValueIfAllIdentical(arrowEndHeads), animated: allShapesAnimated, }; }, [selectedNodes]); @@ -60,7 +69,9 @@ const StylePanel = ({ selectedNodes, onStyleChange }: Props) => { return { line: false, size: true }; } - return { line: true, size: true }; + const hasArrowNodes = selectedNodesTypes.some((type) => type === 'arrow'); + + return { line: true, size: true, arrowHeads: hasArrowNodes }; }, [selectedNodes]); const fillSectionValue = useMemo(() => { @@ -104,12 +115,30 @@ const StylePanel = ({ selectedNodes, onStyleChange }: Props) => { onStyleChange({ fill: value }); }; + const handleArrowHeadChange = ( + direction: ArrowHeadDirection, + value: ArrowHead, + ) => { + if (direction === 'start') { + onStyleChange({ arrowStartHead: value }); + } else { + onStyleChange({ arrowEndHead: value }); + } + }; + return ( + {enabledOptions.arrowHeads && ( + + )} arrow heads > displays correct values 1`] = ` + +`; + +exports[`style panel > arrow heads > displays correct values 2`] = ` + +`; diff --git a/apps/client/src/constants/panels.ts b/apps/client/src/constants/panels.ts index 945308d..24c5fd1 100644 --- a/apps/client/src/constants/panels.ts +++ b/apps/client/src/constants/panels.ts @@ -3,7 +3,14 @@ import { Schemas } from 'shared'; import type { HistoryActionKey } from '@/stores/reducers/history'; import type { Entity } from '@/constants/index'; import type { ShapesThumbnailStyle } from '@/components/Elements/ShapesThumbnail/ShapesThumbnail'; -import type { NodeColor, NodeFill, NodeLine, NodeSize } from 'shared'; +import type { + ArrowHead, + ArrowHeadDirection, + NodeColor, + NodeFill, + NodeLine, + NodeSize, +} from 'shared'; import type { ToolType } from './app'; export type HistoryControlKey = Exclude; @@ -104,6 +111,36 @@ export const FILL: Entity[] = [ }, ]; +export const ARROW_HEADS: Record< + ArrowHeadDirection, + Entity>[] +> = { + start: [ + { + value: 'arrow', + name: 'Arrow', + icon: 'arrowNarrowLeft', + }, + { + value: 'none', + name: 'None', + icon: 'minus', + }, + ], + end: [ + { + value: 'none', + name: 'None', + icon: 'minus', + }, + { + value: 'arrow', + name: 'Arrow', + icon: 'arrowNarrowRight', + }, + ], +}; + export const OPACITY = { minValue: Schemas.Node.shape.style.shape.opacity.minValue || 0.2, maxValue: Schemas.Node.shape.style.shape.opacity.maxValue || 1, diff --git a/packages/shared/src/schemas/node.ts b/packages/shared/src/schemas/node.ts index b041c77..34a0e89 100644 --- a/packages/shared/src/schemas/node.ts +++ b/packages/shared/src/schemas/node.ts @@ -30,6 +30,8 @@ const Fill = z.union([ z.literal('solid'), ]); +const ArrowHead = z.union([z.literal('none'), z.literal('arrow')]); + export const NodePoint = z.tuple([z.number(), z.number()]) as z.ZodTuple< [x: z.ZodNumber, y: z.ZodNumber] >; @@ -52,6 +54,8 @@ const Style = z.object({ size: Size, animated: z.boolean().optional(), opacity: z.number().min(0.2).max(1), + arrowStartHead: ArrowHead.optional(), + arrowEndHead: ArrowHead.optional(), }); export const Node = z.object({ diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 3b63419..bfd8f36 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -27,6 +27,11 @@ export type NodeLine = NodeStyle['line']; export type NodeSize = NodeStyle['size']; export type NodeColor = NodeStyle['color']; export type NodeFill = NodeStyle['fill']; +export type ArrowStartHead = NodeStyle['arrowStartHead']; +export type ArrowEndHead = NodeStyle['arrowEndHead']; + +export type ArrowHead = ArrowEndHead | ArrowStartHead; +export type ArrowHeadDirection = 'start' | 'end'; export type Point = z.infer<(typeof nodeProps)['shape']['point']>;