diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 6b0c91b6f..f155b6bd2 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -491,32 +491,31 @@ export const bindLinearElement = ( return; } - const binding: PointBinding | FixedPointBinding = { + let binding: PointBinding | FixedPointBinding = { elementId: hoveredElement.id, - ...(isElbowArrow(linearElement) - ? { - ...calculateFixedPointForElbowArrowBinding( - linearElement, - hoveredElement, - startOrEnd, - elementsMap, - ), - focus: 0, - gap: 0, - } - : { - ...normalizePointBinding( - calculateFocusAndGap( - linearElement, - hoveredElement, - startOrEnd, - elementsMap, - ), - hoveredElement, - ), - }), + ...normalizePointBinding( + calculateFocusAndGap( + linearElement, + hoveredElement, + startOrEnd, + elementsMap, + ), + hoveredElement, + ), }; + if (isElbowArrow(linearElement)) { + binding = { + ...binding, + ...calculateFixedPointForElbowArrowBinding( + linearElement, + hoveredElement, + startOrEnd, + elementsMap, + ), + }; + } + mutateElement(linearElement, { [startOrEnd === "start" ? "startBinding" : "endBinding"]: binding, }); @@ -1276,39 +1275,35 @@ const updateBoundPoint = ( pointDistance(adjacentPoint, edgePointAbsolute) + pointDistance(adjacentPoint, center) + Math.max(bindableElement.width, bindableElement.height) * 2; - const intersections = intersectElementWithLineSegment( - bindableElement, - lineSegment( - adjacentPoint, - pointFromVector( - vectorScale( - vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)), - interceptorLength, - ), + const intersections = [ + ...intersectElementWithLineSegment( + bindableElement, + lineSegment( adjacentPoint, + pointFromVector( + vectorScale( + vectorNormalize( + vectorFromPoint(focusPointAbsolute, adjacentPoint), + ), + interceptorLength, + ), + adjacentPoint, + ), ), + binding.gap, + ).sort( + (g, h) => + pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint), ), - binding.gap, - ).sort( - (g, h) => - pointDistanceSq(g, adjacentPoint) - pointDistanceSq(h, adjacentPoint), - ); - - // debugClear(); - // debugDrawPoint(intersections[0], { color: "red", permanent: true }); - // debugDrawLine( - // lineSegment( - // adjacentPoint, - // pointFromVector( - // vectorScale( - // vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)), - // interceptorLength, - // ), - // adjacentPoint, - // ), - // ), - // { permanent: true, color: "green" }, - // ); + // Fallback when arrow doesn't point to the shape + pointFromVector( + vectorScale( + vectorNormalize(vectorFromPoint(focusPointAbsolute, adjacentPoint)), + pointDistance(adjacentPoint, edgePointAbsolute), + ), + adjacentPoint, + ), + ]; if (intersections.length > 1) { // The adjacent point is outside the shape (+ gap) @@ -1731,21 +1726,6 @@ const determineFocusDistance = ( ) .sort((g, h) => Math.abs(g) - Math.abs(h)); - // debugClear(); - // [ - // lineSegmentIntersectionPoints(rotatedInterceptor, interceptees[0]), - // lineSegmentIntersectionPoints(rotatedInterceptor, interceptees[1]), - // ] - // .filter((p): p is GlobalPoint => p !== null) - // .forEach((p) => debugDrawPoint(p, { color: "black", permanent: true })); - // debugDrawPoint(determineFocusPoint(element, ordered[0] ?? 0, rotatedA), { - // color: "red", - // permanent: true, - // }); - // debugDrawLine(rotatedInterceptor, { color: "green", permanent: true }); - // debugDrawLine(interceptees[0], { color: "red", permanent: true }); - // debugDrawLine(interceptees[1], { color: "red", permanent: true }); - const signedDistanceRatio = ordered[0] ?? 0; return signedDistanceRatio; diff --git a/packages/element/src/dragElements.ts b/packages/element/src/dragElements.ts index 8881369c3..669417a54 100644 --- a/packages/element/src/dragElements.ts +++ b/packages/element/src/dragElements.ts @@ -13,6 +13,8 @@ import type { import type Scene from "@excalidraw/excalidraw/scene/Scene"; +import type { NonDeletedExcalidrawElement } from "@excalidraw/element/types"; + import { updateBoundElements } from "./binding"; import { getCommonBounds } from "./bounds"; import { mutateElement } from "./mutateElement"; @@ -28,7 +30,7 @@ import { } from "./typeChecks"; import type { Bounds } from "./bounds"; -import type { NonDeletedExcalidrawElement } from "./types"; +import type { ExcalidrawElement } from "./types"; export const dragSelectedElements = ( pointerDownState: PointerDownState, @@ -82,13 +84,20 @@ export const dragSelectedElements = ( } } - const commonBounds = getCommonBounds( - Array.from(elementsToUpdate).map( - (el) => pointerDownState.originalElements.get(el.id) ?? el, - ), - ); + const origElements: ExcalidrawElement[] = []; + + for (const element of elementsToUpdate) { + const origElement = pointerDownState.originalElements.get(element.id); + // if original element is not set (e.g. when you duplicate during a drag + // operation), exit to avoid undefined behavior + if (!origElement) { + return; + } + origElements.push(origElement); + } + const adjustedOffset = calculateOffset( - commonBounds, + getCommonBounds(origElements), offset, snapOffset, gridSize, diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 173ea9b34..3bb225ea7 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -1004,23 +1004,32 @@ export const updateElbowArrowPoints = ( // 0. During all element replacement in the scene, we just need to renormalize // the arrow // TODO (dwelle,mtolmacs): Remove this once Scene.getScene() is removed + const { + startBinding: updatedStartBinding, + endBinding: updatedEndBinding, + ...restOfTheUpdates + } = updates; const startBinding = - typeof updates.startBinding !== "undefined" - ? updates.startBinding + typeof updatedStartBinding !== "undefined" + ? updatedStartBinding : arrow.startBinding; const endBinding = - typeof updates.endBinding !== "undefined" - ? updates.endBinding + typeof updatedEndBinding !== "undefined" + ? updatedEndBinding : arrow.endBinding; const startElement = startBinding && getBindableElementForId(startBinding.elementId, elementsMap); const endElement = endBinding && getBindableElementForId(endBinding.elementId, elementsMap); + if ( + (startBinding && !startElement) || + (endBinding && !endElement) || (elementsMap.size === 0 && validateElbowPoints(updatedPoints)) || - startElement?.id !== startBinding?.elementId || - endElement?.id !== endBinding?.elementId + (Object.keys(restOfTheUpdates).length === 0 && + (startElement?.id !== startBinding?.elementId || + endElement?.id !== endBinding?.elementId)) ) { return normalizeArrowElementUpdate( updatedPoints.map((p) => @@ -1080,7 +1089,8 @@ export const updateElbowArrowPoints = ( p, arrow.points[i] ?? pointFrom(Infinity, Infinity), ), - ) + ) && + validateElbowPoints(updatedPoints) ) { return {}; } diff --git a/packages/excalidraw/actions/actionDuplicateSelection.tsx b/packages/excalidraw/actions/actionDuplicateSelection.tsx index 5e3296ad7..d147a0f38 100644 --- a/packages/excalidraw/actions/actionDuplicateSelection.tsx +++ b/packages/excalidraw/actions/actionDuplicateSelection.tsx @@ -67,6 +67,10 @@ export const actionDuplicateSelection = register({ icon: DuplicateIcon, trackEvent: { category: "element" }, perform: (elements, appState, formData, app) => { + if (appState.selectedElementsAreBeingDragged) { + return false; + } + // duplicate selected point(s) if editing a line if (appState.editingLinearElement) { // TODO: Invariants should be checked here instead of duplicateSelectedPoints() diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 673063d42..5a309b677 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -55,6 +55,8 @@ import { import { hasStrokeColor } from "@excalidraw/element/comparisons"; +import { updateElbowArrowPoints } from "@excalidraw/element/elbowArrow"; + import type { LocalPoint } from "@excalidraw/math"; import type { @@ -1582,7 +1584,7 @@ export const actionChangeArrowType = register({ if (!isArrowElement(el)) { return el; } - const newElement = newElementWith(el, { + let newElement = newElementWith(el, { roundness: value === ARROW_TYPE.round ? { @@ -1597,6 +1599,8 @@ export const actionChangeArrowType = register({ }); if (isElbowArrow(newElement)) { + newElement.fixedSegments = null; + const elementsMap = app.scene.getNonDeletedElementsMap(); app.dismissLinearEditor(); @@ -1671,46 +1675,71 @@ export const actionChangeArrowType = register({ endHoveredElement && bindLinearElement(newElement, endHoveredElement, "end", elementsMap); - mutateElement(newElement, { - points: [finalStartPoint, finalEndPoint].map( - (p): LocalPoint => - pointFrom(p[0] - newElement.x, p[1] - newElement.y), - ), - ...(startElement && newElement.startBinding + const startBinding = + startElement && newElement.startBinding ? { - startBinding: { - // @ts-ignore TS cannot discern check above - ...newElement.startBinding!, - ...calculateFixedPointForElbowArrowBinding( - newElement, - startElement, - "start", - elementsMap, - ), - }, + // @ts-ignore TS cannot discern check above + ...newElement.startBinding!, + ...calculateFixedPointForElbowArrowBinding( + newElement, + startElement, + "start", + elementsMap, + ), } - : {}), - ...(endElement && newElement.endBinding + : null; + const endBinding = + endElement && newElement.endBinding ? { - endBinding: { - // @ts-ignore TS cannot discern check above - ...newElement.endBinding, - ...calculateFixedPointForElbowArrowBinding( - newElement, - endElement, - "end", - elementsMap, - ), - }, + // @ts-ignore TS cannot discern check above + ...newElement.endBinding, + ...calculateFixedPointForElbowArrowBinding( + newElement, + endElement, + "end", + elementsMap, + ), } - : {}), - }); + : null; + + newElement = { + ...newElement, + startBinding, + endBinding, + ...updateElbowArrowPoints(newElement, elementsMap, { + points: [finalStartPoint, finalEndPoint].map( + (p): LocalPoint => + pointFrom(p[0] - newElement.x, p[1] - newElement.y), + ), + startBinding, + endBinding, + fixedSegments: null, + }), + }; LinearElementEditor.updateEditorMidPointsCache( newElement, elementsMap, app.state, ); + } else { + const elementsMap = app.scene.getNonDeletedElementsMap(); + if (newElement.startBinding) { + const startElement = elementsMap.get( + newElement.startBinding.elementId, + ) as ExcalidrawBindableElement; + if (startElement) { + bindLinearElement(newElement, startElement, "start", elementsMap); + } + } + if (newElement.endBinding) { + const endElement = elementsMap.get( + newElement.endBinding.elementId, + ) as ExcalidrawBindableElement; + if (endElement) { + bindLinearElement(newElement, endElement, "end", elementsMap); + } + } } return newElement; diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 165c135fe..d740e975c 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -818,8 +818,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 30, - "width": 50, - "x": 200, + "width": 0, + "x": "149.29289", "y": 0, } `; @@ -852,7 +852,7 @@ History { 0, ], [ - 50, + 0, 0, ], ], @@ -937,7 +937,7 @@ History { 0, ], [ - 50, + 0, 0, ], ],