From 4528854a550cff13d78461afd4456df71b756b6a Mon Sep 17 00:00:00 2001 From: Owais Jamil Date: Thu, 25 Dec 2025 16:10:06 -0600 Subject: [PATCH] fix: arrow handling * added stroke width offset to arrow endpoints * remove bindings when arrows are moved * use resolved endpoints for handle detection --- packages/core/src/geom.ts | 28 +- packages/core/src/tools/select.ts | 85 +++- packages/core/tests/arrow-bindings.test.ts | 483 +++++++++++++++++++++ 3 files changed, 580 insertions(+), 16 deletions(-) create mode 100644 packages/core/tests/arrow-bindings.test.ts diff --git a/packages/core/src/geom.ts b/packages/core/src/geom.ts index 820f3f3..402c602 100644 --- a/packages/core/src/geom.ts +++ b/packages/core/src/geom.ts @@ -510,16 +510,34 @@ export function shapeCenter(shape: ShapeRecord): Vec2 { * @param shape - Target shape * @param nx - Normalized x coordinate in [-1, 1] where -1 is left edge, 1 is right edge, 0 is center * @param ny - Normalized y coordinate in [-1, 1] where -1 is top edge, 1 is bottom edge, 0 is center + * @param offset - Optional offset distance to push the anchor point away from the shape (default: 0) * @returns World coordinates of the anchor point */ -export function computeEdgeAnchor(shape: ShapeRecord, nx: number, ny: number): Vec2 { +export function computeEdgeAnchor(shape: ShapeRecord, nx: number, ny: number, offset = 0): Vec2 { const bounds = shapeBounds(shape); const centerX = (bounds.min.x + bounds.max.x) / 2; const centerY = (bounds.min.y + bounds.max.y) / 2; const halfWidth = (bounds.max.x - bounds.min.x) / 2; const halfHeight = (bounds.max.y - bounds.min.y) / 2; - return { x: centerX + nx * halfWidth, y: centerY + ny * halfHeight }; + const baseX = centerX + nx * halfWidth; + const baseY = centerY + ny * halfHeight; + + if (offset === 0) { + return { x: baseX, y: baseY }; + } + + const dx = baseX - centerX; + const dy = baseY - centerY; + const distance = Math.sqrt(dx * dx + dy * dy); + + if (distance < 0.01) { + return { x: baseX, y: baseY }; + } + + const offsetX = (dx / distance) * offset; + const offsetY = (dy / distance) * offset; + return { x: baseX + offsetX, y: baseY + offsetY }; } /** @@ -565,6 +583,10 @@ export function resolveArrowEndpoints(state: EditorState, arrowId: string): { a: let a: Vec2 = { x: arrow.x + firstPoint.x, y: arrow.y + firstPoint.y }; let b: Vec2 = { x: arrow.x + lastPoint.x, y: arrow.y + lastPoint.y }; + const arrowStrokeWidth = arrow.props.style?.width ?? 2; + const targetShapeStrokeWidth = 2; + const offset = targetShapeStrokeWidth / 2 + arrowStrokeWidth / 2; + for (const binding of Object.values(state.doc.bindings)) { if (binding.fromShapeId !== arrowId) continue; @@ -575,7 +597,7 @@ export function resolveArrowEndpoints(state: EditorState, arrowId: string): { a: if (binding.anchor.kind === "center") { anchorPoint = shapeCenter(targetShape); } else { - anchorPoint = computeEdgeAnchor(targetShape, binding.anchor.nx, binding.anchor.ny); + anchorPoint = computeEdgeAnchor(targetShape, binding.anchor.nx, binding.anchor.ny, offset); } if (binding.handle === "start") { diff --git a/packages/core/src/tools/select.ts b/packages/core/src/tools/select.ts index 7a53562..1343354 100644 --- a/packages/core/src/tools/select.ts +++ b/packages/core/src/tools/select.ts @@ -1,5 +1,12 @@ import type { Action } from "../actions"; -import { computeNormalizedAnchor, computePolylineLength, getPointAtDistance, hitTestPoint, shapeBounds } from "../geom"; +import { + computeNormalizedAnchor, + computePolylineLength, + getPointAtDistance, + hitTestPoint, + resolveArrowEndpoints, + shapeBounds, +} from "../geom"; import { Box2, type Vec2, Vec2 as Vec2Ops } from "../math"; import { BindingRecord, ShapeRecord } from "../model"; import { EditorState, getCurrentPage, type ToolId } from "../reactivity"; @@ -140,7 +147,7 @@ export class SelectTool implements Tool { if (!shape) { return null; } - const handles = this.getHandlePositions(shape); + const handles = this.getHandlePositions(state, shape); for (const handle of handles) { if (Vec2Ops.dist(point, handle.position) <= HANDLE_HIT_RADIUS) { return { handle: handle.id, shape }; @@ -348,6 +355,10 @@ export class SelectTool implements Tool { newState = this.completeMarqueeSelection(state); } + if (this.toolState.isDragging && !this.toolState.activeHandle) { + newState = this.removeBindingsForMovedArrows(newState); + } + if ( this.toolState.handleShapeId && (this.toolState.activeHandle === "line-start" || this.toolState.activeHandle === "line-end") @@ -506,7 +517,7 @@ export class SelectTool implements Tool { return this.toolState.activeHandle; } - private getHandlePositions(shape: ShapeRecord): Array<{ id: HandleKind; position: Vec2 }> { + private getHandlePositions(state: EditorState, shape: ShapeRecord): Array<{ id: HandleKind; position: Vec2 }> { const handles: Array<{ id: HandleKind; position: Vec2 }> = []; if (shape.type === "rect" || shape.type === "ellipse" || shape.type === "text") { const bounds = shapeBounds(shape); @@ -532,20 +543,18 @@ export class SelectTool implements Tool { const end = this.localToWorld(shape, shape.props.b); handles.push({ id: "line-start", position: start }, { id: "line-end", position: end }); } else if (shape.type === "arrow") { - if (shape.props.points && shape.props.points.length >= 2) { - for (let i = 0; i < shape.props.points.length; i++) { + const resolved = resolveArrowEndpoints(state, shape.id); + if (resolved && shape.props.points && shape.props.points.length >= 2) { + handles.push({ id: "line-start", position: resolved.a }); + + for (let i = 1; i < shape.props.points.length - 1; i++) { const point = shape.props.points[i]; const worldPos = this.localToWorld(shape, point); - - if (i === 0) { - handles.push({ id: "line-start", position: worldPos }); - } else if (i === shape.props.points.length - 1) { - handles.push({ id: "line-end", position: worldPos }); - } else { - handles.push({ id: `arrow-point-${i}` as HandleKind, position: worldPos }); - } + handles.push({ id: `arrow-point-${i}` as HandleKind, position: worldPos }); } + handles.push({ id: "line-end", position: resolved.b }); + if (shape.props.label) { const polylineLength = computePolylineLength(shape.props.points); const align = shape.props.label.align ?? "center"; @@ -859,6 +868,56 @@ export class SelectTool implements Tool { return null; } + /** + * Remove bindings for arrows that were moved with the select tool + * + * When an arrow is moved (not just its endpoints), its bindings should be removed + * to prevent the endpoints from snapping back to the old binding positions. + */ + private removeBindingsForMovedArrows(state: EditorState): EditorState { + const movedArrowIds = Array.from(this.toolState.initialShapePositions.keys()).filter((shapeId) => { + const shape = state.doc.shapes[shapeId]; + return shape && shape.type === "arrow"; + }); + + if (movedArrowIds.length === 0) { + return state; + } + + const newBindings = { ...state.doc.bindings }; + const newShapes = { ...state.doc.shapes }; + let bindingsRemoved = false; + + for (const arrowId of movedArrowIds) { + const arrow = newShapes[arrowId]; + if (!arrow || arrow.type !== "arrow") continue; + + for (const [bindingId, binding] of Object.entries(newBindings)) { + if (binding.fromShapeId === arrowId) { + delete newBindings[bindingId]; + bindingsRemoved = true; + + console.log("[Arrow Movement Fix] Removing binding", { + arrowId, + bindingId, + handle: binding.handle, + targetShapeId: binding.toShapeId, + }); + } + } + + if (bindingsRemoved) { + newShapes[arrowId] = { ...arrow, props: { ...arrow.props, start: { kind: "free" }, end: { kind: "free" } } }; + } + } + + if (!bindingsRemoved) { + return state; + } + + return { ...state, doc: { ...state.doc, shapes: newShapes, bindings: newBindings } }; + } + /** * Update arrow bindings when an endpoint is dragged * diff --git a/packages/core/tests/arrow-bindings.test.ts b/packages/core/tests/arrow-bindings.test.ts new file mode 100644 index 0000000..6451de9 --- /dev/null +++ b/packages/core/tests/arrow-bindings.test.ts @@ -0,0 +1,483 @@ +import { describe, expect, it } from "vitest"; +import { Action } from "../src/actions"; +import { resolveArrowEndpoints } from "../src/geom"; +import { BindingRecord, PageRecord, ShapeRecord } from "../src/model"; +import { EditorState } from "../src/reactivity"; +import { SelectTool } from "../src/tools/select"; + +describe("Arrow binding behavior", () => { + describe("Issue #2: Moving arrows with select tool", () => { + it("should remove bindings when an arrow is moved (dragged)", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rectStart = ShapeRecord.createRect(page.id, 50, 50, { + w: 50, + h: 50, + fill: "#fff", + stroke: "#000", + radius: 0, + }); + + const rectEnd = ShapeRecord.createRect(page.id, 250, 50, { + w: 50, + h: 50, + fill: "#fff", + stroke: "#000", + radius: 0, + }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 75, { + points: [{ x: 0, y: 0 }, { x: 150, y: 0 }], + start: { kind: "bound", bindingId: "binding-start" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 2, headEnd: true }, + }); + + const bindingStart = BindingRecord.create( + arrow.id, + rectStart.id, + "start", + { kind: "edge", nx: 1, ny: 0 }, + "binding-start", + ); + + const bindingEnd = BindingRecord.create( + arrow.id, + rectEnd.id, + "end", + { kind: "edge", nx: -1, ny: 0 }, + "binding-end", + ); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rectStart.id]: rectStart, [rectEnd.id]: rectEnd }, + bindings: { [bindingStart.id]: bindingStart, [bindingEnd.id]: bindingEnd }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rectStart.id, rectEnd.id, arrow.id] } }, + }, + ui: { ...state.ui, selectionIds: [arrow.id] }, + }; + + expect(Object.keys(state.doc.bindings).length).toBe(2); + expect(state.doc.bindings[bindingStart.id]).toBeDefined(); + expect(state.doc.bindings[bindingEnd.id]).toBeDefined(); + + const tool = new SelectTool(); + tool.onEnter(state); + + const clickWorld = { x: 175, y: 75 }; + const pointerDown = Action.pointerDown( + { x: 0, y: 0 }, + clickWorld, + 0, + { left: true, middle: false, right: false }, + { ctrl: false, shift: false, alt: false, meta: false }, + 0, + ); + state = tool.onAction(state, pointerDown); + + const newWorld = { x: 175, y: 150 }; + const pointerMove = Action.pointerMove({ x: 0, y: 0 }, newWorld, { left: true, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 100); + state = tool.onAction(state, pointerMove); + + const pointerUp = Action.pointerUp({ x: 0, y: 0 }, newWorld, 0, { left: false, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 200); + state = tool.onAction(state, pointerUp); + + expect(Object.keys(state.doc.bindings).length).toBe(0); + expect(state.doc.bindings[bindingStart.id]).toBeUndefined(); + expect(state.doc.bindings[bindingEnd.id]).toBeUndefined(); + + const updatedArrow = state.doc.shapes[arrow.id]; + expect(updatedArrow.type).toBe("arrow"); + if (updatedArrow.type === "arrow") { + expect(updatedArrow.props.start.kind).toBe("free"); + expect(updatedArrow.props.end.kind).toBe("free"); + } + }); + + it("should NOT remove bindings when dragging an endpoint handle", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rect = ShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 75, { + points: [{ x: 0, y: 0 }, { x: 150, y: 0 }], + start: { kind: "free" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 2, headEnd: true }, + }); + + const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rect.id]: rect }, + bindings: { [binding.id]: binding }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rect.id, arrow.id] } }, + }, + ui: { ...state.ui, selectionIds: [arrow.id] }, + }; + + const tool = new SelectTool(); + tool.onEnter(state); + + const resolved = resolveArrowEndpoints(state, arrow.id); + expect(resolved).not.toBeNull(); + + const endHandleWorld = resolved!.b; + const pointerDown = Action.pointerDown( + { x: 0, y: 0 }, + endHandleWorld, + 0, + { left: true, middle: false, right: false }, + { ctrl: false, shift: false, alt: false, meta: false }, + 0, + ); + state = tool.onAction(state, pointerDown); + + const newWorld = { x: endHandleWorld.x + 50, y: endHandleWorld.y }; + const pointerMove = Action.pointerMove({ x: 0, y: 0 }, newWorld, { left: true, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 100); + state = tool.onAction(state, pointerMove); + + const pointerUp = Action.pointerUp({ x: 0, y: 0 }, newWorld, 0, { left: false, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 200); + state = tool.onAction(state, pointerUp); + + const updatedArrow = state.doc.shapes[arrow.id]; + expect(updatedArrow.type).toBe("arrow"); + }); + }); + + describe("Issue #3: Arrow endpoint handle detection with bindings", () => { + it("should be able to click and drag arrow endpoint handles when arrow is bound", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rect = ShapeRecord.createRect(page.id, 250, 50, { w: 50, h: 50, fill: "#fff", stroke: "#000", radius: 0 }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 75, { + points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], + start: { kind: "free" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 2, headEnd: true }, + }); + + const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rect.id]: rect }, + bindings: { [binding.id]: binding }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rect.id, arrow.id] } }, + }, + ui: { ...state.ui, selectionIds: [arrow.id] }, + }; + + const tool = new SelectTool(); + tool.onEnter(state); + + const resolved = resolveArrowEndpoints(state, arrow.id); + expect(resolved).not.toBeNull(); + const resolvedEndPos = resolved!.b; + + expect(resolvedEndPos.x).toBeCloseTo(275, 0); + expect(resolvedEndPos.y).toBeCloseTo(75, 0); + + const pointerDown = Action.pointerDown( + { x: 0, y: 0 }, + resolvedEndPos, + 0, + { left: true, middle: false, right: false }, + { ctrl: false, shift: false, alt: false, meta: false }, + 0, + ); + state = tool.onAction(state, pointerDown); + + const activeHandle = tool.getActiveHandle(); + expect(activeHandle).toBe("line-end"); + + const newWorld = { x: 300, y: 100 }; + const pointerMove = Action.pointerMove({ x: 0, y: 0 }, newWorld, { left: true, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 100); + state = tool.onAction(state, pointerMove); + + const updatedArrow = state.doc.shapes[arrow.id]; + expect(updatedArrow.type).toBe("arrow"); + if (updatedArrow.type === "arrow") { + expect(updatedArrow.props.points[0]).toEqual({ x: 0, y: 0 }); + + expect(updatedArrow.props.points[updatedArrow.props.points.length - 1].x).toBeGreaterThan(0); + } + }); + }); + + describe("Issue #1: Arrow endpoint offset from bound shapes", () => { + it("should position arrow endpoints with offset to account for stroke widths", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rect = ShapeRecord.createRect(page.id, 200, 100, { + w: 100, + h: 100, + fill: "#fff", + stroke: "#000", + radius: 0, + }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 150, { + points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], + start: { kind: "free" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 2, headEnd: true }, + }); + + const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rect.id]: rect }, + bindings: { [binding.id]: binding }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rect.id, arrow.id] } }, + }, + }; + + const resolved = resolveArrowEndpoints(state, arrow.id); + expect(resolved).not.toBeNull(); + + const expectedX = 200 - 2; + const expectedY = 150; + + expect(resolved!.b.x).toBeCloseTo(expectedX, 0); + expect(resolved!.b.y).toBeCloseTo(expectedY, 0); + }); + + it("should apply offset for arrows with different stroke widths", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rect = ShapeRecord.createRect(page.id, 200, 100, { + w: 100, + h: 100, + fill: "#fff", + stroke: "#000", + radius: 0, + }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 150, { + points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], + start: { kind: "free" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 4, headEnd: true }, + }); + + const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "edge", nx: -1, ny: 0 }, "binding-end"); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rect.id]: rect }, + bindings: { [binding.id]: binding }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rect.id, arrow.id] } }, + }, + }; + + const resolved = resolveArrowEndpoints(state, arrow.id); + expect(resolved).not.toBeNull(); + + const expectedX = 200 - 3; + const expectedY = 150; + + expect(resolved!.b.x).toBeCloseTo(expectedX, 0); + expect(resolved!.b.y).toBeCloseTo(expectedY, 0); + }); + + it("should not apply offset for center anchors", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rect = ShapeRecord.createRect(page.id, 200, 100, { + w: 100, + h: 100, + fill: "#fff", + stroke: "#000", + radius: 0, + }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 150, { + points: [{ x: 0, y: 0 }, { x: 100, y: 0 }], + start: { kind: "free" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 2, headEnd: true }, + }); + + const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rect.id]: rect }, + bindings: { [binding.id]: binding }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rect.id, arrow.id] } }, + }, + }; + + const resolved = resolveArrowEndpoints(state, arrow.id); + expect(resolved).not.toBeNull(); + + expect(resolved!.b.x).toBeCloseTo(250, 0); + expect(resolved!.b.y).toBeCloseTo(150, 0); + }); + }); + + describe("Regression: Arrow endpoint manipulation", () => { + it("should preserve intermediate points when dragging bound endpoints", () => { + let state = EditorState.create(); + + const page = PageRecord.create("Test Page"); + state = { + ...state, + doc: { ...state.doc, pages: { [page.id]: page } }, + ui: { ...state.ui, currentPageId: page.id }, + }; + + const rect = ShapeRecord.createRect(page.id, 300, 100, { + w: 100, + h: 100, + fill: "#fff", + stroke: "#000", + radius: 0, + }); + + const arrow = ShapeRecord.createArrow(page.id, 100, 100, { + points: [{ x: 0, y: 0 }, { x: 100, y: 50 }, { x: 200, y: 0 }], + start: { kind: "free" }, + end: { kind: "bound", bindingId: "binding-end" }, + style: { stroke: "#000", width: 2, headEnd: true }, + }); + + const binding = BindingRecord.create(arrow.id, rect.id, "end", { kind: "center" }, "binding-end"); + + state = { + ...state, + doc: { + ...state.doc, + shapes: { ...state.doc.shapes, [arrow.id]: arrow, [rect.id]: rect }, + bindings: { [binding.id]: binding }, + pages: { ...state.doc.pages, [page.id]: { ...page, shapeIds: [rect.id, arrow.id] } }, + }, + ui: { ...state.ui, selectionIds: [arrow.id] }, + }; + + const tool = new SelectTool(); + tool.onEnter(state); + + const resolved = resolveArrowEndpoints(state, arrow.id); + expect(resolved).not.toBeNull(); + + const startPos = resolved!.a; + const pointerDown = Action.pointerDown({ x: 0, y: 0 }, startPos, 0, { left: true, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 0); + state = tool.onAction(state, pointerDown); + + const newPos = { x: startPos.x - 50, y: startPos.y }; + const pointerMove = Action.pointerMove({ x: 0, y: 0 }, newPos, { left: true, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 100); + state = tool.onAction(state, pointerMove); + + const pointerUp = Action.pointerUp({ x: 0, y: 0 }, newPos, 0, { left: false, middle: false, right: false }, { + ctrl: false, + shift: false, + alt: false, + meta: false, + }, 200); + state = tool.onAction(state, pointerUp); + + const updatedArrow = state.doc.shapes[arrow.id]; + expect(updatedArrow.type).toBe("arrow"); + if (updatedArrow.type === "arrow") { + expect(updatedArrow.props.points.length).toBe(3); + + expect(updatedArrow.props.points[1]).toBeDefined(); + } + }); + }); +}); -- 2.51.2