From ca1a4f25e70d90ff2a04851c1951f671b65258a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Sat, 7 Jun 2025 12:56:32 +0200 Subject: [PATCH 01/22] feat: Precise hit testing (#9488) --- packages/common/src/utils.ts | 11 +- packages/element/src/Shape.ts | 187 ++- packages/element/src/binding.ts | 269 +++-- packages/element/src/bounds.ts | 38 +- packages/element/src/collision.ts | 450 ++++--- packages/element/src/cropElement.ts | 3 +- packages/element/src/distance.ts | 46 +- packages/element/src/elbowArrow.ts | 79 +- packages/element/src/flowchart.ts | 9 +- packages/element/src/shapes.ts | 3 +- packages/element/src/types.ts | 3 +- packages/element/src/utils.ts | 553 +++++---- packages/element/tests/align.test.tsx | 12 +- packages/element/tests/binding.test.tsx | 4 +- packages/element/tests/collision.test.tsx | 38 + .../tests/linearElementEditor.test.tsx | 2 +- packages/element/tests/resize.test.tsx | 12 +- packages/element/tests/sizeHelpers.test.ts | 11 - .../excalidraw/actions/actionProperties.tsx | 74 +- packages/excalidraw/appState.ts | 3 +- packages/excalidraw/components/App.tsx | 204 ++-- .../components/Stats/stats.test.tsx | 2 +- .../components/hyperlink/Hyperlink.tsx | 2 +- .../components/hyperlink/helpers.ts | 2 +- .../data/__snapshots__/transform.test.ts.snap | 6 +- packages/excalidraw/data/transform.test.ts | 2 +- packages/excalidraw/eraser/index.ts | 109 +- packages/excalidraw/lasso/index.ts | 1 + packages/excalidraw/lasso/utils.ts | 84 +- packages/excalidraw/renderer/helpers.ts | 256 ++-- .../excalidraw/renderer/interactiveScene.ts | 22 +- .../__snapshots__/contextmenu.test.tsx.snap | 382 +++--- .../__snapshots__/dragCreate.test.tsx.snap | 16 +- .../tests/__snapshots__/export.test.tsx.snap | 4 +- .../tests/__snapshots__/history.test.tsx.snap | 1050 +++++------------ .../tests/__snapshots__/move.test.tsx.snap | 32 +- .../multiPointCreate.test.tsx.snap | 4 +- .../regressionTests.test.tsx.snap | 484 +++----- .../__snapshots__/selection.test.tsx.snap | 16 +- .../excalidraw/tests/contextmenu.test.tsx | 20 +- .../data/__snapshots__/restore.test.ts.snap | 24 +- packages/excalidraw/tests/helpers/ui.ts | 26 +- packages/excalidraw/tests/lasso.test.tsx | 1 + packages/excalidraw/tests/move.test.tsx | 4 +- packages/excalidraw/tests/rotate.test.tsx | 2 +- .../excalidraw/wysiwyg/textWysiwyg.test.tsx | 6 +- packages/math/src/curve.ts | 131 +- packages/math/src/index.ts | 1 + packages/math/src/vector.ts | 14 +- packages/utils/src/collision.ts | 135 --- .../tests/__snapshots__/export.test.ts.snap | 2 +- packages/utils/tests/collision.test.ts | 90 -- 52 files changed, 2223 insertions(+), 2718 deletions(-) create mode 100644 packages/element/tests/collision.test.tsx delete mode 100644 packages/utils/src/collision.ts delete mode 100644 packages/utils/tests/collision.test.ts diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 707e7292e3..39e2e9149c 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -1,10 +1,12 @@ import { average, pointFrom, type GlobalPoint } from "@excalidraw/math"; +import { getCenterForBounds, getElementBounds } from "@excalidraw/element"; import type { ExcalidrawBindableElement, FontFamilyValues, FontString, ExcalidrawElement, + ElementsMap, } from "@excalidraw/element/types"; import type { @@ -1240,16 +1242,13 @@ export const castArray = (value: T | T[]): T[] => export const elementCenterPoint = ( element: ExcalidrawElement, + elementsMap: ElementsMap, xOffset: number = 0, yOffset: number = 0, ) => { - const { x, y, width, height } = element; + const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap)); - const centerXPoint = x + width / 2 + xOffset; - - const centerYPoint = y + height / 2 + yOffset; - - return pointFrom(centerXPoint, centerYPoint); + return pointFrom(x + xOffset, y + yOffset); }; /** hack for Array.isArray type guard not working with readonly value[] */ diff --git a/packages/element/src/Shape.ts b/packages/element/src/Shape.ts index 4def419574..317dfbacb8 100644 --- a/packages/element/src/Shape.ts +++ b/packages/element/src/Shape.ts @@ -1,8 +1,17 @@ import { simplify } from "points-on-curve"; -import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math"; +import { + pointFrom, + pointDistance, + type LocalPoint, + pointRotateRads, +} from "@excalidraw/math"; import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common"; +import { RoughGenerator } from "roughjs/bin/generator"; + +import type { GlobalPoint } from "@excalidraw/math"; + import type { Mutable } from "@excalidraw/common/utility-types"; import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types"; @@ -20,7 +29,12 @@ import { headingForPointIsHorizontal } from "./heading"; import { canChangeRoundness } from "./comparisons"; import { generateFreeDrawShape } from "./renderElement"; -import { getArrowheadPoints, getDiamondPoints } from "./bounds"; +import { + getArrowheadPoints, + getCenterForBounds, + getDiamondPoints, + getElementBounds, +} from "./bounds"; import type { ExcalidrawElement, @@ -28,10 +42,11 @@ import type { ExcalidrawSelectionElement, ExcalidrawLinearElement, Arrowhead, + ExcalidrawFreeDrawElement, + ElementsMap, } from "./types"; import type { Drawable, Options } from "roughjs/bin/core"; -import type { RoughGenerator } from "roughjs/bin/generator"; import type { Point as RoughPoint } from "roughjs/bin/geometry"; const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; @@ -303,6 +318,172 @@ const getArrowheadShapes = ( } }; +export const generateLinearCollisionShape = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, + elementsMap: ElementsMap, +) => { + const generator = new RoughGenerator(); + const options: Options = { + seed: element.seed, + disableMultiStroke: true, + disableMultiStrokeFill: true, + roughness: 0, + preserveVertices: true, + }; + const center = getCenterForBounds( + getElementBounds(element, elementsMap, true), + ); + + switch (element.type) { + case "line": + case "arrow": { + // points array can be empty in the beginning, so it is important to add + // initial position to it + const points = element.points.length + ? element.points + : [pointFrom(0, 0)]; + + if (isElbowArrow(element)) { + return generator.path(generateElbowArrowShape(points, 16), options) + .sets[0].ops; + } else if (!element.roundness) { + return points.map((point, idx) => { + const p = pointRotateRads( + pointFrom(element.x + point[0], element.y + point[1]), + center, + element.angle, + ); + + return { + op: idx === 0 ? "move" : "lineTo", + data: pointFrom(p[0] - element.x, p[1] - element.y), + }; + }); + } + + return generator + .curve(points as unknown as RoughPoint[], options) + .sets[0].ops.slice(0, element.points.length) + .map((op, i) => { + if (i === 0) { + const p = pointRotateRads( + pointFrom( + element.x + op.data[0], + element.y + op.data[1], + ), + center, + element.angle, + ); + + return { + op: "move", + data: pointFrom(p[0] - element.x, p[1] - element.y), + }; + } + + return { + op: "bcurveTo", + data: [ + pointRotateRads( + pointFrom( + element.x + op.data[0], + element.y + op.data[1], + ), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + op.data[2], + element.y + op.data[3], + ), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + op.data[4], + element.y + op.data[5], + ), + center, + element.angle, + ), + ] + .map((p) => + pointFrom(p[0] - element.x, p[1] - element.y), + ) + .flat(), + }; + }); + } + case "freedraw": { + if (element.points.length < 2) { + return []; + } + + const simplifiedPoints = simplify( + element.points as Mutable, + 0.75, + ); + + return generator + .curve(simplifiedPoints as [number, number][], options) + .sets[0].ops.slice(0, element.points.length) + .map((op, i) => { + if (i === 0) { + const p = pointRotateRads( + pointFrom( + element.x + op.data[0], + element.y + op.data[1], + ), + center, + element.angle, + ); + + return { + op: "move", + data: pointFrom(p[0] - element.x, p[1] - element.y), + }; + } + + return { + op: "bcurveTo", + data: [ + pointRotateRads( + pointFrom( + element.x + op.data[0], + element.y + op.data[1], + ), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + op.data[2], + element.y + op.data[3], + ), + center, + element.angle, + ), + pointRotateRads( + pointFrom( + element.x + op.data[4], + element.y + op.data[5], + ), + center, + element.angle, + ), + ] + .map((p) => + pointFrom(p[0] - element.x, p[1] - element.y), + ) + .flat(), + }; + }); + } + } +}; + /** * Generates the roughjs shape for given element. * diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 2ea05510b4..0a7a4a68ae 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -27,8 +27,6 @@ import { PRECISION, } from "@excalidraw/math"; -import { isPointOnShape } from "@excalidraw/utils/collision"; - import type { LocalPoint, Radians } from "@excalidraw/math"; import type { AppState } from "@excalidraw/excalidraw/types"; @@ -41,7 +39,7 @@ import { doBoundsIntersect, } from "./bounds"; import { intersectElementWithLineSegment } from "./collision"; -import { distanceToBindableElement } from "./distance"; +import { distanceToElement } from "./distance"; import { headingForPointFromElement, headingIsHorizontal, @@ -63,7 +61,7 @@ import { isTextElement, } from "./typeChecks"; -import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; +import { aabbForElement } from "./shapes"; import { updateElbowArrowPoints } from "./elbowArrow"; import type { Scene } from "./Scene"; @@ -109,7 +107,6 @@ export const isBindingEnabled = (appState: AppState): boolean => { export const FIXED_BINDING_DISTANCE = 5; export const BINDING_HIGHLIGHT_THICKNESS = 10; -export const BINDING_HIGHLIGHT_OFFSET = 4; const getNonDeletedElements = ( scene: Scene, @@ -131,6 +128,7 @@ export const bindOrUnbindLinearElement = ( endBindingElement: ExcalidrawBindableElement | null | "keep", scene: Scene, ): void => { + const elementsMap = scene.getNonDeletedElementsMap(); const boundToElementIds: Set = new Set(); const unboundFromElementIds: Set = new Set(); bindOrUnbindLinearElementEdge( @@ -141,6 +139,7 @@ export const bindOrUnbindLinearElement = ( boundToElementIds, unboundFromElementIds, scene, + elementsMap, ); bindOrUnbindLinearElementEdge( linearElement, @@ -150,6 +149,7 @@ export const bindOrUnbindLinearElement = ( boundToElementIds, unboundFromElementIds, scene, + elementsMap, ); const onlyUnbound = Array.from(unboundFromElementIds).filter( @@ -176,6 +176,7 @@ const bindOrUnbindLinearElementEdge = ( // Is mutated unboundFromElementIds: Set, scene: Scene, + elementsMap: ElementsMap, ): void => { // "keep" is for method chaining convenience, a "no-op", so just bail out if (bindableElement === "keep") { @@ -216,43 +217,29 @@ const bindOrUnbindLinearElementEdge = ( } }; -const getOriginalBindingIfStillCloseOfLinearElementEdge = ( - linearElement: NonDeleted, - edge: "start" | "end", - elementsMap: NonDeletedSceneElementsMap, - zoom?: AppState["zoom"], -): NonDeleted | null => { - const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap); - const elementId = - edge === "start" - ? linearElement.startBinding?.elementId - : linearElement.endBinding?.elementId; - if (elementId) { - const element = elementsMap.get(elementId); - if ( - isBindableElement(element) && - bindingBorderTest(element, coors, elementsMap, zoom) - ) { - return element; - } - } - - return null; -}; - const getOriginalBindingsIfStillCloseToArrowEnds = ( linearElement: NonDeleted, elementsMap: NonDeletedSceneElementsMap, zoom?: AppState["zoom"], ): (NonDeleted | null)[] => - ["start", "end"].map((edge) => - getOriginalBindingIfStillCloseOfLinearElementEdge( - linearElement, - edge as "start" | "end", - elementsMap, - zoom, - ), - ); + (["start", "end"] as const).map((edge) => { + const coors = getLinearElementEdgeCoors(linearElement, edge, elementsMap); + const elementId = + edge === "start" + ? linearElement.startBinding?.elementId + : linearElement.endBinding?.elementId; + if (elementId) { + const element = elementsMap.get(elementId); + if ( + isBindableElement(element) && + bindingBorderTest(element, coors, elementsMap, zoom) + ) { + return element; + } + } + + return null; + }); const getBindingStrategyForDraggingArrowEndpoints = ( selectedElement: NonDeleted, @@ -268,7 +255,7 @@ const getBindingStrategyForDraggingArrowEndpoints = ( const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1; const start = startDragged ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "start", elementsMap, @@ -279,7 +266,7 @@ const getBindingStrategyForDraggingArrowEndpoints = ( : "keep"; const end = endDragged ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "end", elementsMap, @@ -311,7 +298,7 @@ const getBindingStrategyForDraggingArrowOrJoints = ( ); const start = startIsClose ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "start", elementsMap, @@ -322,7 +309,7 @@ const getBindingStrategyForDraggingArrowOrJoints = ( : null; const end = endIsClose ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "end", elementsMap, @@ -441,22 +428,13 @@ export const maybeBindLinearElement = ( const normalizePointBinding = ( binding: { focus: number; gap: number }, hoveredElement: ExcalidrawBindableElement, -) => { - let gap = binding.gap; - const maxGap = maxBindingGap( - hoveredElement, - hoveredElement.width, - hoveredElement.height, - ); - - if (gap > maxGap) { - gap = BINDING_HIGHLIGHT_THICKNESS + BINDING_HIGHLIGHT_OFFSET; - } - return { - ...binding, - gap, - }; -}; +) => ({ + ...binding, + gap: Math.min( + binding.gap, + maxBindingGap(hoveredElement, hoveredElement.width, hoveredElement.height), + ), +}); export const bindLinearElement = ( linearElement: NonDeleted, @@ -488,6 +466,7 @@ export const bindLinearElement = ( linearElement, hoveredElement, startOrEnd, + scene.getNonDeletedElementsMap(), ), }; } @@ -703,8 +682,13 @@ const calculateFocusAndGap = ( ); return { - focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), - gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)), + focus: determineFocusDistance( + hoveredElement, + elementsMap, + adjacentPoint, + edgePoint, + ), + gap: Math.max(1, distanceToElement(hoveredElement, elementsMap, edgePoint)), }; }; @@ -874,6 +858,7 @@ export const getHeadingForElbowArrowSnap = ( bindableElement: ExcalidrawBindableElement | undefined | null, aabb: Bounds | undefined | null, origPoint: GlobalPoint, + elementsMap: ElementsMap, zoom?: AppState["zoom"], ): Heading => { const otherPointHeading = vectorToHeading(vectorFromPoint(otherPoint, p)); @@ -882,11 +867,16 @@ export const getHeadingForElbowArrowSnap = ( return otherPointHeading; } - const distance = getDistanceForBinding(origPoint, bindableElement, zoom); + const distance = getDistanceForBinding( + origPoint, + bindableElement, + elementsMap, + zoom, + ); if (!distance) { return vectorToHeading( - vectorFromPoint(p, elementCenterPoint(bindableElement)), + vectorFromPoint(p, elementCenterPoint(bindableElement, elementsMap)), ); } @@ -896,9 +886,10 @@ export const getHeadingForElbowArrowSnap = ( const getDistanceForBinding = ( point: Readonly, bindableElement: ExcalidrawBindableElement, + elementsMap: ElementsMap, zoom?: AppState["zoom"], ) => { - const distance = distanceToBindableElement(bindableElement, point); + const distance = distanceToElement(bindableElement, elementsMap, point); const bindDistance = maxBindingGap( bindableElement, bindableElement.width, @@ -913,12 +904,13 @@ export const bindPointToSnapToElementOutline = ( arrow: ExcalidrawElbowArrowElement, bindableElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): GlobalPoint => { if (isDevEnv() || isTestEnv()) { invariant(arrow.points.length > 1, "Arrow should have at least 2 points"); } - const aabb = aabbForElement(bindableElement); + const aabb = aabbForElement(bindableElement, elementsMap); const localP = arrow.points[startOrEnd === "start" ? 0 : arrow.points.length - 1]; const globalP = pointFrom( @@ -926,7 +918,7 @@ export const bindPointToSnapToElementOutline = ( arrow.y + localP[1], ); const edgePoint = isRectanguloidElement(bindableElement) - ? avoidRectangularCorner(bindableElement, globalP) + ? avoidRectangularCorner(bindableElement, elementsMap, globalP) : globalP; const elbowed = isElbowArrow(arrow); const center = getCenterForBounds(aabb); @@ -945,26 +937,31 @@ export const bindPointToSnapToElementOutline = ( const isHorizontal = headingIsHorizontal( headingForPointFromElement(bindableElement, aabb, globalP), ); + const snapPoint = snapToMid(bindableElement, elementsMap, edgePoint); const otherPoint = pointFrom( - isHorizontal ? center[0] : edgePoint[0], - !isHorizontal ? center[1] : edgePoint[1], + isHorizontal ? center[0] : snapPoint[0], + !isHorizontal ? center[1] : snapPoint[1], + ); + const intersector = lineSegment( + otherPoint, + pointFromVector( + vectorScale( + vectorNormalize(vectorFromPoint(snapPoint, otherPoint)), + Math.max(bindableElement.width, bindableElement.height) * 2, + ), + otherPoint, + ), ); intersection = intersectElementWithLineSegment( bindableElement, - lineSegment( - otherPoint, - pointFromVector( - vectorScale( - vectorNormalize(vectorFromPoint(edgePoint, otherPoint)), - Math.max(bindableElement.width, bindableElement.height) * 2, - ), - otherPoint, - ), - ), - )[0]; + elementsMap, + intersector, + FIXED_BINDING_DISTANCE, + ).sort(pointDistanceSq)[0]; } else { intersection = intersectElementWithLineSegment( bindableElement, + elementsMap, lineSegment( adjacentPoint, pointFromVector( @@ -991,31 +988,15 @@ export const bindPointToSnapToElementOutline = ( return edgePoint; } - if (elbowed) { - const scalar = - pointDistanceSq(edgePoint, center) - - pointDistanceSq(intersection, center) > - 0 - ? FIXED_BINDING_DISTANCE - : -FIXED_BINDING_DISTANCE; - - return pointFromVector( - vectorScale( - vectorNormalize(vectorFromPoint(edgePoint, intersection)), - scalar, - ), - intersection, - ); - } - - return edgePoint; + return elbowed ? intersection : edgePoint; }; export const avoidRectangularCorner = ( element: ExcalidrawBindableElement, + elementsMap: ElementsMap, p: GlobalPoint, ): GlobalPoint => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); const nonRotatedPoint = pointRotateRads(p, center, -element.angle as Radians); if (nonRotatedPoint[0] < element.x && nonRotatedPoint[1] < element.y) { @@ -1108,35 +1089,34 @@ export const avoidRectangularCorner = ( export const snapToMid = ( element: ExcalidrawBindableElement, + elementsMap: ElementsMap, p: GlobalPoint, tolerance: number = 0.05, ): GlobalPoint => { const { x, y, width, height, angle } = element; - - const center = elementCenterPoint(element, -0.1, -0.1); - + const center = elementCenterPoint(element, elementsMap, -0.1, -0.1); const nonRotated = pointRotateRads(p, center, -angle as Radians); // snap-to-center point is adaptive to element size, but we don't want to go // above and below certain px distance - const verticalThrehsold = clamp(tolerance * height, 5, 80); - const horizontalThrehsold = clamp(tolerance * width, 5, 80); + const verticalThreshold = clamp(tolerance * height, 5, 80); + const horizontalThreshold = clamp(tolerance * width, 5, 80); if ( nonRotated[0] <= x + width / 2 && - nonRotated[1] > center[1] - verticalThrehsold && - nonRotated[1] < center[1] + verticalThrehsold + nonRotated[1] > center[1] - verticalThreshold && + nonRotated[1] < center[1] + verticalThreshold ) { // LEFT - return pointRotateRads( + return pointRotateRads( pointFrom(x - FIXED_BINDING_DISTANCE, center[1]), center, angle, ); } else if ( nonRotated[1] <= y + height / 2 && - nonRotated[0] > center[0] - horizontalThrehsold && - nonRotated[0] < center[0] + horizontalThrehsold + nonRotated[0] > center[0] - horizontalThreshold && + nonRotated[0] < center[0] + horizontalThreshold ) { // TOP return pointRotateRads( @@ -1146,8 +1126,8 @@ export const snapToMid = ( ); } else if ( nonRotated[0] >= x + width / 2 && - nonRotated[1] > center[1] - verticalThrehsold && - nonRotated[1] < center[1] + verticalThrehsold + nonRotated[1] > center[1] - verticalThreshold && + nonRotated[1] < center[1] + verticalThreshold ) { // RIGHT return pointRotateRads( @@ -1157,8 +1137,8 @@ export const snapToMid = ( ); } else if ( nonRotated[1] >= y + height / 2 && - nonRotated[0] > center[0] - horizontalThrehsold && - nonRotated[0] < center[0] + horizontalThrehsold + nonRotated[0] > center[0] - horizontalThreshold && + nonRotated[0] < center[0] + horizontalThreshold ) { // DOWN return pointRotateRads( @@ -1167,7 +1147,7 @@ export const snapToMid = ( angle, ); } else if (element.type === "diamond") { - const distance = FIXED_BINDING_DISTANCE - 1; + const distance = FIXED_BINDING_DISTANCE; const topLeft = pointFrom( x + width / 4 - distance, y + height / 4 - distance, @@ -1184,27 +1164,28 @@ export const snapToMid = ( x + (3 * width) / 4 + distance, y + (3 * height) / 4 + distance, ); + if ( pointDistance(topLeft, nonRotated) < - Math.max(horizontalThrehsold, verticalThrehsold) + Math.max(horizontalThreshold, verticalThreshold) ) { return pointRotateRads(topLeft, center, angle); } if ( pointDistance(topRight, nonRotated) < - Math.max(horizontalThrehsold, verticalThrehsold) + Math.max(horizontalThreshold, verticalThreshold) ) { return pointRotateRads(topRight, center, angle); } if ( pointDistance(bottomLeft, nonRotated) < - Math.max(horizontalThrehsold, verticalThrehsold) + Math.max(horizontalThreshold, verticalThreshold) ) { return pointRotateRads(bottomLeft, center, angle); } if ( pointDistance(bottomRight, nonRotated) < - Math.max(horizontalThrehsold, verticalThrehsold) + Math.max(horizontalThreshold, verticalThreshold) ) { return pointRotateRads(bottomRight, center, angle); } @@ -1239,8 +1220,9 @@ const updateBoundPoint = ( linearElement, bindableElement, startOrEnd === "startBinding" ? "start" : "end", + elementsMap, ).fixedPoint; - const globalMidPoint = elementCenterPoint(bindableElement); + const globalMidPoint = elementCenterPoint(bindableElement, elementsMap); const global = pointFrom( bindableElement.x + fixedPoint[0] * bindableElement.width, bindableElement.y + fixedPoint[1] * bindableElement.height, @@ -1266,6 +1248,7 @@ const updateBoundPoint = ( ); const focusPointAbsolute = determineFocusPoint( bindableElement, + elementsMap, binding.focus, adjacentPoint, ); @@ -1284,7 +1267,7 @@ const updateBoundPoint = ( elementsMap, ); - const center = elementCenterPoint(bindableElement); + const center = elementCenterPoint(bindableElement, elementsMap); const interceptorLength = pointDistance(adjacentPoint, edgePointAbsolute) + pointDistance(adjacentPoint, center) + @@ -1292,6 +1275,7 @@ const updateBoundPoint = ( const intersections = [ ...intersectElementWithLineSegment( bindableElement, + elementsMap, lineSegment( adjacentPoint, pointFromVector( @@ -1342,6 +1326,7 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): { fixedPoint: FixedPoint } => { const bounds = [ hoveredElement.x, @@ -1353,6 +1338,7 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement, hoveredElement, startOrEnd, + elementsMap, ); const globalMidPoint = pointFrom( bounds[0] + (bounds[2] - bounds[0]) / 2, @@ -1396,7 +1382,7 @@ const maybeCalculateNewGapWhenScaling = ( return { ...currentBinding, gap: newGap }; }; -const getElligibleElementForBindingElement = ( +const getEligibleElementForBindingElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", elementsMap: NonDeletedSceneElementsMap, @@ -1548,14 +1534,38 @@ export const bindingBorderTest = ( zoom?: AppState["zoom"], fullShape?: boolean, ): boolean => { + const p = pointFrom(x, y); const threshold = maxBindingGap(element, element.width, element.height, zoom); + const shouldTestInside = + // disable fullshape snapping for frame elements so we + // can bind to frame children + (fullShape || !isBindingFallthroughEnabled(element)) && + !isFrameLikeElement(element); - const shape = getElementShape(element, elementsMap); - return ( - isPointOnShape(pointFrom(x, y), shape, threshold) || - (fullShape === true && - pointInsideBounds(pointFrom(x, y), aabbForElement(element))) + // PERF: Run a cheap test to see if the binding element + // is even close to the element + const bounds = [ + x - threshold, + y - threshold, + x + threshold, + y + threshold, + ] as Bounds; + const elementBounds = getElementBounds(element, elementsMap); + if (!doBoundsIntersect(bounds, elementBounds)) { + return false; + } + + // Do the intersection test against the element since it's close enough + const intersections = intersectElementWithLineSegment( + element, + elementsMap, + lineSegment(elementCenterPoint(element, elementsMap), p), ); + const distance = distanceToElement(element, elementsMap, p); + + return shouldTestInside + ? intersections.length === 0 || distance <= threshold + : intersections.length > 0 && distance <= threshold; }; export const maxBindingGap = ( @@ -1575,7 +1585,7 @@ export const maxBindingGap = ( // bigger bindable boundary for bigger elements Math.min(0.25 * smallerDimension, 32), // keep in sync with the zoomed highlight - BINDING_HIGHLIGHT_THICKNESS / zoomValue + BINDING_HIGHLIGHT_OFFSET, + BINDING_HIGHLIGHT_THICKNESS / zoomValue + FIXED_BINDING_DISTANCE, ); }; @@ -1586,12 +1596,13 @@ export const maxBindingGap = ( // of the element. const determineFocusDistance = ( element: ExcalidrawBindableElement, + elementsMap: ElementsMap, // Point on the line, in absolute coordinates a: GlobalPoint, // Another point on the line, in absolute coordinates (closer to element) b: GlobalPoint, ): number => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); if (pointsEqual(a, b)) { return 0; @@ -1716,12 +1727,13 @@ const determineFocusDistance = ( const determineFocusPoint = ( element: ExcalidrawBindableElement, + elementsMap: ElementsMap, // The oriented, relative distance from the center of `element` of the // returned focusPoint focus: number, adjacentPoint: GlobalPoint, ): GlobalPoint => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); if (focus === 0) { return center; @@ -2144,6 +2156,7 @@ export class BindableElement { export const getGlobalFixedPointForBindableElement = ( fixedPointRatio: [number, number], element: ExcalidrawBindableElement, + elementsMap: ElementsMap, ): GlobalPoint => { const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio); @@ -2152,7 +2165,7 @@ export const getGlobalFixedPointForBindableElement = ( element.x + element.width * fixedX, element.y + element.height * fixedY, ), - elementCenterPoint(element), + elementCenterPoint(element, elementsMap), element.angle, ); }; @@ -2176,6 +2189,7 @@ export const getGlobalFixedPoints = ( ? getGlobalFixedPointForBindableElement( arrow.startBinding.fixedPoint, startElement as ExcalidrawBindableElement, + elementsMap, ) : pointFrom( arrow.x + arrow.points[0][0], @@ -2186,6 +2200,7 @@ export const getGlobalFixedPoints = ( ? getGlobalFixedPointForBindableElement( arrow.endBinding.fixedPoint, endElement as ExcalidrawBindableElement, + elementsMap, ) : pointFrom( arrow.x + arrow.points[arrow.points.length - 1][0], diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index a5b91922b4..1bfb441585 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -102,9 +102,23 @@ export class ElementBounds { version: ExcalidrawElement["version"]; } >(); + private static nonRotatedBoundsCache = new WeakMap< + ExcalidrawElement, + { + bounds: Bounds; + version: ExcalidrawElement["version"]; + } + >(); - static getBounds(element: ExcalidrawElement, elementsMap: ElementsMap) { - const cachedBounds = ElementBounds.boundsCache.get(element); + static getBounds( + element: ExcalidrawElement, + elementsMap: ElementsMap, + nonRotated: boolean = false, + ) { + const cachedBounds = + nonRotated && element.angle !== 0 + ? ElementBounds.nonRotatedBoundsCache.get(element) + : ElementBounds.boundsCache.get(element); if ( cachedBounds?.version && @@ -115,6 +129,23 @@ export class ElementBounds { ) { return cachedBounds.bounds; } + + if (nonRotated && element.angle !== 0) { + const nonRotatedBounds = ElementBounds.calculateBounds( + { + ...element, + angle: 0 as Radians, + }, + elementsMap, + ); + ElementBounds.nonRotatedBoundsCache.set(element, { + version: element.version, + bounds: nonRotatedBounds, + }); + + return nonRotatedBounds; + } + const bounds = ElementBounds.calculateBounds(element, elementsMap); ElementBounds.boundsCache.set(element, { @@ -939,8 +970,9 @@ const getLinearElementRotatedBounds = ( export const getElementBounds = ( element: ExcalidrawElement, elementsMap: ElementsMap, + nonRotated: boolean = false, ): Bounds => { - return ElementBounds.getBounds(element, elementsMap); + return ElementBounds.getBounds(element, elementsMap, nonRotated); }; export const getCommonBounds = ( diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 07b17bfde5..af81ff99ce 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -2,51 +2,60 @@ import { isTransparent, elementCenterPoint } from "@excalidraw/common"; import { curveIntersectLineSegment, isPointWithinBounds, - line, lineSegment, lineSegmentIntersectionPoints, pointFrom, + pointFromVector, pointRotateRads, pointsEqual, + vectorFromPoint, + vectorNormalize, + vectorScale, } from "@excalidraw/math"; import { ellipse, - ellipseLineIntersectionPoints, + ellipseSegmentInterceptPoints, } from "@excalidraw/math/ellipse"; -import { isPointInShape, isPointOnShape } from "@excalidraw/utils/collision"; -import { type GeometricShape, getPolygonShape } from "@excalidraw/utils/shape"; - -import type { - GlobalPoint, - LineSegment, - LocalPoint, - Polygon, - Radians, -} from "@excalidraw/math"; +import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; -import { getBoundTextShape, isPathALoop } from "./shapes"; -import { getElementBounds } from "./bounds"; +import { isPathALoop } from "./shapes"; +import { + type Bounds, + doBoundsIntersect, + getCenterForBounds, + getElementBounds, +} from "./bounds"; import { hasBoundTextElement, + isFreeDrawElement, isIframeLikeElement, isImageElement, + isLinearElement, isTextElement, } from "./typeChecks"; import { deconstructDiamondElement, + deconstructLinearOrFreeDrawElement, deconstructRectanguloidElement, } from "./utils"; +import { getBoundTextElement } from "./textElement"; + +import { LinearElementEditor } from "./linearElementEditor"; + +import { distanceToElement } from "./distance"; + import type { ElementsMap, ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawEllipseElement, - ExcalidrawRectangleElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, ExcalidrawRectanguloidElement, } from "./types"; @@ -72,45 +81,64 @@ export const shouldTestInside = (element: ExcalidrawElement) => { return isDraggableFromInside || isImageElement(element); }; -export type HitTestArgs = { - x: number; - y: number; +export type HitTestArgs = { + point: GlobalPoint; element: ExcalidrawElement; - shape: GeometricShape; - threshold?: number; + threshold: number; + elementsMap: ElementsMap; frameNameBound?: FrameNameBounds | null; }; -export const hitElementItself = ({ - x, - y, +export const hitElementItself = ({ + point, element, - shape, - threshold = 10, + threshold, + elementsMap, frameNameBound = null, -}: HitTestArgs) => { - let hit = shouldTestInside(element) - ? // Since `inShape` tests STRICTLY againt the insides of a shape - // we would need `onShape` as well to include the "borders" - isPointInShape(pointFrom(x, y), shape) || - isPointOnShape(pointFrom(x, y), shape, threshold) - : isPointOnShape(pointFrom(x, y), shape, threshold); +}: HitTestArgs) => { + // Hit test against a frame's name + const hitFrameName = frameNameBound + ? isPointWithinBounds( + pointFrom(frameNameBound.x - threshold, frameNameBound.y - threshold), + point, + pointFrom( + frameNameBound.x + frameNameBound.width + threshold, + frameNameBound.y + frameNameBound.height + threshold, + ), + ) + : false; - // hit test against a frame's name - if (!hit && frameNameBound) { - hit = isPointInShape(pointFrom(x, y), { - type: "polygon", - data: getPolygonShape(frameNameBound as ExcalidrawRectangleElement) - .data as Polygon, - }); + // Hit test against the extended, rotated bounding box of the element first + const bounds = getElementBounds(element, elementsMap, true); + const hitBounds = isPointWithinBounds( + pointFrom(bounds[0] - threshold, bounds[1] - threshold), + pointRotateRads( + point, + getCenterForBounds(bounds), + -element.angle as Radians, + ), + pointFrom(bounds[2] + threshold, bounds[3] + threshold), + ); + + // PERF: Bail out early if the point is not even in the + // rotated bounding box or not hitting the frame name (saves 99%) + if (!hitBounds && !hitFrameName) { + return false; } - return hit; + // Do the precise (and relatively costly) hit test + const hitElement = shouldTestInside(element) + ? // Since `inShape` tests STRICTLY againt the insides of a shape + // we would need `onShape` as well to include the "borders" + isPointInElement(point, element, elementsMap) || + isPointOnElementOutline(point, element, elementsMap, threshold) + : isPointOnElementOutline(point, element, elementsMap, threshold); + + return hitElement || hitFrameName; }; export const hitElementBoundingBox = ( - x: number, - y: number, + point: GlobalPoint, element: ExcalidrawElement, elementsMap: ElementsMap, tolerance = 0, @@ -120,37 +148,42 @@ export const hitElementBoundingBox = ( y1 -= tolerance; x2 += tolerance; y2 += tolerance; - return isPointWithinBounds( - pointFrom(x1, y1), - pointFrom(x, y), - pointFrom(x2, y2), - ); + return isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2)); }; -export const hitElementBoundingBoxOnly = < - Point extends GlobalPoint | LocalPoint, ->( - hitArgs: HitTestArgs, +export const hitElementBoundingBoxOnly = ( + hitArgs: HitTestArgs, elementsMap: ElementsMap, -) => { - return ( - !hitElementItself(hitArgs) && - // bound text is considered part of the element (even if it's outside the bounding box) - !hitElementBoundText( - hitArgs.x, - hitArgs.y, - getBoundTextShape(hitArgs.element, elementsMap), - ) && - hitElementBoundingBox(hitArgs.x, hitArgs.y, hitArgs.element, elementsMap) - ); -}; +) => + !hitElementItself(hitArgs) && + // bound text is considered part of the element (even if it's outside the bounding box) + !hitElementBoundText(hitArgs.point, hitArgs.element, elementsMap) && + hitElementBoundingBox(hitArgs.point, hitArgs.element, elementsMap); -export const hitElementBoundText = ( - x: number, - y: number, - textShape: GeometricShape | null, +export const hitElementBoundText = ( + point: GlobalPoint, + element: ExcalidrawElement, + elementsMap: ElementsMap, ): boolean => { - return !!textShape && isPointInShape(pointFrom(x, y), textShape); + const boundTextElementCandidate = getBoundTextElement(element, elementsMap); + + if (!boundTextElementCandidate) { + return false; + } + const boundTextElement = isLinearElement(element) + ? { + ...boundTextElementCandidate, + // arrow's bound text accurate position is not stored in the element's property + // but rather calculated and returned from the following static method + ...LinearElementEditor.getBoundTextElementPosition( + element, + boundTextElementCandidate, + elementsMap, + ), + } + : boundTextElementCandidate; + + return isPointInElement(point, boundTextElement, elementsMap); }; /** @@ -163,9 +196,26 @@ export const hitElementBoundText = ( */ export const intersectElementWithLineSegment = ( element: ExcalidrawElement, + elementsMap: ElementsMap, line: LineSegment, offset: number = 0, + onlyFirst = false, ): GlobalPoint[] => { + // First check if the line intersects the element's axis-aligned bounding box + // as it is much faster than checking intersection against the element's shape + const intersectorBounds = [ + Math.min(line[0][0] - offset, line[1][0] - offset), + Math.min(line[0][1] - offset, line[1][1] - offset), + Math.max(line[0][0] + offset, line[1][0] + offset), + Math.max(line[0][1] + offset, line[1][1] + offset), + ] as Bounds; + const elementBounds = getElementBounds(element, elementsMap); + + if (!doBoundsIntersect(intersectorBounds, elementBounds)) { + return []; + } + + // Do the actual intersection test against the element's shape switch (element.type) { case "rectangle": case "image": @@ -173,23 +223,88 @@ export const intersectElementWithLineSegment = ( case "iframe": case "embeddable": case "frame": + case "selection": case "magicframe": - return intersectRectanguloidWithLineSegment(element, line, offset); + return intersectRectanguloidWithLineSegment( + element, + elementsMap, + line, + offset, + onlyFirst, + ); case "diamond": - return intersectDiamondWithLineSegment(element, line, offset); + return intersectDiamondWithLineSegment( + element, + elementsMap, + line, + offset, + onlyFirst, + ); case "ellipse": - return intersectEllipseWithLineSegment(element, line, offset); - default: - throw new Error(`Unimplemented element type '${element.type}'`); + return intersectEllipseWithLineSegment( + element, + elementsMap, + line, + offset, + ); + case "line": + case "freedraw": + case "arrow": + return intersectLinearOrFreeDrawWithLineSegment( + element, + elementsMap, + line, + onlyFirst, + ); } }; +const intersectLinearOrFreeDrawWithLineSegment = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, + elementsMap: ElementsMap, + segment: LineSegment, + onlyFirst = false, +): GlobalPoint[] => { + const [lines, curves] = deconstructLinearOrFreeDrawElement( + element, + elementsMap, + ); + const intersections = []; + + for (const l of lines) { + const intersection = lineSegmentIntersectionPoints(l, segment); + if (intersection) { + intersections.push(intersection); + + if (onlyFirst) { + return intersections; + } + } + } + + for (const c of curves) { + const hits = curveIntersectLineSegment(c, segment); + + if (hits.length > 0) { + intersections.push(...hits); + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; +}; + const intersectRectanguloidWithLineSegment = ( element: ExcalidrawRectanguloidElement, + elementsMap: ElementsMap, l: LineSegment, offset: number = 0, + onlyFirst = false, ): GlobalPoint[] => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); // To emulate a rotated rectangle we rotate the point in the inverse angle // instead. It's all the same distance-wise. const rotatedA = pointRotateRads( @@ -206,34 +321,37 @@ const intersectRectanguloidWithLineSegment = ( // Get the element's building components we can test against const [sides, corners] = deconstructRectanguloidElement(element, offset); - return ( - // Test intersection against the sides, keep only the valid - // intersection points and rotate them back to scene space - sides - .map((s) => - lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ), - ) - .filter((x) => x != null) - .map((j) => pointRotateRads(j!, center, element.angle)) - // Test intersection against the corners which are cubic bezier curves, - // keep only the valid intersection points and rotate them back to scene - // space - .concat( - corners - .flatMap((t) => - curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)), - ) - .filter((i) => i != null) - .map((j) => pointRotateRads(j, center, element.angle)), - ) - // Remove duplicates - .filter( - (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx, - ) - ); + const intersections: GlobalPoint[] = []; + + for (const s of sides) { + const intersection = lineSegmentIntersectionPoints( + lineSegment(rotatedA, rotatedB), + s, + ); + if (intersection) { + intersections.push(pointRotateRads(intersection, center, element.angle)); + + if (onlyFirst) { + return intersections; + } + } + } + + for (const t of corners) { + const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)); + + if (hits.length > 0) { + for (const j of hits) { + intersections.push(pointRotateRads(j, center, element.angle)); + } + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; }; /** @@ -245,43 +363,51 @@ const intersectRectanguloidWithLineSegment = ( */ const intersectDiamondWithLineSegment = ( element: ExcalidrawDiamondElement, + elementsMap: ElementsMap, l: LineSegment, offset: number = 0, + onlyFirst = false, ): GlobalPoint[] => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); // Rotate the point to the inverse direction to simulate the rotated diamond // points. It's all the same distance-wise. const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); - const [sides, curves] = deconstructDiamondElement(element, offset); + const [sides, corners] = deconstructDiamondElement(element, offset); - return ( - sides - .map((s) => - lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ), - ) - .filter((p): p is GlobalPoint => p != null) - // Rotate back intersection points - .map((p) => pointRotateRads(p!, center, element.angle)) - .concat( - curves - .flatMap((p) => - curveIntersectLineSegment(p, lineSegment(rotatedA, rotatedB)), - ) - .filter((p) => p != null) - // Rotate back intersection points - .map((p) => pointRotateRads(p, center, element.angle)), - ) - // Remove duplicates - .filter( - (p, idx, points) => points.findIndex((d) => pointsEqual(p, d)) === idx, - ) - ); + const intersections: GlobalPoint[] = []; + + for (const s of sides) { + const intersection = lineSegmentIntersectionPoints( + lineSegment(rotatedA, rotatedB), + s, + ); + if (intersection) { + intersections.push(pointRotateRads(intersection, center, element.angle)); + + if (onlyFirst) { + return intersections; + } + } + } + + for (const t of corners) { + const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)); + + if (hits.length > 0) { + for (const j of hits) { + intersections.push(pointRotateRads(j, center, element.angle)); + } + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; }; /** @@ -293,16 +419,76 @@ const intersectDiamondWithLineSegment = ( */ const intersectEllipseWithLineSegment = ( element: ExcalidrawEllipseElement, + elementsMap: ElementsMap, l: LineSegment, offset: number = 0, ): GlobalPoint[] => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); - return ellipseLineIntersectionPoints( + return ellipseSegmentInterceptPoints( ellipse(center, element.width / 2 + offset, element.height / 2 + offset), - line(rotatedA, rotatedB), + lineSegment(rotatedA, rotatedB), ).map((p) => pointRotateRads(p, center, element.angle)); }; + +/** + * Check if the given point is considered on the given shape's border + * + * @param point + * @param element + * @param tolerance + * @returns + */ +const isPointOnElementOutline = ( + point: GlobalPoint, + element: ExcalidrawElement, + elementsMap: ElementsMap, + tolerance = 1, +) => distanceToElement(element, elementsMap, point) <= tolerance; + +/** + * Check if the given point is considered inside the element's border + * + * @param point + * @param element + * @returns + */ +export const isPointInElement = ( + point: GlobalPoint, + element: ExcalidrawElement, + elementsMap: ElementsMap, +) => { + if ( + (isLinearElement(element) || isFreeDrawElement(element)) && + !isPathALoop(element.points) + ) { + // There isn't any "inside" for a non-looping path + return false; + } + + const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); + + if (!isPointWithinBounds(pointFrom(x1, y1), point, pointFrom(x2, y2))) { + return false; + } + + const center = pointFrom((x1 + x2) / 2, (y1 + y2) / 2); + const otherPoint = pointFromVector( + vectorScale( + vectorNormalize(vectorFromPoint(point, center, 0.1)), + Math.max(element.width, element.height) * 2, + ), + center, + ); + const intersector = lineSegment(point, otherPoint); + const intersections = intersectElementWithLineSegment( + element, + elementsMap, + intersector, + ).filter((p, pos, arr) => arr.findIndex((q) => pointsEqual(q, p)) === pos); + + return intersections.length % 2 === 1; +}; diff --git a/packages/element/src/cropElement.ts b/packages/element/src/cropElement.ts index 2bc930d668..c2a9f91fdb 100644 --- a/packages/element/src/cropElement.ts +++ b/packages/element/src/cropElement.ts @@ -34,6 +34,7 @@ export const MINIMAL_CROP_SIZE = 10; export const cropElement = ( element: ExcalidrawImageElement, + elementsMap: ElementsMap, transformHandle: TransformHandleType, naturalWidth: number, naturalHeight: number, @@ -63,7 +64,7 @@ export const cropElement = ( const rotatedPointer = pointRotateRads( pointFrom(pointerX, pointerY), - elementCenterPoint(element), + elementCenterPoint(element, elementsMap), -element.angle as Radians, ); diff --git a/packages/element/src/distance.ts b/packages/element/src/distance.ts index d261faf7df..55e9ed2bdb 100644 --- a/packages/element/src/distance.ts +++ b/packages/element/src/distance.ts @@ -12,21 +12,27 @@ import type { GlobalPoint, Radians } from "@excalidraw/math"; import { deconstructDiamondElement, + deconstructLinearOrFreeDrawElement, deconstructRectanguloidElement, } from "./utils"; import type { - ExcalidrawBindableElement, + ElementsMap, ExcalidrawDiamondElement, + ExcalidrawElement, ExcalidrawEllipseElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, ExcalidrawRectanguloidElement, } from "./types"; -export const distanceToBindableElement = ( - element: ExcalidrawBindableElement, +export const distanceToElement = ( + element: ExcalidrawElement, + elementsMap: ElementsMap, p: GlobalPoint, ): number => { switch (element.type) { + case "selection": case "rectangle": case "image": case "text": @@ -34,11 +40,15 @@ export const distanceToBindableElement = ( case "embeddable": case "frame": case "magicframe": - return distanceToRectanguloidElement(element, p); + return distanceToRectanguloidElement(element, elementsMap, p); case "diamond": - return distanceToDiamondElement(element, p); + return distanceToDiamondElement(element, elementsMap, p); case "ellipse": - return distanceToEllipseElement(element, p); + return distanceToEllipseElement(element, elementsMap, p); + case "line": + case "arrow": + case "freedraw": + return distanceToLinearOrFreeDraElement(element, elementsMap, p); } }; @@ -52,9 +62,10 @@ export const distanceToBindableElement = ( */ const distanceToRectanguloidElement = ( element: ExcalidrawRectanguloidElement, + elementsMap: ElementsMap, p: GlobalPoint, ) => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); // To emulate a rotated rectangle we rotate the point in the inverse angle // instead. It's all the same distance-wise. const rotatedPoint = pointRotateRads(p, center, -element.angle as Radians); @@ -80,9 +91,10 @@ const distanceToRectanguloidElement = ( */ const distanceToDiamondElement = ( element: ExcalidrawDiamondElement, + elementsMap: ElementsMap, p: GlobalPoint, ): number => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); // Rotate the point to the inverse direction to simulate the rotated diamond // points. It's all the same distance-wise. @@ -108,12 +120,28 @@ const distanceToDiamondElement = ( */ const distanceToEllipseElement = ( element: ExcalidrawEllipseElement, + elementsMap: ElementsMap, p: GlobalPoint, ): number => { - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); return ellipseDistanceFromPoint( // Instead of rotating the ellipse, rotate the point to the inverse angle pointRotateRads(p, center, -element.angle as Radians), ellipse(center, element.width / 2, element.height / 2), ); }; + +const distanceToLinearOrFreeDraElement = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, + elementsMap: ElementsMap, + p: GlobalPoint, +) => { + const [lines, curves] = deconstructLinearOrFreeDrawElement( + element, + elementsMap, + ); + return Math.min( + ...lines.map((s) => distanceToLineSegment(p, s)), + ...curves.map((a) => curvePointDistance(a, p)), + ); +}; diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 73c82a8980..fb60c10db7 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -29,10 +29,9 @@ import { FIXED_BINDING_DISTANCE, getHeadingForElbowArrowSnap, getGlobalFixedPointForBindableElement, - snapToMid, getHoveredElementForBinding, } from "./binding"; -import { distanceToBindableElement } from "./distance"; +import { distanceToElement } from "./distance"; import { compareHeading, flipHeading, @@ -898,50 +897,6 @@ export const updateElbowArrowPoints = ( return { points: updates.points ?? arrow.points }; } - // NOTE (mtolmacs): This is a temporary check to ensure that the incoming elbow - // arrow size is valid. This check will be removed once the issue is identified - if ( - arrow.x < -MAX_POS || - arrow.x > MAX_POS || - arrow.y < -MAX_POS || - arrow.y > MAX_POS || - arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) < - -MAX_POS || - arrow.x + (updates?.points?.[updates?.points?.length - 1]?.[0] ?? 0) > - MAX_POS || - arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) < - -MAX_POS || - arrow.y + (updates?.points?.[updates?.points?.length - 1]?.[1] ?? 0) > - MAX_POS || - arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) < - -MAX_POS || - arrow.x + (arrow?.points?.[arrow?.points?.length - 1]?.[0] ?? 0) > - MAX_POS || - arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) < - -MAX_POS || - arrow.y + (arrow?.points?.[arrow?.points?.length - 1]?.[1] ?? 0) > MAX_POS - ) { - console.error( - "Elbow arrow (or update) is outside reasonable bounds (> 1e6)", - { - arrow, - updates, - }, - ); - } - // @ts-ignore See above note - arrow.x = clamp(arrow.x, -MAX_POS, MAX_POS); - // @ts-ignore See above note - arrow.y = clamp(arrow.y, -MAX_POS, MAX_POS); - if (updates.points) { - updates.points = updates.points.map(([x, y]) => - pointFrom( - clamp(x, -MAX_POS, MAX_POS), - clamp(y, -MAX_POS, MAX_POS), - ), - ); - } - if (!import.meta.env.PROD) { invariant( !updates.points || updates.points.length >= 2, @@ -1273,6 +1228,7 @@ const getElbowArrowData = ( arrow.startBinding?.fixedPoint, origStartGlobalPoint, hoveredStartElement, + elementsMap, options?.isDragging, ); const endGlobalPoint = getGlobalPoint( @@ -1286,6 +1242,7 @@ const getElbowArrowData = ( arrow.endBinding?.fixedPoint, origEndGlobalPoint, hoveredEndElement, + elementsMap, options?.isDragging, ); const startHeading = getBindPointHeading( @@ -1293,12 +1250,14 @@ const getElbowArrowData = ( endGlobalPoint, hoveredStartElement, origStartGlobalPoint, + elementsMap, ); const endHeading = getBindPointHeading( endGlobalPoint, startGlobalPoint, hoveredEndElement, origEndGlobalPoint, + elementsMap, ); const startPointBounds = [ startGlobalPoint[0] - 2, @@ -1315,6 +1274,7 @@ const getElbowArrowData = ( const startElementBounds = hoveredStartElement ? aabbForElement( hoveredStartElement, + elementsMap, offsetFromHeading( startHeading, arrow.startArrowhead @@ -1327,6 +1287,7 @@ const getElbowArrowData = ( const endElementBounds = hoveredEndElement ? aabbForElement( hoveredEndElement, + elementsMap, offsetFromHeading( endHeading, arrow.endArrowhead @@ -1342,6 +1303,7 @@ const getElbowArrowData = ( hoveredEndElement ? aabbForElement( hoveredEndElement, + elementsMap, offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING), ) : endPointBounds, @@ -1351,6 +1313,7 @@ const getElbowArrowData = ( hoveredStartElement ? aabbForElement( hoveredStartElement, + elementsMap, offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING), ) : startPointBounds, @@ -1397,8 +1360,8 @@ const getElbowArrowData = ( BASE_PADDING, ), boundsOverlap, - hoveredStartElement && aabbForElement(hoveredStartElement), - hoveredEndElement && aabbForElement(hoveredEndElement), + hoveredStartElement && aabbForElement(hoveredStartElement, elementsMap), + hoveredEndElement && aabbForElement(hoveredEndElement, elementsMap), ); const startDonglePosition = getDonglePosition( dynamicAABBs[0], @@ -2229,34 +2192,35 @@ const getGlobalPoint = ( fixedPointRatio: [number, number] | undefined | null, initialPoint: GlobalPoint, element?: ExcalidrawBindableElement | null, + elementsMap?: ElementsMap, isDragging?: boolean, ): GlobalPoint => { if (isDragging) { - if (element) { - const snapPoint = bindPointToSnapToElementOutline( + if (element && elementsMap) { + return bindPointToSnapToElementOutline( arrow, element, startOrEnd, + elementsMap, ); - - return snapToMid(element, snapPoint); } return initialPoint; } - if (element) { + if (element && elementsMap) { const fixedGlobalPoint = getGlobalFixedPointForBindableElement( fixedPointRatio || [0, 0], element, + elementsMap, ); // NOTE: Resize scales the binding position point too, so we need to update it return Math.abs( - distanceToBindableElement(element, fixedGlobalPoint) - + distanceToElement(element, elementsMap, fixedGlobalPoint) - FIXED_BINDING_DISTANCE, ) > 0.01 - ? bindPointToSnapToElementOutline(arrow, element, startOrEnd) + ? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap) : fixedGlobalPoint; } @@ -2268,6 +2232,7 @@ const getBindPointHeading = ( otherPoint: GlobalPoint, hoveredElement: ExcalidrawBindableElement | null | undefined, origPoint: GlobalPoint, + elementsMap: ElementsMap, ): Heading => getHeadingForElbowArrowSnap( p, @@ -2276,7 +2241,8 @@ const getBindPointHeading = ( hoveredElement && aabbForElement( hoveredElement, - Array(4).fill(distanceToBindableElement(hoveredElement, p)) as [ + elementsMap, + Array(4).fill(distanceToElement(hoveredElement, elementsMap, p)) as [ number, number, number, @@ -2284,6 +2250,7 @@ const getBindPointHeading = ( ], ), origPoint, + elementsMap, ); const getHoveredElement = ( diff --git a/packages/element/src/flowchart.ts b/packages/element/src/flowchart.ts index 5194e54259..9e5af4216e 100644 --- a/packages/element/src/flowchart.ts +++ b/packages/element/src/flowchart.ts @@ -95,10 +95,11 @@ const getNodeRelatives = ( type === "predecessors" ? el.points[el.points.length - 1] : [0, 0] ) as Readonly; - const heading = headingForPointFromElement(node, aabbForElement(node), [ - edgePoint[0] + el.x, - edgePoint[1] + el.y, - ] as Readonly); + const heading = headingForPointFromElement( + node, + aabbForElement(node, elementsMap), + [edgePoint[0] + el.x, edgePoint[1] + el.y] as Readonly, + ); acc.push({ relative, diff --git a/packages/element/src/shapes.ts b/packages/element/src/shapes.ts index 20041ce1bc..3abf7f5905 100644 --- a/packages/element/src/shapes.ts +++ b/packages/element/src/shapes.ts @@ -291,6 +291,7 @@ export const mapIntervalToBezierT =

( */ export const aabbForElement = ( element: Readonly, + elementsMap: ElementsMap, offset?: [number, number, number, number], ) => { const bbox = { @@ -302,7 +303,7 @@ export const aabbForElement = ( midY: element.y + element.height / 2, }; - const center = elementCenterPoint(element); + const center = elementCenterPoint(element, elementsMap); const [topLeftX, topLeftY] = pointRotateRads( pointFrom(bbox.minX, bbox.minY), center, diff --git a/packages/element/src/types.ts b/packages/element/src/types.ts index 23e4f99290..c2becd3e6c 100644 --- a/packages/element/src/types.ts +++ b/packages/element/src/types.ts @@ -195,7 +195,8 @@ export type ExcalidrawRectanguloidElement = | ExcalidrawFreeDrawElement | ExcalidrawIframeLikeElement | ExcalidrawFrameLikeElement - | ExcalidrawEmbeddableElement; + | ExcalidrawEmbeddableElement + | ExcalidrawSelectionElement; /** * ExcalidrawElement should be JSON serializable and (eventually) contain diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 57b1e4346c..673b9ef1b0 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -1,28 +1,168 @@ import { curve, + curveCatmullRomCubicApproxPoints, + curveOffsetPoints, lineSegment, pointFrom, - pointFromVector, + pointFromArray, rectangle, - vectorFromPoint, - vectorNormalize, - vectorScale, type GlobalPoint, } from "@excalidraw/math"; -import { elementCenterPoint } from "@excalidraw/common"; - -import type { Curve, LineSegment } from "@excalidraw/math"; +import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math"; import { getCornerRadius } from "./shapes"; import { getDiamondPoints } from "./bounds"; +import { generateLinearCollisionShape } from "./Shape"; + import type { + ElementsMap, ExcalidrawDiamondElement, + ExcalidrawElement, + ExcalidrawFreeDrawElement, + ExcalidrawLinearElement, ExcalidrawRectanguloidElement, } from "./types"; +type ElementShape = [LineSegment[], Curve[]]; + +const ElementShapesCache = new WeakMap< + ExcalidrawElement, + { version: ExcalidrawElement["version"]; shapes: Map } +>(); + +const getElementShapesCacheEntry = ( + element: T, + offset: number, +): ElementShape | undefined => { + const record = ElementShapesCache.get(element); + + if (!record) { + return undefined; + } + + const { version, shapes } = record; + + if (version !== element.version) { + ElementShapesCache.delete(element); + return undefined; + } + + return shapes.get(offset); +}; + +const setElementShapesCacheEntry = ( + element: T, + shape: ElementShape, + offset: number, +) => { + const record = ElementShapesCache.get(element); + + if (!record) { + ElementShapesCache.set(element, { + version: element.version, + shapes: new Map([[offset, shape]]), + }); + + return; + } + + const { version, shapes } = record; + + if (version !== element.version) { + ElementShapesCache.set(element, { + version: element.version, + shapes: new Map([[offset, shape]]), + }); + + return; + } + + shapes.set(offset, shape); +}; + +export function deconstructLinearOrFreeDrawElement( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, + elementsMap: ElementsMap, +): [LineSegment[], Curve[]] { + const cachedShape = getElementShapesCacheEntry(element, 0); + + if (cachedShape) { + return cachedShape; + } + + const ops = generateLinearCollisionShape(element, elementsMap) as { + op: string; + data: number[]; + }[]; + const lines = []; + const curves = []; + + for (let idx = 0; idx < ops.length; idx += 1) { + const op = ops[idx]; + const prevPoint = + ops[idx - 1] && pointFromArray(ops[idx - 1].data.slice(-2)); + switch (op.op) { + case "move": + continue; + case "lineTo": + if (!prevPoint) { + throw new Error("prevPoint is undefined"); + } + + lines.push( + lineSegment( + pointFrom( + element.x + prevPoint[0], + element.y + prevPoint[1], + ), + pointFrom( + element.x + op.data[0], + element.y + op.data[1], + ), + ), + ); + continue; + case "bcurveTo": + if (!prevPoint) { + throw new Error("prevPoint is undefined"); + } + + curves.push( + curve( + pointFrom( + element.x + prevPoint[0], + element.y + prevPoint[1], + ), + pointFrom( + element.x + op.data[0], + element.y + op.data[1], + ), + pointFrom( + element.x + op.data[2], + element.y + op.data[3], + ), + pointFrom( + element.x + op.data[4], + element.y + op.data[5], + ), + ), + ); + continue; + default: { + console.error("Unknown op type", op.op); + } + } + } + + const shape = [lines, curves] as ElementShape; + setElementShapesCacheEntry(element, shape, 0); + + return shape; +} + /** * Get the building components of a rectanguloid element in the form of * line segments and curves. @@ -35,175 +175,132 @@ export function deconstructRectanguloidElement( element: ExcalidrawRectanguloidElement, offset: number = 0, ): [LineSegment[], Curve[]] { - const roundness = getCornerRadius( + const cachedShape = getElementShapesCacheEntry(element, offset); + + if (cachedShape) { + return cachedShape; + } + + let radius = getCornerRadius( Math.min(element.width, element.height), element, ); - if (roundness <= 0) { - const r = rectangle( - pointFrom(element.x - offset, element.y - offset), - pointFrom( - element.x + element.width + offset, - element.y + element.height + offset, - ), - ); - - const top = lineSegment( - pointFrom(r[0][0] + roundness, r[0][1]), - pointFrom(r[1][0] - roundness, r[0][1]), - ); - const right = lineSegment( - pointFrom(r[1][0], r[0][1] + roundness), - pointFrom(r[1][0], r[1][1] - roundness), - ); - const bottom = lineSegment( - pointFrom(r[0][0] + roundness, r[1][1]), - pointFrom(r[1][0] - roundness, r[1][1]), - ); - const left = lineSegment( - pointFrom(r[0][0], r[1][1] - roundness), - pointFrom(r[0][0], r[0][1] + roundness), - ); - const sides = [top, right, bottom, left]; - - return [sides, []]; + if (radius === 0) { + radius = 0.01; } - const center = elementCenterPoint(element); - const r = rectangle( pointFrom(element.x, element.y), pointFrom(element.x + element.width, element.y + element.height), ); const top = lineSegment( - pointFrom(r[0][0] + roundness, r[0][1]), - pointFrom(r[1][0] - roundness, r[0][1]), + pointFrom(r[0][0] + radius, r[0][1]), + pointFrom(r[1][0] - radius, r[0][1]), ); const right = lineSegment( - pointFrom(r[1][0], r[0][1] + roundness), - pointFrom(r[1][0], r[1][1] - roundness), + pointFrom(r[1][0], r[0][1] + radius), + pointFrom(r[1][0], r[1][1] - radius), ); const bottom = lineSegment( - pointFrom(r[0][0] + roundness, r[1][1]), - pointFrom(r[1][0] - roundness, r[1][1]), + pointFrom(r[0][0] + radius, r[1][1]), + pointFrom(r[1][0] - radius, r[1][1]), ); const left = lineSegment( - pointFrom(r[0][0], r[1][1] - roundness), - pointFrom(r[0][0], r[0][1] + roundness), + pointFrom(r[0][0], r[1][1] - radius), + pointFrom(r[0][0], r[0][1] + radius), ); - const offsets = [ - vectorScale( - vectorNormalize( - vectorFromPoint(pointFrom(r[0][0] - offset, r[0][1] - offset), center), - ), - offset, - ), // TOP LEFT - vectorScale( - vectorNormalize( - vectorFromPoint(pointFrom(r[1][0] + offset, r[0][1] - offset), center), - ), - offset, - ), //TOP RIGHT - vectorScale( - vectorNormalize( - vectorFromPoint(pointFrom(r[1][0] + offset, r[1][1] + offset), center), - ), - offset, - ), // BOTTOM RIGHT - vectorScale( - vectorNormalize( - vectorFromPoint(pointFrom(r[0][0] - offset, r[1][1] + offset), center), - ), - offset, - ), // BOTTOM LEFT - ]; - - const corners = [ + const baseCorners = [ curve( - pointFromVector(offsets[0], left[1]), - pointFromVector( - offsets[0], - pointFrom( - left[1][0] + (2 / 3) * (r[0][0] - left[1][0]), - left[1][1] + (2 / 3) * (r[0][1] - left[1][1]), - ), + left[1], + pointFrom( + left[1][0] + (2 / 3) * (r[0][0] - left[1][0]), + left[1][1] + (2 / 3) * (r[0][1] - left[1][1]), ), - pointFromVector( - offsets[0], - pointFrom( - top[0][0] + (2 / 3) * (r[0][0] - top[0][0]), - top[0][1] + (2 / 3) * (r[0][1] - top[0][1]), - ), + pointFrom( + top[0][0] + (2 / 3) * (r[0][0] - top[0][0]), + top[0][1] + (2 / 3) * (r[0][1] - top[0][1]), ), - pointFromVector(offsets[0], top[0]), + top[0], ), // TOP LEFT curve( - pointFromVector(offsets[1], top[1]), - pointFromVector( - offsets[1], - pointFrom( - top[1][0] + (2 / 3) * (r[1][0] - top[1][0]), - top[1][1] + (2 / 3) * (r[0][1] - top[1][1]), - ), + top[1], + pointFrom( + top[1][0] + (2 / 3) * (r[1][0] - top[1][0]), + top[1][1] + (2 / 3) * (r[0][1] - top[1][1]), ), - pointFromVector( - offsets[1], - pointFrom( - right[0][0] + (2 / 3) * (r[1][0] - right[0][0]), - right[0][1] + (2 / 3) * (r[0][1] - right[0][1]), - ), + pointFrom( + right[0][0] + (2 / 3) * (r[1][0] - right[0][0]), + right[0][1] + (2 / 3) * (r[0][1] - right[0][1]), ), - pointFromVector(offsets[1], right[0]), + right[0], ), // TOP RIGHT curve( - pointFromVector(offsets[2], right[1]), - pointFromVector( - offsets[2], - pointFrom( - right[1][0] + (2 / 3) * (r[1][0] - right[1][0]), - right[1][1] + (2 / 3) * (r[1][1] - right[1][1]), - ), + right[1], + pointFrom( + right[1][0] + (2 / 3) * (r[1][0] - right[1][0]), + right[1][1] + (2 / 3) * (r[1][1] - right[1][1]), ), - pointFromVector( - offsets[2], - pointFrom( - bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]), - bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]), - ), + pointFrom( + bottom[1][0] + (2 / 3) * (r[1][0] - bottom[1][0]), + bottom[1][1] + (2 / 3) * (r[1][1] - bottom[1][1]), ), - pointFromVector(offsets[2], bottom[1]), + bottom[1], ), // BOTTOM RIGHT curve( - pointFromVector(offsets[3], bottom[0]), - pointFromVector( - offsets[3], - pointFrom( - bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]), - bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]), - ), + bottom[0], + pointFrom( + bottom[0][0] + (2 / 3) * (r[0][0] - bottom[0][0]), + bottom[0][1] + (2 / 3) * (r[1][1] - bottom[0][1]), ), - pointFromVector( - offsets[3], - pointFrom( - left[0][0] + (2 / 3) * (r[0][0] - left[0][0]), - left[0][1] + (2 / 3) * (r[1][1] - left[0][1]), - ), + pointFrom( + left[0][0] + (2 / 3) * (r[0][0] - left[0][0]), + left[0][1] + (2 / 3) * (r[1][1] - left[0][1]), ), - pointFromVector(offsets[3], left[0]), + left[0], ), // BOTTOM LEFT ]; - const sides = [ - lineSegment(corners[0][3], corners[1][0]), - lineSegment(corners[1][3], corners[2][0]), - lineSegment(corners[2][3], corners[3][0]), - lineSegment(corners[3][3], corners[0][0]), - ]; + const corners = + offset > 0 + ? baseCorners.map( + (corner) => + curveCatmullRomCubicApproxPoints( + curveOffsetPoints(corner, offset), + )!, + ) + : [ + [baseCorners[0]], + [baseCorners[1]], + [baseCorners[2]], + [baseCorners[3]], + ]; - return [sides, corners]; + const sides = [ + lineSegment( + corners[0][corners[0].length - 1][3], + corners[1][0][0], + ), + lineSegment( + corners[1][corners[1].length - 1][3], + corners[2][0][0], + ), + lineSegment( + corners[2][corners[2].length - 1][3], + corners[3][0][0], + ), + lineSegment( + corners[3][corners[3].length - 1][3], + corners[0][0][0], + ), + ]; + const shape = [sides, corners.flat()] as ElementShape; + + setElementShapesCacheEntry(element, shape, offset); + + return shape; } /** @@ -218,42 +315,20 @@ export function deconstructDiamondElement( element: ExcalidrawDiamondElement, offset: number = 0, ): [LineSegment[], Curve[]] { - const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = - getDiamondPoints(element); - const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); - const horizontalRadius = getCornerRadius(Math.abs(rightY - topY), element); + const cachedShape = getElementShapesCacheEntry(element, offset); - if (element.roundness?.type == null) { - const [top, right, bottom, left]: GlobalPoint[] = [ - pointFrom(element.x + topX, element.y + topY - offset), - pointFrom(element.x + rightX + offset, element.y + rightY), - pointFrom(element.x + bottomX, element.y + bottomY + offset), - pointFrom(element.x + leftX - offset, element.y + leftY), - ]; - - // Create the line segment parts of the diamond - // NOTE: Horizontal and vertical seems to be flipped here - const topRight = lineSegment( - pointFrom(top[0] + verticalRadius, top[1] + horizontalRadius), - pointFrom(right[0] - verticalRadius, right[1] - horizontalRadius), - ); - const bottomRight = lineSegment( - pointFrom(right[0] - verticalRadius, right[1] + horizontalRadius), - pointFrom(bottom[0] + verticalRadius, bottom[1] - horizontalRadius), - ); - const bottomLeft = lineSegment( - pointFrom(bottom[0] - verticalRadius, bottom[1] - horizontalRadius), - pointFrom(left[0] + verticalRadius, left[1] + horizontalRadius), - ); - const topLeft = lineSegment( - pointFrom(left[0] + verticalRadius, left[1] - horizontalRadius), - pointFrom(top[0] - verticalRadius, top[1] + horizontalRadius), - ); - - return [[topRight, bottomRight, bottomLeft, topLeft], []]; + if (cachedShape) { + return cachedShape; } - const center = elementCenterPoint(element); + const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = + getDiamondPoints(element); + const verticalRadius = element.roundness + ? getCornerRadius(Math.abs(topX - leftX), element) + : (topX - leftX) * 0.01; + const horizontalRadius = element.roundness + ? getCornerRadius(Math.abs(rightY - topY), element) + : (rightY - topY) * 0.01; const [top, right, bottom, left]: GlobalPoint[] = [ pointFrom(element.x + topX, element.y + topY), @@ -262,94 +337,94 @@ export function deconstructDiamondElement( pointFrom(element.x + leftX, element.y + leftY), ]; - const offsets = [ - vectorScale(vectorNormalize(vectorFromPoint(right, center)), offset), // RIGHT - vectorScale(vectorNormalize(vectorFromPoint(bottom, center)), offset), // BOTTOM - vectorScale(vectorNormalize(vectorFromPoint(left, center)), offset), // LEFT - vectorScale(vectorNormalize(vectorFromPoint(top, center)), offset), // TOP - ]; - - const corners = [ + const baseCorners = [ curve( - pointFromVector( - offsets[0], - pointFrom( - right[0] - verticalRadius, - right[1] - horizontalRadius, - ), + pointFrom( + right[0] - verticalRadius, + right[1] - horizontalRadius, ), - pointFromVector(offsets[0], right), - pointFromVector(offsets[0], right), - pointFromVector( - offsets[0], - pointFrom( - right[0] - verticalRadius, - right[1] + horizontalRadius, - ), + right, + right, + pointFrom( + right[0] - verticalRadius, + right[1] + horizontalRadius, ), ), // RIGHT curve( - pointFromVector( - offsets[1], - pointFrom( - bottom[0] + verticalRadius, - bottom[1] - horizontalRadius, - ), + pointFrom( + bottom[0] + verticalRadius, + bottom[1] - horizontalRadius, ), - pointFromVector(offsets[1], bottom), - pointFromVector(offsets[1], bottom), - pointFromVector( - offsets[1], - pointFrom( - bottom[0] - verticalRadius, - bottom[1] - horizontalRadius, - ), + bottom, + bottom, + pointFrom( + bottom[0] - verticalRadius, + bottom[1] - horizontalRadius, ), ), // BOTTOM curve( - pointFromVector( - offsets[2], - pointFrom( - left[0] + verticalRadius, - left[1] + horizontalRadius, - ), + pointFrom( + left[0] + verticalRadius, + left[1] + horizontalRadius, ), - pointFromVector(offsets[2], left), - pointFromVector(offsets[2], left), - pointFromVector( - offsets[2], - pointFrom( - left[0] + verticalRadius, - left[1] - horizontalRadius, - ), + left, + left, + pointFrom( + left[0] + verticalRadius, + left[1] - horizontalRadius, ), ), // LEFT curve( - pointFromVector( - offsets[3], - pointFrom( - top[0] - verticalRadius, - top[1] + horizontalRadius, - ), + pointFrom( + top[0] - verticalRadius, + top[1] + horizontalRadius, ), - pointFromVector(offsets[3], top), - pointFromVector(offsets[3], top), - pointFromVector( - offsets[3], - pointFrom( - top[0] + verticalRadius, - top[1] + horizontalRadius, - ), + top, + top, + pointFrom( + top[0] + verticalRadius, + top[1] + horizontalRadius, ), ), // TOP ]; + const corners = + offset > 0 + ? baseCorners.map( + (corner) => + curveCatmullRomCubicApproxPoints( + curveOffsetPoints(corner, offset), + )!, + ) + : [ + [baseCorners[0]], + [baseCorners[1]], + [baseCorners[2]], + [baseCorners[3]], + ]; + const sides = [ - lineSegment(corners[0][3], corners[1][0]), - lineSegment(corners[1][3], corners[2][0]), - lineSegment(corners[2][3], corners[3][0]), - lineSegment(corners[3][3], corners[0][0]), + lineSegment( + corners[0][corners[0].length - 1][3], + corners[1][0][0], + ), + lineSegment( + corners[1][corners[1].length - 1][3], + corners[2][0][0], + ), + lineSegment( + corners[2][corners[2].length - 1][3], + corners[3][0][0], + ), + lineSegment( + corners[3][corners[3].length - 1][3], + corners[0][0][0], + ), ]; - return [sides, corners]; + const shape = [sides, corners.flat()] as ElementShape; + + setElementShapesCacheEntry(element, shape, offset); + + return shape; } diff --git a/packages/element/tests/align.test.tsx b/packages/element/tests/align.test.tsx index 2dcafc65b8..afffb72cb4 100644 --- a/packages/element/tests/align.test.tsx +++ b/packages/element/tests/align.test.tsx @@ -35,6 +35,7 @@ const createAndSelectTwoRectangles = () => { // The second rectangle is already reselected because it was the last element created mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); }; @@ -52,6 +53,7 @@ const createAndSelectTwoRectanglesWithDifferentSizes = () => { // The second rectangle is already reselected because it was the last element created mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); }; @@ -202,6 +204,7 @@ describe("aligning", () => { // The second rectangle is already reselected because it was the last element created mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); @@ -215,6 +218,7 @@ describe("aligning", () => { // Add the created group to the current selection mouse.restorePosition(0, 0); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); }; @@ -316,6 +320,7 @@ describe("aligning", () => { // The second rectangle is already selected because it was the last element created mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); @@ -330,7 +335,7 @@ describe("aligning", () => { mouse.down(); mouse.up(100, 100); - mouse.restorePosition(200, 200); + mouse.restorePosition(210, 200); Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(); }); @@ -341,6 +346,7 @@ describe("aligning", () => { // The second group is already selected because it was the last group created mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); }; @@ -454,6 +460,7 @@ describe("aligning", () => { // The second rectangle is already reselected because it was the last element created mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); @@ -466,7 +473,7 @@ describe("aligning", () => { mouse.up(100, 100); // Add group to current selection - mouse.restorePosition(0, 0); + mouse.restorePosition(10, 0); Keyboard.withModifierKeys({ shift: true }, () => { mouse.click(); }); @@ -482,6 +489,7 @@ describe("aligning", () => { // Select the nested group, the rectangle is already selected mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); }; diff --git a/packages/element/tests/binding.test.tsx b/packages/element/tests/binding.test.tsx index f57d7793ae..bfc34af28c 100644 --- a/packages/element/tests/binding.test.tsx +++ b/packages/element/tests/binding.test.tsx @@ -172,12 +172,12 @@ describe("element binding", () => { const arrow = UI.createElement("arrow", { x: 0, y: 0, - size: 50, + size: 49, }); expect(arrow.endBinding).toBe(null); - mouse.downAt(50, 50); + mouse.downAt(49, 49); mouse.moveTo(51, 0); mouse.up(0, 0); diff --git a/packages/element/tests/collision.test.tsx b/packages/element/tests/collision.test.tsx new file mode 100644 index 0000000000..bcbf114f5e --- /dev/null +++ b/packages/element/tests/collision.test.tsx @@ -0,0 +1,38 @@ +import { type GlobalPoint, type LocalPoint, pointFrom } from "@excalidraw/math"; +import { Excalidraw } from "@excalidraw/excalidraw"; +import { UI } from "@excalidraw/excalidraw/tests/helpers/ui"; +import "@excalidraw/utils/test-utils"; +import { render } from "@excalidraw/excalidraw/tests/test-utils"; + +import { hitElementItself } from "../src/collision"; + +describe("check rotated elements can be hit:", () => { + beforeEach(async () => { + localStorage.clear(); + await render(); + }); + + it("arrow", () => { + UI.createElement("arrow", { + x: 0, + y: 0, + width: 124, + height: 302, + angle: 1.8700426423973724, + points: [ + [0, 0], + [120, -198], + [-4, -302], + ] as LocalPoint[], + }); + //const p = [120, -211]; + //const p = [0, 13]; + const hit = hitElementItself({ + point: pointFrom(87, -68), + element: window.h.elements[0], + threshold: 10, + elementsMap: window.h.scene.getNonDeletedElementsMap(), + }); + expect(hit).toBe(true); + }); +}); diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index b8e49c6339..35b1447801 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -1262,7 +1262,7 @@ describe("Test Linear Elements", () => { mouse.downAt(rect.x, rect.y); mouse.moveTo(200, 0); mouse.upAt(200, 0); - expect(arrow.width).toBeCloseTo(204, 0); + expect(arrow.width).toBeCloseTo(200, 0); expect(rect.x).toBe(200); expect(rect.y).toBe(0); expect(handleBindTextResizeSpy).toHaveBeenCalledWith( diff --git a/packages/element/tests/resize.test.tsx b/packages/element/tests/resize.test.tsx index 98fbf2a9a0..1d0b6ac0b2 100644 --- a/packages/element/tests/resize.test.tsx +++ b/packages/element/tests/resize.test.tsx @@ -510,12 +510,12 @@ describe("arrow element", () => { h.state, )[0] as ExcalidrawElbowArrowElement; - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); UI.resize(rectangle, "se", [-200, -150]); - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); }); @@ -538,11 +538,11 @@ describe("arrow element", () => { h.state, )[0] as ExcalidrawElbowArrowElement; - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(1.05); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.75); UI.resize([rectangle, arrow], "nw", [300, 350]); - expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(0); + expect(arrow.startBinding?.fixedPoint?.[0]).toBeCloseTo(-0.05); expect(arrow.startBinding?.fixedPoint?.[1]).toBeCloseTo(0.25); }); }); @@ -819,7 +819,7 @@ describe("image element", () => { UI.resize(image, "ne", [40, 0]); - expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(31, 0); + expect(arrow.width + arrow.endBinding!.gap).toBeCloseTo(30, 0); const imageWidth = image.width; const scale = 20 / image.height; @@ -1033,7 +1033,7 @@ describe("multiple selection", () => { expect(leftBoundArrow.x).toBeCloseTo(-110); expect(leftBoundArrow.y).toBeCloseTo(50); - expect(leftBoundArrow.width).toBeCloseTo(143, 0); + expect(leftBoundArrow.width).toBeCloseTo(140, 0); expect(leftBoundArrow.height).toBeCloseTo(7, 0); expect(leftBoundArrow.angle).toEqual(0); expect(leftBoundArrow.startBinding).toBeNull(); diff --git a/packages/element/tests/sizeHelpers.test.ts b/packages/element/tests/sizeHelpers.test.ts index 168a9a2adf..4e589fee7f 100644 --- a/packages/element/tests/sizeHelpers.test.ts +++ b/packages/element/tests/sizeHelpers.test.ts @@ -1,7 +1,5 @@ import { vi } from "vitest"; -import * as constants from "@excalidraw/common"; - import { getPerfectElementSize } from "../src/sizeHelpers"; const EPSILON_DIGITS = 3; @@ -57,13 +55,4 @@ describe("getPerfectElementSize", () => { expect(width).toBeCloseTo(0, EPSILON_DIGITS); expect(height).toBeCloseTo(0, EPSILON_DIGITS); }); - - describe("should respond to SHIFT_LOCKING_ANGLE constant", () => { - it("should have only 2 locking angles per section if SHIFT_LOCKING_ANGLE = 45 deg (Math.PI/4)", () => { - (constants as any).SHIFT_LOCKING_ANGLE = Math.PI / 4; - const { height, width } = getPerfectElementSize("arrow", 120, 185); - expect(width).toBeCloseTo(120, EPSILON_DIGITS); - expect(height).toBeCloseTo(120, EPSILON_DIGITS); - }); - }); }); diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 30c9e74343..62c6bae91b 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -18,7 +18,6 @@ import { arrayToMap, getFontFamilyString, getShortcutKey, - tupleToCoors, getLineHeight, isTransparent, reduceToCommonValue, @@ -28,9 +27,7 @@ import { canBecomePolygon, getNonDeletedElements } from "@excalidraw/element"; import { bindLinearElement, - bindPointToSnapToElementOutline, calculateFixedPointForElbowArrowBinding, - getHoveredElementForBinding, updateBoundElements, } from "@excalidraw/element"; @@ -1661,63 +1658,16 @@ export const actionChangeArrowType = register({ -1, elementsMap, ); - const startHoveredElement = - !newElement.startBinding && - getHoveredElementForBinding( - tupleToCoors(startGlobalPoint), - elements, - elementsMap, - appState.zoom, - false, - true, - ); - const endHoveredElement = - !newElement.endBinding && - getHoveredElementForBinding( - tupleToCoors(endGlobalPoint), - elements, - elementsMap, - appState.zoom, - false, - true, - ); - const startElement = startHoveredElement - ? startHoveredElement - : newElement.startBinding && - (elementsMap.get( - newElement.startBinding.elementId, - ) as ExcalidrawBindableElement); - const endElement = endHoveredElement - ? endHoveredElement - : newElement.endBinding && - (elementsMap.get( - newElement.endBinding.elementId, - ) as ExcalidrawBindableElement); - - const finalStartPoint = startHoveredElement - ? bindPointToSnapToElementOutline( - newElement, - startHoveredElement, - "start", - ) - : startGlobalPoint; - const finalEndPoint = endHoveredElement - ? bindPointToSnapToElementOutline( - newElement, - endHoveredElement, - "end", - ) - : endGlobalPoint; - - startHoveredElement && - bindLinearElement( - newElement, - startHoveredElement, - "start", - app.scene, - ); - endHoveredElement && - bindLinearElement(newElement, endHoveredElement, "end", app.scene); + const startElement = + newElement.startBinding && + (elementsMap.get( + newElement.startBinding.elementId, + ) as ExcalidrawBindableElement); + const endElement = + newElement.endBinding && + (elementsMap.get( + newElement.endBinding.elementId, + ) as ExcalidrawBindableElement); const startBinding = startElement && newElement.startBinding @@ -1728,6 +1678,7 @@ export const actionChangeArrowType = register({ newElement, startElement, "start", + elementsMap, ), } : null; @@ -1740,6 +1691,7 @@ export const actionChangeArrowType = register({ newElement, endElement, "end", + elementsMap, ), } : null; @@ -1749,7 +1701,7 @@ export const actionChangeArrowType = register({ startBinding, endBinding, ...updateElbowArrowPoints(newElement, elementsMap, { - points: [finalStartPoint, finalEndPoint].map( + points: [startGlobalPoint, endGlobalPoint].map( (p): LocalPoint => pointFrom(p[0] - newElement.x, p[1] - newElement.y), ), diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index b45a6f7d30..75e99768c6 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -10,6 +10,7 @@ import { STATS_PANELS, THEME, DEFAULT_GRID_STEP, + isTestEnv, } from "@excalidraw/common"; import type { AppState, NormalizedZoomValue } from "./types"; @@ -36,7 +37,7 @@ export const getDefaultAppState = (): Omit< currentItemRoughness: DEFAULT_ELEMENT_PROPS.roughness, currentItemStartArrowhead: null, currentItemStrokeColor: DEFAULT_ELEMENT_PROPS.strokeColor, - currentItemRoundness: "round", + currentItemRoundness: isTestEnv() ? "sharp" : "round", currentItemArrowType: ARROW_TYPE.round, currentItemStrokeStyle: DEFAULT_ELEMENT_PROPS.strokeStyle, currentItemStrokeWidth: DEFAULT_ELEMENT_PROPS.strokeWidth, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 15b5e5b6fd..b1ec1aebff 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -17,8 +17,6 @@ import { vectorDot, vectorNormalize, } from "@excalidraw/math"; -import { isPointInShape } from "@excalidraw/utils/collision"; -import { getSelectionBoxShape } from "@excalidraw/utils/shape"; import { COLOR_PALETTE, @@ -104,9 +102,9 @@ import { Emitter, } from "@excalidraw/common"; -import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element"; - import { + getCommonBounds, + getElementAbsoluteCoords, bindOrUnbindLinearElements, fixBindingsAfterDeletion, getHoveredElementForBinding, @@ -115,13 +113,8 @@ import { shouldEnableBindingForPointerEvent, updateBoundElements, getSuggestedBindingsForArrows, -} from "@excalidraw/element"; - -import { LinearElementEditor } from "@excalidraw/element"; - -import { newElementWith } from "@excalidraw/element"; - -import { + LinearElementEditor, + newElementWith, newFrameElement, newFreeDrawElement, newEmbeddableElement, @@ -133,11 +126,8 @@ import { newLinearElement, newTextElement, refreshTextDimensions, -} from "@excalidraw/element"; - -import { deepCopyElement, duplicateElements } from "@excalidraw/element"; - -import { + deepCopyElement, + duplicateElements, hasBoundTextElement, isArrowElement, isBindingElement, @@ -158,48 +148,27 @@ import { isFlowchartNodeElement, isBindableElement, isTextElement, -} from "@excalidraw/element"; - -import { getLockedLinearCursorAlignSize, getNormalizedDimensions, isElementCompletelyInViewport, isElementInViewport, isInvisiblySmallElement, -} from "@excalidraw/element"; - -import { - getBoundTextShape, getCornerRadius, - getElementShape, isPathALoop, -} from "@excalidraw/element"; - -import { createSrcDoc, embeddableURLValidator, maybeParseEmbedSrc, getEmbedLink, -} from "@excalidraw/element"; - -import { getInitializedImageElements, loadHTMLImageElement, normalizeSVG, updateImageCache as _updateImageCache, -} from "@excalidraw/element"; - -import { getBoundTextElement, getContainerCenter, getContainerElement, isValidTextContainer, redrawTextBoundingBox, -} from "@excalidraw/element"; - -import { shouldShowBoundingBox } from "@excalidraw/element"; - -import { + shouldShowBoundingBox, getFrameChildren, isCursorInFrame, addElementsToFrame, @@ -214,29 +183,17 @@ import { getFrameLikeTitle, getElementsOverlappingFrame, filterElementsEligibleAsFrameChildren, -} from "@excalidraw/element"; - -import { hitElementBoundText, hitElementBoundingBoxOnly, hitElementItself, -} from "@excalidraw/element"; - -import { getVisibleSceneBounds } from "@excalidraw/element"; - -import { + getVisibleSceneBounds, FlowChartCreator, FlowChartNavigator, getLinkDirectionFromKey, -} from "@excalidraw/element"; - -import { cropElement } from "@excalidraw/element"; - -import { wrapText } from "@excalidraw/element"; - -import { isElementLink, parseElementLinkFromURL } from "@excalidraw/element"; - -import { + cropElement, + wrapText, + isElementLink, + parseElementLinkFromURL, isMeasureTextSupported, normalizeText, measureText, @@ -244,13 +201,8 @@ import { getApproxMinLineWidth, getApproxMinLineHeight, getMinTextElementWidth, -} from "@excalidraw/element"; - -import { ShapeCache } from "@excalidraw/element"; - -import { getRenderOpacity } from "@excalidraw/element"; - -import { + ShapeCache, + getRenderOpacity, editGroupForSelectedElement, getElementsInGroup, getSelectedGroupIdForElement, @@ -258,42 +210,28 @@ import { isElementInGroup, isSelectedViaGroup, selectGroupsForSelectedElements, -} from "@excalidraw/element"; - -import { syncInvalidIndices, syncMovedIndices } from "@excalidraw/element"; - -import { + syncInvalidIndices, + syncMovedIndices, excludeElementsInFramesFromSelection, getSelectionStateForElements, makeNextSelectedElementIds, -} from "@excalidraw/element"; - -import { getResizeOffsetXY, getResizeArrowDirection, transformElements, -} from "@excalidraw/element"; - -import { getCursorForResizingElement, getElementWithTransformHandleType, getTransformHandleTypeFromCoords, -} from "@excalidraw/element"; - -import { dragNewElement, dragSelectedElements, getDragOffsetXY, + isNonDeletedElement, + Scene, + Store, + CaptureUpdateAction, + type ElementUpdate, + hitElementBoundingBox, } from "@excalidraw/element"; -import { isNonDeletedElement } from "@excalidraw/element"; - -import { Scene } from "@excalidraw/element"; - -import { Store, CaptureUpdateAction } from "@excalidraw/element"; - -import type { ElementUpdate } from "@excalidraw/element"; - import type { LocalPoint, Radians } from "@excalidraw/math"; import type { @@ -5095,6 +5033,7 @@ class App extends React.Component { return null; } + // NOTE: Hot path for hit testing, so avoid unnecessary computations private getElementAtPosition( x: number, y: number, @@ -5134,16 +5073,12 @@ class App extends React.Component { // If we're hitting element with highest z-index only on its bounding box // while also hitting other element figure, the latter should be considered. return hitElementItself({ - x, - y, + point: pointFrom(x, y), element: elementWithHighestZIndex, - shape: getElementShape( - elementWithHighestZIndex, - this.scene.getNonDeletedElementsMap(), - ), // when overlapping, we would like to be more precise // this also avoids the need to update past tests - threshold: this.getElementHitThreshold() / 2, + threshold: this.getElementHitThreshold(elementWithHighestZIndex) / 2, + elementsMap: this.scene.getNonDeletedElementsMap(), frameNameBound: isFrameLikeElement(elementWithHighestZIndex) ? this.frameNameBoundsCache.get(elementWithHighestZIndex) : null, @@ -5158,6 +5093,7 @@ class App extends React.Component { return null; } + // NOTE: Hot path for hit testing, so avoid unnecessary computations private getElementsAtPosition( x: number, y: number, @@ -5208,8 +5144,14 @@ class App extends React.Component { return elements; } - getElementHitThreshold() { - return DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value; + getElementHitThreshold(element: ExcalidrawElement) { + return Math.max( + element.strokeWidth / 2 + 0.1, + // NOTE: Here be dragons. Do not go under the 0.63 multiplier unless you're + // willing to test extensively. The hit testing starts to become unreliable + // due to FP imprecision under 0.63 in high zoom levels. + 0.85 * (DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value), + ); } private hitElement( @@ -5224,35 +5166,35 @@ class App extends React.Component { this.state.selectedElementIds[element.id] && shouldShowBoundingBox([element], this.state) ) { - const selectionShape = getSelectionBoxShape( - element, - this.scene.getNonDeletedElementsMap(), - isImageElement(element) ? 0 : this.getElementHitThreshold(), - ); - // if hitting the bounding box, return early // but if not, we should check for other cases as well (e.g. frame name) - if (isPointInShape(pointFrom(x, y), selectionShape)) { + if ( + hitElementBoundingBox( + pointFrom(x, y), + element, + this.scene.getNonDeletedElementsMap(), + this.getElementHitThreshold(element), + ) + ) { return true; } } // take bound text element into consideration for hit collision as well const hitBoundTextOfElement = hitElementBoundText( - x, - y, - getBoundTextShape(element, this.scene.getNonDeletedElementsMap()), + pointFrom(x, y), + element, + this.scene.getNonDeletedElementsMap(), ); if (hitBoundTextOfElement) { return true; } return hitElementItself({ - x, - y, + point: pointFrom(x, y), element, - shape: getElementShape(element, this.scene.getNonDeletedElementsMap()), - threshold: this.getElementHitThreshold(), + threshold: this.getElementHitThreshold(element), + elementsMap: this.scene.getNonDeletedElementsMap(), frameNameBound: isFrameLikeElement(element) ? this.frameNameBoundsCache.get(element) : null, @@ -5280,14 +5222,10 @@ class App extends React.Component { if ( isArrowElement(elements[index]) && hitElementItself({ - x, - y, + point: pointFrom(x, y), element: elements[index], - shape: getElementShape( - elements[index], - this.scene.getNonDeletedElementsMap(), - ), - threshold: this.getElementHitThreshold(), + elementsMap: this.scene.getNonDeletedElementsMap(), + threshold: this.getElementHitThreshold(elements[index]), }) ) { hitElement = elements[index]; @@ -5632,14 +5570,10 @@ class App extends React.Component { hasBoundTextElement(container) || !isTransparent(container.backgroundColor) || hitElementItself({ - x: sceneX, - y: sceneY, + point: pointFrom(sceneX, sceneY), element: container, - shape: getElementShape( - container, - this.scene.getNonDeletedElementsMap(), - ), - threshold: this.getElementHitThreshold(), + elementsMap: this.scene.getNonDeletedElementsMap(), + threshold: this.getElementHitThreshold(container), }) ) { const midPoint = getContainerCenter( @@ -6329,13 +6263,10 @@ class App extends React.Component { let segmentMidPointHoveredCoords = null; if ( hitElementItself({ - x: scenePointerX, - y: scenePointerY, + point: pointFrom(scenePointerX, scenePointerY), element, - shape: getElementShape( - element, - this.scene.getNonDeletedElementsMap(), - ), + elementsMap, + threshold: this.getElementHitThreshold(element), }) ) { hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor( @@ -7505,7 +7436,10 @@ class App extends React.Component { } // How many pixels off the shape boundary we still consider a hit - const threshold = this.getElementHitThreshold(); + const threshold = Math.max( + DEFAULT_COLLISION_THRESHOLD / this.state.zoom.value, + 1, + ); const [x1, y1, x2, y2] = getCommonBounds(selectedElements); return ( point.x > x1 - threshold && @@ -9768,14 +9702,13 @@ class App extends React.Component { ((hitElement && hitElementBoundingBoxOnly( { - x: pointerDownState.origin.x, - y: pointerDownState.origin.y, - element: hitElement, - shape: getElementShape( - hitElement, - this.scene.getNonDeletedElementsMap(), + point: pointFrom( + pointerDownState.origin.x, + pointerDownState.origin.y, ), - threshold: this.getElementHitThreshold(), + element: hitElement, + elementsMap, + threshold: this.getElementHitThreshold(hitElement), frameNameBound: isFrameLikeElement(hitElement) ? this.frameNameBoundsCache.get(hitElement) : null, @@ -10882,6 +10815,7 @@ class App extends React.Component { croppingElement, cropElement( croppingElement, + this.scene.getNonDeletedElementsMap(), transformHandleType, image.naturalWidth, image.naturalHeight, diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index cc1cfce984..05163a32f3 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -133,7 +133,6 @@ describe("binding with linear elements", () => { const inputX = UI.queryStatsProperty("X")?.querySelector( ".drag-input", ) as HTMLInputElement; - expect(linear.startBinding).not.toBe(null); expect(inputX).not.toBeNull(); UI.updateInput(inputX, String("204")); @@ -657,6 +656,7 @@ describe("stats for multiple elements", () => { mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); diff --git a/packages/excalidraw/components/hyperlink/Hyperlink.tsx b/packages/excalidraw/components/hyperlink/Hyperlink.tsx index 292659bdb6..5e380e4e6e 100644 --- a/packages/excalidraw/components/hyperlink/Hyperlink.tsx +++ b/packages/excalidraw/components/hyperlink/Hyperlink.tsx @@ -463,7 +463,7 @@ const shouldHideLinkPopup = ( const threshold = 15 / appState.zoom.value; // hitbox to prevent hiding when hovered in element bounding box - if (hitElementBoundingBox(sceneX, sceneY, element, elementsMap)) { + if (hitElementBoundingBox(pointFrom(sceneX, sceneY), element, elementsMap)) { return false; } const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap); diff --git a/packages/excalidraw/components/hyperlink/helpers.ts b/packages/excalidraw/components/hyperlink/helpers.ts index 7d39b7ff74..e89ecdc554 100644 --- a/packages/excalidraw/components/hyperlink/helpers.ts +++ b/packages/excalidraw/components/hyperlink/helpers.ts @@ -92,7 +92,7 @@ export const isPointHittingLink = ( if ( !isMobile && appState.viewModeEnabled && - hitElementBoundingBox(x, y, element, elementsMap) + hitElementBoundingBox(pointFrom(x, y), element, elementsMap) ) { return true; } diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 48337288a9..1bc3ce9253 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -175,7 +175,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s "startBinding": { "elementId": "diamond-1", "focus": 0, - "gap": 4.545343408287929, + "gap": 4.535423522449215, }, "strokeColor": "#e67700", "strokeStyle": "solid", @@ -335,7 +335,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t "endBinding": { "elementId": "text-2", "focus": 0, - "gap": 14, + "gap": 16, }, "fillStyle": "solid", "frameId": null, @@ -1540,7 +1540,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "endBinding": { "elementId": "B", "focus": 0, - "gap": 14, + "gap": 32, }, "fillStyle": "solid", "frameId": null, diff --git a/packages/excalidraw/data/transform.test.ts b/packages/excalidraw/data/transform.test.ts index 0b0718e8e3..0d9fcf3161 100644 --- a/packages/excalidraw/data/transform.test.ts +++ b/packages/excalidraw/data/transform.test.ts @@ -781,7 +781,7 @@ describe("Test Transform", () => { expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ elementId: "rect-1", focus: -0, - gap: 14, + gap: 25, }); expect(rect.boundElements).toStrictEqual([ { diff --git a/packages/excalidraw/eraser/index.ts b/packages/excalidraw/eraser/index.ts index 5e6c4e5171..6fbe2bd4ca 100644 --- a/packages/excalidraw/eraser/index.ts +++ b/packages/excalidraw/eraser/index.ts @@ -1,25 +1,19 @@ import { arrayToMap, easeOut, THEME } from "@excalidraw/common"; -import { getElementLineSegments } from "@excalidraw/element"; import { - lineSegment, - lineSegmentIntersectionPoints, - pointFrom, -} from "@excalidraw/math"; + computeBoundTextPosition, + getBoundTextElement, + intersectElementWithLineSegment, + isPointInElement, +} from "@excalidraw/element"; +import { lineSegment, pointFrom } from "@excalidraw/math"; import { getElementsInGroup } from "@excalidraw/element"; -import { getElementShape } from "@excalidraw/element"; import { shouldTestInside } from "@excalidraw/element"; -import { isPointInShape } from "@excalidraw/utils/collision"; import { hasBoundTextElement, isBoundToContainer } from "@excalidraw/element"; import { getBoundTextElementId } from "@excalidraw/element"; -import type { GeometricShape } from "@excalidraw/utils/shape"; -import type { - ElementsSegmentsMap, - GlobalPoint, - LineSegment, -} from "@excalidraw/math/types"; +import type { GlobalPoint, LineSegment } from "@excalidraw/math/types"; import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import { AnimatedTrail } from "../animated-trail"; @@ -28,15 +22,9 @@ import type { AnimationFrameHandler } from "../animation-frame-handler"; import type App from "../components/App"; -// just enough to form a segment; this is sufficient for eraser -const POINTS_ON_TRAIL = 2; - export class EraserTrail extends AnimatedTrail { private elementsToErase: Set = new Set(); private groupsToErase: Set = new Set(); - private segmentsCache: Map[]> = new Map(); - private geometricShapesCache: Map> = - new Map(); constructor(animationFrameHandler: AnimationFrameHandler, app: App) { super(animationFrameHandler, app, { @@ -79,14 +67,21 @@ export class EraserTrail extends AnimatedTrail { } private updateElementsToBeErased(restoreToErase?: boolean) { - let eraserPath: GlobalPoint[] = + const eraserPath: GlobalPoint[] = super .getCurrentTrail() ?.originalPoints?.map((p) => pointFrom(p[0], p[1])) || []; + if (eraserPath.length < 2) { + return []; + } + // for efficiency and avoid unnecessary calculations, // take only POINTS_ON_TRAIL points to form some number of segments - eraserPath = eraserPath?.slice(eraserPath.length - POINTS_ON_TRAIL); + const pathSegment = lineSegment( + eraserPath[eraserPath.length - 1], + eraserPath[eraserPath.length - 2], + ); const candidateElements = this.app.visibleElements.filter( (el) => !el.locked, @@ -94,28 +89,13 @@ export class EraserTrail extends AnimatedTrail { const candidateElementsMap = arrayToMap(candidateElements); - const pathSegments = eraserPath.reduce((acc, point, index) => { - if (index === 0) { - return acc; - } - acc.push(lineSegment(eraserPath[index - 1], point)); - return acc; - }, [] as LineSegment[]); - - if (pathSegments.length === 0) { - return []; - } - for (const element of candidateElements) { // restore only if already added to the to-be-erased set if (restoreToErase && this.elementsToErase.has(element.id)) { const intersects = eraserTest( - pathSegments, + pathSegment, element, - this.segmentsCache, - this.geometricShapesCache, candidateElementsMap, - this.app, ); if (intersects) { @@ -148,12 +128,9 @@ export class EraserTrail extends AnimatedTrail { } } else if (!restoreToErase && !this.elementsToErase.has(element.id)) { const intersects = eraserTest( - pathSegments, + pathSegment, element, - this.segmentsCache, - this.geometricShapesCache, candidateElementsMap, - this.app, ); if (intersects) { @@ -196,45 +173,37 @@ export class EraserTrail extends AnimatedTrail { super.clearTrails(); this.elementsToErase.clear(); this.groupsToErase.clear(); - this.segmentsCache.clear(); } } const eraserTest = ( - pathSegments: LineSegment[], + pathSegment: LineSegment, element: ExcalidrawElement, - elementsSegments: ElementsSegmentsMap, - shapesCache: Map>, elementsMap: ElementsMap, - app: App, ): boolean => { - let shape = shapesCache.get(element.id); - - if (!shape) { - shape = getElementShape(element, elementsMap); - shapesCache.set(element.id, shape); - } - - const lastPoint = pathSegments[pathSegments.length - 1][1]; - if (shouldTestInside(element) && isPointInShape(lastPoint, shape)) { + const lastPoint = pathSegment[1]; + if ( + shouldTestInside(element) && + isPointInElement(lastPoint, element, elementsMap) + ) { return true; } - let elementSegments = elementsSegments.get(element.id); + const boundTextElement = getBoundTextElement(element, elementsMap); - if (!elementSegments) { - elementSegments = getElementLineSegments(element, elementsMap); - elementsSegments.set(element.id, elementSegments); - } - - return pathSegments.some((pathSegment) => - elementSegments?.some( - (elementSegment) => - lineSegmentIntersectionPoints( - pathSegment, - elementSegment, - app.getElementHitThreshold(), - ) !== null, - ), + return ( + intersectElementWithLineSegment(element, elementsMap, pathSegment, 0, true) + .length > 0 || + (!!boundTextElement && + intersectElementWithLineSegment( + { + ...boundTextElement, + ...computeBoundTextPosition(element, boundTextElement, elementsMap), + }, + elementsMap, + pathSegment, + 0, + true, + ).length > 0) ); }; diff --git a/packages/excalidraw/lasso/index.ts b/packages/excalidraw/lasso/index.ts index 163a8b7a98..5d9f704583 100644 --- a/packages/excalidraw/lasso/index.ts +++ b/packages/excalidraw/lasso/index.ts @@ -199,6 +199,7 @@ export class LassoTrail extends AnimatedTrail { const { selectedElementIds } = getLassoSelectedElementIds({ lassoPath, elements: this.app.visibleElements, + elementsMap: this.app.scene.getNonDeletedElementsMap(), elementsSegments: this.elementsSegments, intersectedElements: this.intersectedElements, enclosedElements: this.enclosedElements, diff --git a/packages/excalidraw/lasso/utils.ts b/packages/excalidraw/lasso/utils.ts index 0721ccf0ef..2cab64662b 100644 --- a/packages/excalidraw/lasso/utils.ts +++ b/packages/excalidraw/lasso/utils.ts @@ -3,20 +3,25 @@ import { simplify } from "points-on-curve"; import { polygonFromPoints, lineSegment, - lineSegmentIntersectionPoints, polygonIncludesPointNonZero, } from "@excalidraw/math"; -import type { - ElementsSegmentsMap, - GlobalPoint, - LineSegment, -} from "@excalidraw/math/types"; -import type { ExcalidrawElement } from "@excalidraw/element/types"; +import { + type Bounds, + computeBoundTextPosition, + doBoundsIntersect, + getBoundTextElement, + getElementBounds, + intersectElementWithLineSegment, +} from "@excalidraw/element"; + +import type { ElementsSegmentsMap, GlobalPoint } from "@excalidraw/math/types"; +import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; export const getLassoSelectedElementIds = (input: { lassoPath: GlobalPoint[]; elements: readonly ExcalidrawElement[]; + elementsMap: ElementsMap; elementsSegments: ElementsSegmentsMap; intersectedElements: Set; enclosedElements: Set; @@ -27,6 +32,7 @@ export const getLassoSelectedElementIds = (input: { const { lassoPath, elements, + elementsMap, elementsSegments, intersectedElements, enclosedElements, @@ -40,8 +46,26 @@ export const getLassoSelectedElementIds = (input: { const unlockedElements = elements.filter((el) => !el.locked); // as the path might not enclose a shape anymore, clear before checking enclosedElements.clear(); + intersectedElements.clear(); + const lassoBounds = lassoPath.reduce( + (acc, item) => { + return [ + Math.min(acc[0], item[0]), + Math.min(acc[1], item[1]), + Math.max(acc[2], item[0]), + Math.max(acc[3], item[1]), + ]; + }, + [Infinity, Infinity, -Infinity, -Infinity], + ) as Bounds; for (const element of unlockedElements) { + // First check if the lasso segment intersects the element's axis-aligned + // bounding box as it is much faster than checking intersection against + // the element's shape + const elementBounds = getElementBounds(element, elementsMap); + if ( + doBoundsIntersect(lassoBounds, elementBounds) && !intersectedElements.has(element.id) && !enclosedElements.has(element.id) ) { @@ -49,7 +73,7 @@ export const getLassoSelectedElementIds = (input: { if (enclosed) { enclosedElements.add(element.id); } else { - const intersects = intersectionTest(path, element, elementsSegments); + const intersects = intersectionTest(path, element, elementsMap); if (intersects) { intersectedElements.add(element.id); } @@ -85,26 +109,34 @@ const enclosureTest = ( const intersectionTest = ( lassoPath: GlobalPoint[], element: ExcalidrawElement, - elementsSegments: ElementsSegmentsMap, + elementsMap: ElementsMap, ): boolean => { - const elementSegments = elementsSegments.get(element.id); - if (!elementSegments) { - return false; - } + const lassoSegments = lassoPath + .slice(1) + .map((point: GlobalPoint, index) => lineSegment(lassoPath[index], point)) + .concat([lineSegment(lassoPath[lassoPath.length - 1], lassoPath[0])]); - const lassoSegments = lassoPath.reduce((acc, point, index) => { - if (index === 0) { - return acc; - } - acc.push(lineSegment(lassoPath[index - 1], point)); - return acc; - }, [] as LineSegment[]); + const boundTextElement = getBoundTextElement(element, elementsMap); - return lassoSegments.some((lassoSegment) => - elementSegments.some( - (elementSegment) => - // introduce a bit of tolerance to account for roughness and simplification of paths - lineSegmentIntersectionPoints(lassoSegment, elementSegment, 1) !== null, - ), + return lassoSegments.some( + (lassoSegment) => + intersectElementWithLineSegment( + element, + elementsMap, + lassoSegment, + 0, + true, + ).length > 0 || + (!!boundTextElement && + intersectElementWithLineSegment( + { + ...boundTextElement, + ...computeBoundTextPosition(element, boundTextElement, elementsMap), + }, + elementsMap, + lassoSegment, + 0, + true, + ).length > 0), ); }; diff --git a/packages/excalidraw/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts index e42f4166b0..9dd26df646 100644 --- a/packages/excalidraw/renderer/helpers.ts +++ b/packages/excalidraw/renderer/helpers.ts @@ -5,17 +5,14 @@ import { getDiamondPoints } from "@excalidraw/element"; import { getCornerRadius } from "@excalidraw/element"; import { - bezierEquation, curve, - curveTangent, + curveCatmullRomCubicApproxPoints, + curveCatmullRomQuadraticApproxPoints, + curveOffsetPoints, type GlobalPoint, + offsetPointsForQuadraticBezier, pointFrom, - pointFromVector, pointRotateRads, - vector, - vectorNormal, - vectorNormalize, - vectorScale, } from "@excalidraw/math"; import type { @@ -102,25 +99,14 @@ export const bootstrapCanvas = ({ function drawCatmullRomQuadraticApprox( ctx: CanvasRenderingContext2D, points: GlobalPoint[], - segments = 20, + tension = 0.5, ) { - ctx.lineTo(points[0][0], points[0][1]); + const pointSets = curveCatmullRomQuadraticApproxPoints(points, tension); + if (pointSets) { + for (let i = 0; i < pointSets.length - 1; i++) { + const [[cpX, cpY], [p2X, p2Y]] = pointSets[i]; - for (let i = 0; i < points.length - 1; i++) { - const p0 = points[i - 1 < 0 ? 0 : i - 1]; - const p1 = points[i]; - const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; - - for (let t = 0; t <= 1; t += 1 / segments) { - const t2 = t * t; - - const x = - (1 - t) * (1 - t) * p0[0] + 2 * (1 - t) * t * p1[0] + t2 * p2[0]; - - const y = - (1 - t) * (1 - t) * p0[1] + 2 * (1 - t) * t * p1[1] + t2 * p2[1]; - - ctx.lineTo(x, y); + ctx.quadraticCurveTo(cpX, cpY, p2X, p2Y); } } } @@ -128,35 +114,13 @@ function drawCatmullRomQuadraticApprox( function drawCatmullRomCubicApprox( ctx: CanvasRenderingContext2D, points: GlobalPoint[], - segments = 20, + tension = 0.5, ) { - ctx.lineTo(points[0][0], points[0][1]); - - for (let i = 0; i < points.length - 1; i++) { - const p0 = points[i - 1 < 0 ? 0 : i - 1]; - const p1 = points[i]; - const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; - const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2]; - - for (let t = 0; t <= 1; t += 1 / segments) { - const t2 = t * t; - const t3 = t2 * t; - - const x = - 0.5 * - (2 * p1[0] + - (-p0[0] + p2[0]) * t + - (2 * p0[0] - 5 * p1[0] + 4 * p2[0] - p3[0]) * t2 + - (-p0[0] + 3 * p1[0] - 3 * p2[0] + p3[0]) * t3); - - const y = - 0.5 * - (2 * p1[1] + - (-p0[1] + p2[1]) * t + - (2 * p0[1] - 5 * p1[1] + 4 * p2[1] - p3[1]) * t2 + - (-p0[1] + 3 * p1[1] - 3 * p2[1] + p3[1]) * t3); - - ctx.lineTo(x, y); + const pointSets = curveCatmullRomCubicApproxPoints(points, tension); + if (pointSets) { + for (let i = 0; i < pointSets.length; i++) { + const [[cp1x, cp1y], [cp2x, cp2y], [x, y]] = pointSets[i]; + ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } } } @@ -168,7 +132,10 @@ export const drawHighlightForRectWithRotation = ( ) => { const [x, y] = pointRotateRads( pointFrom(element.x, element.y), - elementCenterPoint(element), + elementCenterPoint( + element, + window.h.app.scene.getElementsMapIncludingDeleted(), + ), element.angle, ); @@ -187,25 +154,25 @@ export const drawHighlightForRectWithRotation = ( context.beginPath(); { - const topLeftApprox = offsetQuadraticBezier( + const topLeftApprox = offsetPointsForQuadraticBezier( pointFrom(0, 0 + radius), pointFrom(0, 0), pointFrom(0 + radius, 0), padding, ); - const topRightApprox = offsetQuadraticBezier( + const topRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width - radius, 0), pointFrom(element.width, 0), pointFrom(element.width, radius), padding, ); - const bottomRightApprox = offsetQuadraticBezier( + const bottomRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width, element.height - radius), pointFrom(element.width, element.height), pointFrom(element.width - radius, element.height), padding, ); - const bottomLeftApprox = offsetQuadraticBezier( + const bottomLeftApprox = offsetPointsForQuadraticBezier( pointFrom(radius, element.height), pointFrom(0, element.height), pointFrom(0, element.height - radius), @@ -230,25 +197,25 @@ export const drawHighlightForRectWithRotation = ( // mask" on a filled shape for the diamond highlight, because stroking creates // sharp inset edges on line joins < 90 degrees. { - const topLeftApprox = offsetQuadraticBezier( + const topLeftApprox = offsetPointsForQuadraticBezier( pointFrom(0 + radius, 0), pointFrom(0, 0), pointFrom(0, 0 + radius), -FIXED_BINDING_DISTANCE, ); - const topRightApprox = offsetQuadraticBezier( + const topRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width, radius), pointFrom(element.width, 0), pointFrom(element.width - radius, 0), -FIXED_BINDING_DISTANCE, ); - const bottomRightApprox = offsetQuadraticBezier( + const bottomRightApprox = offsetPointsForQuadraticBezier( pointFrom(element.width - radius, element.height), pointFrom(element.width, element.height), pointFrom(element.width, element.height - radius), -FIXED_BINDING_DISTANCE, ); - const bottomLeftApprox = offsetQuadraticBezier( + const bottomLeftApprox = offsetPointsForQuadraticBezier( pointFrom(0, element.height - radius), pointFrom(0, element.height), pointFrom(radius, element.height), @@ -325,7 +292,10 @@ export const drawHighlightForDiamondWithRotation = ( ) => { const [x, y] = pointRotateRads( pointFrom(element.x, element.y), - elementCenterPoint(element), + elementCenterPoint( + element, + window.h.app.scene.getElementsMapIncludingDeleted(), + ), element.angle, ); context.save(); @@ -343,32 +313,40 @@ export const drawHighlightForDiamondWithRotation = ( const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; - const topApprox = offsetCubicBezier( - pointFrom(topX - verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX + verticalRadius, topY + horizontalRadius), + const topApprox = curveOffsetPoints( + curve( + pointFrom(topX - verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX + verticalRadius, topY + horizontalRadius), + ), padding, ); - const rightApprox = offsetCubicBezier( - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + const rightApprox = curveOffsetPoints( + curve( + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + ), padding, ); - const bottomApprox = offsetCubicBezier( - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + const bottomApprox = curveOffsetPoints( + curve( + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + ), padding, ); - const leftApprox = offsetCubicBezier( - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + const leftApprox = curveOffsetPoints( + curve( + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + ), padding, ); @@ -376,13 +354,13 @@ export const drawHighlightForDiamondWithRotation = ( topApprox[topApprox.length - 1][0], topApprox[topApprox.length - 1][1], ); - context.lineTo(rightApprox[0][0], rightApprox[0][1]); + context.lineTo(rightApprox[1][0], rightApprox[1][1]); drawCatmullRomCubicApprox(context, rightApprox); - context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); + context.lineTo(bottomApprox[1][0], bottomApprox[1][1]); drawCatmullRomCubicApprox(context, bottomApprox); - context.lineTo(leftApprox[0][0], leftApprox[0][1]); + context.lineTo(leftApprox[1][0], leftApprox[1][1]); drawCatmullRomCubicApprox(context, leftApprox); - context.lineTo(topApprox[0][0], topApprox[0][1]); + context.lineTo(topApprox[1][0], topApprox[1][1]); drawCatmullRomCubicApprox(context, topApprox); } @@ -398,32 +376,40 @@ export const drawHighlightForDiamondWithRotation = ( const horizontalRadius = element.roundness ? getCornerRadius(Math.abs(rightY - topY), element) : (rightY - topY) * 0.01; - const topApprox = offsetCubicBezier( - pointFrom(topX + verticalRadius, topY + horizontalRadius), - pointFrom(topX, topY), - pointFrom(topX, topY), - pointFrom(topX - verticalRadius, topY + horizontalRadius), + const topApprox = curveOffsetPoints( + curve( + pointFrom(topX + verticalRadius, topY + horizontalRadius), + pointFrom(topX, topY), + pointFrom(topX, topY), + pointFrom(topX - verticalRadius, topY + horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); - const rightApprox = offsetCubicBezier( - pointFrom(rightX - verticalRadius, rightY + horizontalRadius), - pointFrom(rightX, rightY), - pointFrom(rightX, rightY), - pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + const rightApprox = curveOffsetPoints( + curve( + pointFrom(rightX - verticalRadius, rightY + horizontalRadius), + pointFrom(rightX, rightY), + pointFrom(rightX, rightY), + pointFrom(rightX - verticalRadius, rightY - horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); - const bottomApprox = offsetCubicBezier( - pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), - pointFrom(bottomX, bottomY), - pointFrom(bottomX, bottomY), - pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + const bottomApprox = curveOffsetPoints( + curve( + pointFrom(bottomX - verticalRadius, bottomY - horizontalRadius), + pointFrom(bottomX, bottomY), + pointFrom(bottomX, bottomY), + pointFrom(bottomX + verticalRadius, bottomY - horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); - const leftApprox = offsetCubicBezier( - pointFrom(leftX + verticalRadius, leftY - horizontalRadius), - pointFrom(leftX, leftY), - pointFrom(leftX, leftY), - pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + const leftApprox = curveOffsetPoints( + curve( + pointFrom(leftX + verticalRadius, leftY - horizontalRadius), + pointFrom(leftX, leftY), + pointFrom(leftX, leftY), + pointFrom(leftX + verticalRadius, leftY + horizontalRadius), + ), -FIXED_BINDING_DISTANCE, ); @@ -431,66 +417,16 @@ export const drawHighlightForDiamondWithRotation = ( topApprox[topApprox.length - 1][0], topApprox[topApprox.length - 1][1], ); - context.lineTo(leftApprox[0][0], leftApprox[0][1]); + context.lineTo(leftApprox[1][0], leftApprox[1][1]); drawCatmullRomCubicApprox(context, leftApprox); - context.lineTo(bottomApprox[0][0], bottomApprox[0][1]); + context.lineTo(bottomApprox[1][0], bottomApprox[1][1]); drawCatmullRomCubicApprox(context, bottomApprox); - context.lineTo(rightApprox[0][0], rightApprox[0][1]); + context.lineTo(rightApprox[1][0], rightApprox[1][1]); drawCatmullRomCubicApprox(context, rightApprox); - context.lineTo(topApprox[0][0], topApprox[0][1]); + context.lineTo(topApprox[1][0], topApprox[1][1]); drawCatmullRomCubicApprox(context, topApprox); } context.closePath(); context.fill(); context.restore(); }; - -function offsetCubicBezier( - p0: GlobalPoint, - p1: GlobalPoint, - p2: GlobalPoint, - p3: GlobalPoint, - offsetDist: number, - steps = 20, -) { - const offsetPoints = []; - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const c = curve(p0, p1, p2, p3); - const point = bezierEquation(c, t); - const tangent = vectorNormalize(curveTangent(c, t)); - const normal = vectorNormal(tangent); - - offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); - } - - return offsetPoints; -} - -function offsetQuadraticBezier( - p0: GlobalPoint, - p1: GlobalPoint, - p2: GlobalPoint, - offsetDist: number, - steps = 20, -) { - const offsetPoints = []; - - for (let i = 0; i <= steps; i++) { - const t = i / steps; - const t1 = 1 - t; - const point = pointFrom( - t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0], - t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1], - ); - const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]); - const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]); - const tangent = vectorNormalize(vector(tangentX, tangentY)); - const normal = vectorNormal(tangent); - - offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); - } - - return offsetPoints; -} diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 7d8b474858..40bce1c7d0 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -193,16 +193,10 @@ const renderBindingHighlightForBindableElement = ( elementsMap: ElementsMap, zoom: InteractiveCanvasAppState["zoom"], ) => { - const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); - const width = x2 - x1; - const height = y2 - y1; - - context.strokeStyle = "rgba(0,0,0,.05)"; - context.fillStyle = "rgba(0,0,0,.05)"; - - // To ensure the binding highlight doesn't overlap the element itself const padding = maxBindingGap(element, element.width, element.height, zoom); + context.fillStyle = "rgba(0,0,0,.05)"; + switch (element.type) { case "rectangle": case "text": @@ -216,10 +210,13 @@ const renderBindingHighlightForBindableElement = ( case "diamond": drawHighlightForDiamondWithRotation(context, padding, element); break; - case "ellipse": - context.lineWidth = - maxBindingGap(element, element.width, element.height, zoom) - - FIXED_BINDING_DISTANCE; + case "ellipse": { + const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); + const width = x2 - x1; + const height = y2 - y1; + + context.strokeStyle = "rgba(0,0,0,.05)"; + context.lineWidth = padding - FIXED_BINDING_DISTANCE; strokeEllipseWithRotation( context, @@ -230,6 +227,7 @@ const renderBindingHighlightForBindableElement = ( element.angle, ); break; + } } }; diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 23f4ccb4f2..54fce16156 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -898,7 +898,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1016,9 +1016,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1052,9 +1050,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1101,7 +1097,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1162,7 +1158,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "resizingElement": null, "scrollX": 0, "scrollY": 0, - "scrolledOutside": false, + "scrolledOutside": true, "searchMatches": null, "selectedElementIds": { "id0": true, @@ -1205,7 +1201,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "id": "id0", "index": "a0", "isDeleted": false, @@ -1213,20 +1209,18 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 449462985, + "roundness": null, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, - "width": 20, - "x": -10, - "y": 0, + "versionNonce": 2019559783, + "width": 10, + "x": -20, + "y": -10, } `; @@ -1263,23 +1257,21 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "width": 20, - "x": -10, - "y": 0, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, @@ -1317,7 +1309,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1427,9 +1419,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1461,9 +1451,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1518,9 +1506,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1572,9 +1558,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1650,7 +1634,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1760,9 +1744,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1794,9 +1776,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -1851,9 +1831,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1905,9 +1883,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1983,7 +1959,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2044,7 +2020,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "resizingElement": null, "scrollX": 0, "scrollY": 0, - "scrolledOutside": false, + "scrolledOutside": true, "searchMatches": null, "selectedElementIds": { "id0": true, @@ -2087,7 +2063,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "id": "id0", "index": "a0", "isDeleted": false, @@ -2095,20 +2071,18 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 449462985, + "roundness": null, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, - "width": 20, - "x": -10, - "y": 0, + "versionNonce": 2019559783, + "width": 10, + "x": -20, + "y": -10, } `; @@ -2145,23 +2119,21 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "width": 20, - "x": -10, - "y": 0, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, @@ -2199,7 +2171,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2299,7 +2271,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "id": "id0", "index": "a0", "isDeleted": true, @@ -2307,20 +2279,18 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 449462985, + "roundness": null, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1014066025, - "width": 20, - "x": -10, - "y": 0, + "versionNonce": 1116226695, + "width": 10, + "x": -20, + "y": -10, } `; @@ -2357,23 +2327,21 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "width": 20, - "x": -10, - "y": 0, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, @@ -2440,7 +2408,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2542,7 +2510,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "id": "id0", "index": "a0", "isDeleted": false, @@ -2550,20 +2518,18 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 449462985, + "roundness": null, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, - "width": 20, - "x": -10, - "y": 0, + "versionNonce": 2019559783, + "width": 10, + "x": -20, + "y": -10, } `; @@ -2576,7 +2542,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "id": "id3", "index": "a1", "isDeleted": false, @@ -2584,9 +2550,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -2595,9 +2559,9 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "updated": 1, "version": 5, "versionNonce": 400692809, - "width": 20, - "x": 0, - "y": 10, + "width": 10, + "x": -10, + "y": 0, } `; @@ -2634,23 +2598,21 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "width": 20, - "x": -10, - "y": 0, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, @@ -2688,23 +2650,21 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "index": "a1", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "width": 20, - "x": 0, - "y": 10, + "width": 10, + "x": -10, + "y": 0, }, "inserted": { "isDeleted": true, @@ -2742,7 +2702,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2859,9 +2819,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -2895,9 +2853,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -2952,9 +2908,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3006,9 +2960,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3114,7 +3066,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "currentItemFontSize": 20, "currentItemOpacity": 60, "currentItemRoughness": 2, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#e03131", "currentItemStrokeStyle": "dotted", @@ -3226,9 +3178,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "locked": false, "opacity": 60, "roughness": 2, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#e03131", "strokeStyle": "dotted", @@ -3236,7 +3186,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 941653321, + "versionNonce": 908564423, "width": 20, "x": -10, "y": 0, @@ -3260,17 +3210,15 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "locked": false, "opacity": 60, "roughness": 2, - "roundness": { - "type": 3, - }, - "seed": 289600103, + "roundness": null, + "seed": 1315507081, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 9, - "versionNonce": 640725609, + "versionNonce": 406373543, "width": 20, "x": 20, "y": 30, @@ -3317,9 +3265,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3371,9 +3317,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3597,7 +3541,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3707,17 +3651,15 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1014066025, + "roundness": null, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 1604849351, "width": 20, "x": 20, "y": 30, @@ -3741,17 +3683,15 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 449462985, + "roundness": null, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -3798,9 +3738,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3852,9 +3790,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3922,7 +3858,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4032,9 +3968,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -4066,9 +4000,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -4123,9 +4055,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4177,9 +4107,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4247,7 +4175,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4360,9 +4288,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -4394,9 +4320,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -4451,9 +4375,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4505,9 +4427,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5528,7 +5448,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5641,9 +5561,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -5675,17 +5593,15 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 400692809, + "roundness": null, + "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 493213705, "width": 10, "x": 12, "y": 0, @@ -5732,9 +5648,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5786,9 +5700,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6749,7 +6661,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -6866,9 +6778,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -6902,9 +6812,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -6959,9 +6867,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7013,9 +6919,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7684,7 +7588,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8684,7 +8588,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8745,7 +8649,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "resizingElement": null, "scrollX": 0, "scrollY": 0, - "scrolledOutside": false, + "scrolledOutside": true, "searchMatches": null, "selectedElementIds": { "id0": true, @@ -9675,7 +9579,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9780,7 +9684,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "id": "id0", "index": "a0", "isDeleted": false, @@ -9788,20 +9692,18 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 449462985, + "roundness": null, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, - "width": 20, - "x": -10, - "y": 0, + "versionNonce": 2019559783, + "width": 10, + "x": -20, + "y": -10, } `; @@ -9822,9 +9724,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -9856,9 +9756,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -9912,23 +9810,21 @@ exports[`contextMenu element > shows context menu for element > [end of test] un "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 20, + "height": 10, "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", - "width": 20, - "x": -10, - "y": 0, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, diff --git a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap index 62beb7f7cf..c25b269f4b 100644 --- a/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/dragCreate.test.tsx.snap @@ -71,9 +71,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -107,9 +105,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -155,9 +151,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "startArrowhead": null, "startBinding": null, @@ -193,9 +187,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap index 3105273934..59ee0a3f85 100644 --- a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`export > export svg-embedded scene > svg-embdedded scene export output 1`] = ` -"eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTTW/TQFx1MDAxML3zKyz3iqiTqlx1MDAxY3IrUERcdTAwMGblQJA4IFx1MDAwZVvvxFx1MDAxZWWyu9pcdTAwMWQ3XHRVJH5cdTAwMDY3/lwiP4HZjeuNnWJLlvbtfLx58/z0qihK3jsoXHUwMDE3RVx0u1pcdTAwMTFqr7bl64g/glx1MDAwZmiNXFzN0znYztcpsmV2i8tLspLQ2sCLq6qqjklAsFx1MDAwMcNBwr7LuSie0lduUMfUm1x1MDAxNJaA575cZjvO6E6gajjtR6ctam5cdTAwMDWZvVx1MDAxZKBcdTAwMTawaXmMKdNcdTAwMTCMXHUwMDEyXHUwMDAze7uG95asj1x1MDAxZC9mXHUwMDEw39z0QdXrxtvO6CGGvTLBKS/D5LhcdTAwMTVcdTAwMTIteZ+qi1x1MDAxZaJWOenxrac4n+D/y5KmTWsgRMFmXHUwMDAzap2qkePwsypPXHUwMDExXHUwMDE5ujudtP2ROXm1gbsorumIXHUwMDA2XHUwMDE4jYbdXHUwMDE0TCP23Z5cdTAwMTeTN3HVI4fMXHUwMDFkQI+IZU+cYZ+tqceqY/ggduBUYqUoQNY78rjNVlx1MDAxOZHsnFY86Uto1tM4sd/6hdrJTlwi9N8/v3+dbM5cdTAwMWFe4s9IcF6N0I9qg1x1MDAxNKW+XHUwMDFllbghbOKcJcHqxFx1MDAwMTIso9h+uGbr8m0t9Vx1MDAxNFx1MDAxYfDn+7BcdTAwMWVcdTAwMWI0ir6+SE91bL9AOFx1MDAxMmTfwenk8Gkw+Zv5dbo4yDdZoFTOLVn0XHUwMDFhNio2QT1cdTAwMTn1iDG4PGaC7q2GW6NcdTAwMWVoqmP5iLB9d/5XXFys0tNcdTAwMTPvV3DfXHUwMDEx41JWXbP4IHkr8ks2ir9cZvTQ4Vx1MDAxZvRzK/4ifQ==" `; diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 080d8fbf05..0dc6e525ba 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -24,7 +24,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -133,9 +133,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -165,9 +163,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -200,7 +196,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "102.45605", + "height": "99.19972", "id": "id691", "index": "a2", "isDeleted": false, @@ -214,8 +210,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "102.80179", - "102.45605", + "98.40611", + "99.19972", ], ], "roughness": 1, @@ -230,8 +226,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 37, - "width": "102.80179", - "x": "-0.42182", + "width": "98.40611", + "x": 1, "y": 0, } `; @@ -258,9 +254,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -314,15 +308,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": 0, "gap": 1, }, - "height": "70.45017", + "height": "68.55969", "points": [ [ 0, 0, ], [ - "100.70774", - "70.45017", + 98, + "68.55969", ], ], "startBinding": { @@ -337,15 +331,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "-0.02000", "gap": 1, }, - "height": "0.09250", + "height": "0.00656", "points": [ [ 0, 0, ], [ - "98.58579", - "0.09250", + "98.00000", + "-0.00656", ], ], "startBinding": { @@ -398,30 +392,30 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "id691": { "deleted": { - "height": "102.45584", + "height": "99.19972", "points": [ [ 0, 0, ], [ - "102.79971", - "102.45584", + "98.40611", + "99.19972", ], ], "startBinding": null, "y": 0, }, "inserted": { - "height": "70.33521", + "height": "68.58402", "points": [ [ 0, 0, ], [ - "100.78887", - "70.33521", + 98, + "68.58402", ], ], "startBinding": { @@ -429,7 +423,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02970", "gap": 1, }, - "y": "35.20327", + "y": "35.82151", }, }, }, @@ -467,9 +461,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -498,9 +490,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -615,7 +605,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -724,9 +714,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -756,9 +744,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -814,7 +800,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "updated": 1, "version": 33, "width": 100, - "x": "149.29289", + "x": 149, "y": 0, } `; @@ -976,9 +962,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1007,9 +991,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1124,7 +1106,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1235,7 +1217,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.30038", + "height": "1.36342", "id": "id715", "index": "Zz", "isDeleted": false, @@ -1249,14 +1231,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "98.58579", - "1.30038", + 98, + "1.36342", ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": { "elementId": "id711", @@ -1273,8 +1253,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 11, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -1301,9 +1281,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1338,9 +1316,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1387,9 +1363,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1418,9 +1392,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1492,7 +1464,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1603,7 +1575,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.30038", + "height": "1.36342", "id": "id725", "index": "a0", "isDeleted": false, @@ -1617,14 +1589,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "98.58579", - "1.30038", + 98, + "1.36342", ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": { "elementId": "id720", @@ -1641,8 +1611,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 11, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -1669,9 +1639,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1706,9 +1674,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1759,7 +1725,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "11.27227", + "height": "11.63758", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1772,14 +1738,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "98.58579", - "11.27227", + 98, + "11.63758", ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": { "elementId": "id720", @@ -1794,8 +1758,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, }, "inserted": { @@ -1861,7 +1825,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1969,9 +1933,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2001,9 +1963,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2050,9 +2010,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2081,9 +2039,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2128,7 +2084,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2240,9 +2196,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2277,9 +2231,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2308,7 +2260,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "374.05754", + "height": "370.26975", "id": "id740", "index": "a2", "isDeleted": false, @@ -2322,8 +2274,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "502.78936", - "-374.05754", + "498.00000", + "-370.26975", ], ], "roughness": 1, @@ -2342,9 +2294,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 10, - "width": "502.78936", - "x": "-0.83465", - "y": "-36.58211", + "width": "498.00000", + "x": 1, + "y": "-37.92697", } `; @@ -2382,9 +2334,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2413,9 +2363,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2565,7 +2513,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2678,9 +2626,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2716,9 +2662,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2757,9 +2701,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2811,9 +2753,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2864,7 +2804,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2977,9 +2917,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3015,9 +2953,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3056,9 +2992,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3148,7 +3082,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3261,9 +3195,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3299,9 +3231,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3340,9 +3270,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3442,7 +3370,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3550,9 +3478,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3587,9 +3513,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3625,9 +3549,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3728,7 +3650,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3836,9 +3758,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3874,9 +3794,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3963,7 +3881,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4076,9 +3994,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4114,9 +4030,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4166,9 +4080,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4222,7 +4134,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4335,9 +4247,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4373,9 +4283,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4431,9 +4339,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4495,7 +4401,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4608,9 +4514,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4646,9 +4550,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4726,7 +4628,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4839,9 +4741,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4877,9 +4777,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4957,7 +4855,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5065,9 +4963,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5103,9 +4999,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5186,7 +5080,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5299,9 +5193,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5337,9 +5229,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "opacity": 100, "originalText": "que pasa", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5415,7 +5305,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5523,9 +5413,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5556,9 +5444,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "name": null, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5605,9 +5491,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5675,7 +5559,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5784,9 +5668,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5818,9 +5700,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5881,9 +5761,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5914,9 +5792,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6008,7 +5884,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -6115,9 +5991,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6147,9 +6021,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6179,9 +6051,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6234,9 +6104,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6289,9 +6157,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6366,9 +6232,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6437,7 +6301,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -6547,9 +6411,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6579,9 +6441,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6611,9 +6471,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6666,9 +6524,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6697,9 +6553,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6728,9 +6582,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6817,7 +6669,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -6935,9 +6787,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6969,9 +6819,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7003,9 +6851,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7037,9 +6883,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7137,7 +6981,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7438,7 +7282,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7543,9 +7387,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7599,9 +7441,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7668,7 +7508,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7773,9 +7613,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7805,9 +7643,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7837,9 +7673,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7915,9 +7749,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7946,9 +7778,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7977,9 +7807,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8024,7 +7852,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8129,9 +7957,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8161,9 +7987,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8193,9 +8017,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8271,9 +8093,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8302,9 +8122,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8333,9 +8151,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8380,7 +8196,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8491,9 +8307,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8523,9 +8337,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8555,9 +8367,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -8610,9 +8420,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8664,9 +8472,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8788,7 +8594,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8952,9 +8758,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -9075,7 +8879,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9182,9 +8986,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9214,9 +9016,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -9269,9 +9069,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9341,7 +9139,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9448,9 +9246,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9480,9 +9276,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -9561,9 +9355,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9608,7 +9400,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9715,9 +9507,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#ffec99", "strokeStyle": "solid", "strokeWidth": 2, @@ -9770,9 +9560,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9840,7 +9628,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9951,9 +9739,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9986,9 +9772,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10020,9 +9804,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10054,9 +9836,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10141,7 +9921,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -10483,7 +10263,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -10588,9 +10368,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10643,9 +10421,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10719,7 +10495,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -10827,9 +10603,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10859,9 +10633,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10919,9 +10691,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": { "elementId": "KPrBI4g_v9qUB1XxYLgSz", @@ -11007,9 +10777,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": { "elementId": "KPrBI4g_v9qUB1XxYLgSz", @@ -11091,9 +10859,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11122,9 +10888,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11169,7 +10933,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11276,9 +11040,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11380,9 +11142,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11427,7 +11187,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11532,9 +11292,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11564,9 +11322,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11619,9 +11375,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11666,7 +11420,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11773,9 +11527,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11805,9 +11557,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11860,9 +11610,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11907,7 +11655,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#e03131", "currentItemStrokeStyle": "solid", @@ -12012,9 +11760,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12175,9 +11921,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12310,7 +12054,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -12418,9 +12162,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12449,9 +12191,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12510,9 +12250,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12557,7 +12295,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -12664,9 +12402,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12696,9 +12432,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12751,9 +12485,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12798,7 +12530,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -12905,9 +12637,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12937,9 +12667,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12992,9 +12720,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13039,7 +12765,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -13146,9 +12872,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13241,9 +12965,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13288,7 +13010,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -13395,9 +13117,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13427,9 +13147,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13482,9 +13200,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13576,9 +13292,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13623,7 +13337,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -13731,9 +13445,7 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13795,7 +13507,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -13907,9 +13619,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13941,9 +13651,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14003,9 +13711,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14036,9 +13742,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14083,7 +13787,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14191,9 +13895,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14223,9 +13925,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14272,9 +13972,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14303,9 +14001,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14350,7 +14046,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14459,9 +14155,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14583,9 +14277,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14630,7 +14322,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14792,7 +14484,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14910,9 +14602,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14948,9 +14638,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14988,9 +14676,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15033,7 +14719,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -15053,8 +14739,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -15174,9 +14860,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15211,9 +14895,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15245,9 +14927,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15493,7 +15173,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -15611,9 +15291,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15649,9 +15327,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15689,9 +15365,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15734,7 +15408,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -15754,8 +15428,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -15794,9 +15468,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15831,9 +15503,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15865,9 +15535,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16113,7 +15781,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -16231,9 +15899,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16269,9 +15935,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16309,9 +15973,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16354,7 +16016,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -16374,8 +16036,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -16414,9 +16076,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16451,9 +16111,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16485,9 +16143,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16733,7 +16389,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -16849,9 +16505,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16887,9 +16541,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16927,9 +16579,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16972,7 +16622,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -16992,8 +16642,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 10, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -17105,9 +16755,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17142,9 +16790,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17176,9 +16822,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17448,7 +17092,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -17567,9 +17211,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17605,9 +17247,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17645,9 +17285,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17690,7 +17328,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -17710,8 +17348,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", "updated": 1, "version": 11, - "width": "98.58579", - "x": "0.70711", + "width": 98, + "x": 1, "y": 0, } `; @@ -17838,9 +17476,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17875,9 +17511,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "opacity": 100, "originalText": "ola", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17909,9 +17543,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18201,7 +17833,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -18311,9 +17943,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18343,9 +17973,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18375,9 +18003,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18430,9 +18056,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18484,9 +18108,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18538,9 +18160,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18681,7 +18301,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -18795,9 +18415,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18829,9 +18447,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18863,9 +18479,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18897,9 +18511,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18931,9 +18543,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18965,9 +18575,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19027,9 +18635,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19060,9 +18666,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19124,9 +18728,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19157,9 +18759,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19204,7 +18804,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -19311,9 +18911,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19343,9 +18941,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19375,9 +18971,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19430,9 +19024,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19484,9 +19076,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19538,9 +19128,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19665,7 +19253,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index 729e53d225..ddbb76c273 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -17,9 +17,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -51,9 +49,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -85,9 +81,7 @@ exports[`move element > rectangle 5`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -124,9 +118,7 @@ exports[`move element > rectangles with binding arrow 5`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -163,9 +155,7 @@ exports[`move element > rectangles with binding arrow 6`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -196,7 +186,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "87.29887", + "height": "81.40630", "id": "id6", "index": "a2", "isDeleted": false, @@ -210,8 +200,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` 0, ], [ - "86.85786", - "87.29887", + "81.00000", + "81.40630", ], ], "roughness": 1, @@ -232,8 +222,8 @@ exports[`move element > rectangles with binding arrow 7`] = ` "updated": 1, "version": 11, "versionNonce": 1996028265, - "width": "86.85786", - "x": "107.07107", - "y": "47.07107", + "width": "81.00000", + "x": "110.00000", + "y": 50, } `; diff --git a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap index 5f2a82e4e8..1b0092757a 100644 --- a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap @@ -95,9 +95,7 @@ exports[`multi point mode in linear elements > line 3`] = ` ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "startArrowhead": null, "startBinding": null, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index a81ed30352..60fe9249fc 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -24,7 +24,7 @@ exports[`given element A and group of elements B and given both are selected whe "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -164,9 +164,7 @@ exports[`given element A and group of elements B and given both are selected whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -218,9 +216,7 @@ exports[`given element A and group of elements B and given both are selected whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -272,9 +268,7 @@ exports[`given element A and group of elements B and given both are selected whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -446,7 +440,7 @@ exports[`given element A and group of elements B and given both are selected whe "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -588,9 +582,7 @@ exports[`given element A and group of elements B and given both are selected whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -642,9 +634,7 @@ exports[`given element A and group of elements B and given both are selected whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -696,9 +686,7 @@ exports[`given element A and group of elements B and given both are selected whe "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -858,7 +846,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -991,9 +979,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1045,9 +1031,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1210,9 +1194,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1414,7 +1396,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1547,9 +1529,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1619,7 +1599,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -1757,9 +1737,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1811,9 +1789,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1865,9 +1841,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1999,7 +1973,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2134,9 +2108,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2188,9 +2160,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2235,7 +2205,7 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2368,9 +2338,7 @@ exports[`regression tests > arrow keys > [end of test] undo stack 1`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2415,7 +2383,7 @@ exports[`regression tests > can drag element that covers another element, while "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -2550,9 +2518,7 @@ exports[`regression tests > can drag element that covers another element, while "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2604,9 +2570,7 @@ exports[`regression tests > can drag element that covers another element, while "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2658,9 +2622,7 @@ exports[`regression tests > can drag element that covers another element, while "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2738,7 +2700,7 @@ exports[`regression tests > change the properties of a shape > [end of test] app "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1971c2", "currentItemStrokeStyle": "solid", @@ -2871,9 +2833,7 @@ exports[`regression tests > change the properties of a shape > [end of test] und "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2987,7 +2947,7 @@ exports[`regression tests > click on an element and drag it > [dragged] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3099,9 +3059,7 @@ exports[`regression tests > click on an element and drag it > [dragged] element "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -3156,9 +3114,7 @@ exports[`regression tests > click on an element and drag it > [dragged] undo sta "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3228,7 +3184,7 @@ exports[`regression tests > click on an element and drag it > [end of test] appS "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3363,9 +3319,7 @@ exports[`regression tests > click on an element and drag it > [end of test] undo "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3460,7 +3414,7 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`] "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3595,9 +3549,7 @@ exports[`regression tests > click to select a shape > [end of test] undo stack 1 "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3649,9 +3601,7 @@ exports[`regression tests > click to select a shape > [end of test] undo stack 1 "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3718,7 +3668,7 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -3854,9 +3804,7 @@ exports[`regression tests > click-drag to select a group > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3908,9 +3856,7 @@ exports[`regression tests > click-drag to select a group > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3962,9 +3908,7 @@ exports[`regression tests > click-drag to select a group > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4032,7 +3976,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4167,9 +4111,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4221,9 +4163,7 @@ exports[`regression tests > deleting last but one element in editing group shoul "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4462,7 +4402,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4548,9 +4488,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -4626,9 +4564,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4680,9 +4616,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4747,7 +4681,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -4881,9 +4815,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4935,9 +4867,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5023,7 +4953,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5108,9 +5038,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -5186,9 +5114,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5233,7 +5159,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5366,9 +5292,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5433,7 +5357,7 @@ exports[`regression tests > double click to edit a group > [end of test] appStat "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5566,9 +5490,7 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5620,9 +5542,7 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5674,9 +5594,7 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5820,7 +5738,7 @@ exports[`regression tests > drags selected elements from point inside common bou "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -5957,9 +5875,7 @@ exports[`regression tests > drags selected elements from point inside common bou "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6011,9 +5927,7 @@ exports[`regression tests > drags selected elements from point inside common bou "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6113,7 +6027,7 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -6244,9 +6158,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6298,9 +6210,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6352,9 +6262,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6494,9 +6402,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -6719,9 +6625,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -6935,7 +6839,7 @@ exports[`regression tests > given a group of selected elements with an element t "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7071,9 +6975,7 @@ exports[`regression tests > given a group of selected elements with an element t "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7125,9 +7027,7 @@ exports[`regression tests > given a group of selected elements with an element t "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7179,9 +7079,7 @@ exports[`regression tests > given a group of selected elements with an element t "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7269,7 +7167,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7405,9 +7303,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7459,9 +7355,7 @@ exports[`regression tests > given a selected element A and a not selected elemen "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7548,7 +7442,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7683,9 +7577,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7714,9 +7606,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7783,7 +7673,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -7918,9 +7808,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7949,9 +7837,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8021,7 +7907,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8154,9 +8040,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8201,7 +8085,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8334,9 +8218,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8381,7 +8263,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8514,9 +8396,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8561,7 +8441,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8785,7 +8665,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -8960,9 +8840,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1 ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -9009,7 +8887,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9203,7 +9081,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9427,7 +9305,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9560,9 +9438,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9607,7 +9483,7 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9782,9 +9658,7 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1 ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -9831,7 +9705,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -9964,9 +9838,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10011,7 +9883,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -10205,7 +10077,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -10338,9 +10210,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10385,7 +10255,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -10526,9 +10396,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10580,9 +10448,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10634,9 +10500,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10772,9 +10636,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10805,9 +10667,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10838,9 +10698,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10885,7 +10743,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11020,9 +10878,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11074,9 +10930,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11165,7 +11019,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11288,7 +11142,7 @@ exports[`regression tests > shift click on selected element should deselect it o "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11421,9 +11275,7 @@ exports[`regression tests > shift click on selected element should deselect it o "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11488,7 +11340,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11625,9 +11477,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11679,9 +11529,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11803,7 +11651,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -11942,9 +11790,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11996,9 +11842,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12050,9 +11894,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12220,7 +12062,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -12363,9 +12205,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12417,9 +12257,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12559,9 +12397,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12613,9 +12449,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12844,7 +12678,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -12970,7 +12804,7 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -13105,9 +12939,7 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13159,9 +12991,7 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13213,9 +13043,7 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13591,7 +13419,7 @@ exports[`regression tests > switches from group of selected elements to another "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -13679,9 +13507,7 @@ exports[`regression tests > switches from group of selected elements to another "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -13757,9 +13583,7 @@ exports[`regression tests > switches from group of selected elements to another "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13811,9 +13635,7 @@ exports[`regression tests > switches from group of selected elements to another "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13865,9 +13687,7 @@ exports[`regression tests > switches from group of selected elements to another "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13932,7 +13752,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14019,9 +13839,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1604849351, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -14097,9 +13915,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14151,9 +13967,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14198,7 +14012,7 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14321,7 +14135,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14605,9 +14419,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14659,9 +14471,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] undo st "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14706,7 +14516,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", @@ -14829,7 +14639,7 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", diff --git a/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap index c134a23eda..f47b89813f 100644 --- a/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/selection.test.tsx.snap @@ -81,9 +81,7 @@ exports[`select single element on the scene > arrow escape 1`] = ` ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "startArrowhead": null, "startBinding": null, @@ -117,9 +115,7 @@ exports[`select single element on the scene > diamond 1`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -151,9 +147,7 @@ exports[`select single element on the scene > ellipse 1`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -185,9 +179,7 @@ exports[`select single element on the scene > rectangle 1`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", diff --git a/packages/excalidraw/tests/contextmenu.test.tsx b/packages/excalidraw/tests/contextmenu.test.tsx index 75de2717f8..5bb7fee8e1 100644 --- a/packages/excalidraw/tests/contextmenu.test.tsx +++ b/packages/excalidraw/tests/contextmenu.test.tsx @@ -110,8 +110,8 @@ describe("contextMenu element", () => { it("shows context menu for element", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); - mouse.up(20, 20); + mouse.down(0, 0); + mouse.up(10, 10); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -304,8 +304,8 @@ describe("contextMenu element", () => { it("selecting 'Copy styles' in context menu copies styles", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); - mouse.up(20, 20); + mouse.down(0, 0); + mouse.up(10, 10); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -389,8 +389,8 @@ describe("contextMenu element", () => { it("selecting 'Delete' in context menu deletes element", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); - mouse.up(20, 20); + mouse.down(0, 0); + mouse.up(10, 10); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -405,8 +405,8 @@ describe("contextMenu element", () => { it("selecting 'Add to library' in context menu adds element to library", async () => { UI.clickTool("rectangle"); - mouse.down(10, 10); - mouse.up(20, 20); + mouse.down(0, 0); + mouse.up(10, 10); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, @@ -424,8 +424,8 @@ describe("contextMenu element", () => { it("selecting 'Duplicate' in context menu duplicates element", () => { UI.clickTool("rectangle"); - mouse.down(10, 10); - mouse.up(20, 20); + mouse.down(0, 0); + mouse.up(10, 10); fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, diff --git a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap index dfb0f8d2da..d59a829a0f 100644 --- a/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap +++ b/packages/excalidraw/tests/data/__snapshots__/restore.test.ts.snap @@ -31,9 +31,7 @@ exports[`restoreElements > should restore arrow element correctly 1`] = ` ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": Any, "startArrowhead": null, "startBinding": null, @@ -193,9 +191,7 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = ` ], "pressures": [], "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": Any, "simulatePressure": true, "strokeColor": "#1e1e1e", @@ -242,9 +238,7 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] = ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": Any, "startArrowhead": null, "startBinding": null, @@ -292,9 +286,7 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] = ], "polygon": false, "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "seed": Any, "startArrowhead": null, "startBinding": null, @@ -334,9 +326,7 @@ exports[`restoreElements > should restore text element correctly passing value f "opacity": 100, "originalText": "text", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": Any, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -378,9 +368,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo "opacity": 100, "originalText": "", "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "seed": Any, "strokeColor": "#1e1e1e", "strokeStyle": "solid", diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index abfadf3316..9a37bf6a0b 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -32,6 +32,7 @@ import type { ExcalidrawTextContainer, ExcalidrawTextElementWithContainer, ExcalidrawImageElement, + ElementsMap, } from "@excalidraw/element/types"; import { createTestHook } from "../../components/App"; @@ -146,6 +147,7 @@ export class Keyboard { const getElementPointForSelection = ( element: ExcalidrawElement, + elementsMap: ElementsMap, ): GlobalPoint => { const { x, y, width, angle } = element; const target = pointFrom( @@ -162,7 +164,7 @@ const getElementPointForSelection = ( (bounds[1] + bounds[3]) / 2, ); } else { - center = elementCenterPoint(element); + center = elementCenterPoint(element, elementsMap); } if (isTextElement(element)) { @@ -299,7 +301,12 @@ export class Pointer { elements = Array.isArray(elements) ? elements : [elements]; elements.forEach((element) => { this.reset(); - this.click(...getElementPointForSelection(element)); + this.click( + ...getElementPointForSelection( + element, + h.app.scene.getElementsMapIncludingDeleted(), + ), + ); }); }); @@ -308,13 +315,23 @@ export class Pointer { clickOn(element: ExcalidrawElement) { this.reset(); - this.click(...getElementPointForSelection(element)); + this.click( + ...getElementPointForSelection( + element, + h.app.scene.getElementsMapIncludingDeleted(), + ), + ); this.reset(); } doubleClickOn(element: ExcalidrawElement) { this.reset(); - this.doubleClick(...getElementPointForSelection(element)); + this.doubleClick( + ...getElementPointForSelection( + element, + h.app.scene.getElementsMapIncludingDeleted(), + ), + ); this.reset(); } } @@ -598,6 +615,7 @@ export class UI { const mutations = cropElement( element, + h.scene.getNonDeletedElementsMap(), handle, naturalWidth, naturalHeight, diff --git a/packages/excalidraw/tests/lasso.test.tsx b/packages/excalidraw/tests/lasso.test.tsx index 7e67d9b5b5..d84ce1ffb9 100644 --- a/packages/excalidraw/tests/lasso.test.tsx +++ b/packages/excalidraw/tests/lasso.test.tsx @@ -70,6 +70,7 @@ const updatePath = (startPoint: GlobalPoint, points: LocalPoint[]) => { ?.originalPoints?.map((p) => pointFrom(p[0], p[1])) ?? [], elements: h.elements, + elementsMap: h.scene.getNonDeletedElementsMap(), elementsSegments, intersectedElements: new Set(), enclosedElements: new Set(), diff --git a/packages/excalidraw/tests/move.test.tsx b/packages/excalidraw/tests/move.test.tsx index 1a02ba1db0..095db38a0c 100644 --- a/packages/excalidraw/tests/move.test.tsx +++ b/packages/excalidraw/tests/move.test.tsx @@ -124,8 +124,8 @@ describe("move element", () => { expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); expect([rectA.x, rectA.y]).toEqual([0, 0]); expect([rectB.x, rectB.y]).toEqual([201, 2]); - expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[107.07, 47.07]]); - expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[86.86, 87.3]]); + expect([[arrow.x, arrow.y]]).toCloselyEqualPoints([[110, 50]]); + expect([[arrow.width, arrow.height]]).toCloselyEqualPoints([[81, 81.4]]); h.elements.forEach((element) => expect(element).toMatchSnapshot()); }); diff --git a/packages/excalidraw/tests/rotate.test.tsx b/packages/excalidraw/tests/rotate.test.tsx index 9687b08f25..38079db8f3 100644 --- a/packages/excalidraw/tests/rotate.test.tsx +++ b/packages/excalidraw/tests/rotate.test.tsx @@ -35,7 +35,7 @@ test("unselected bound arrow updates when rotating its target element", async () expect(arrow.endBinding?.elementId).toEqual(rectangle.id); expect(arrow.x).toBeCloseTo(-80); expect(arrow.y).toBeCloseTo(50); - expect(arrow.width).toBeCloseTo(116.7, 1); + expect(arrow.width).toBeCloseTo(110.7, 1); expect(arrow.height).toBeCloseTo(0); }); diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index e7cd975092..d3ec968efc 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx @@ -682,7 +682,7 @@ describe("textWysiwyg", () => { expect(diamond.height).toBe(70); }); - it("should bind text to container when double clicked on center of transparent container", async () => { + it("should bind text to container when double clicked inside of the transparent container", async () => { const rectangle = API.createElement({ type: "rectangle", x: 10, @@ -1500,9 +1500,7 @@ describe("textWysiwyg", () => { locked: false, opacity: 100, roughness: 1, - roundness: { - type: 3, - }, + roundness: null, strokeColor: "#1e1e1e", strokeStyle: "solid", strokeWidth: 2, diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 359caee09d..26ab690e9c 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,8 +1,7 @@ -import type { Bounds } from "@excalidraw/element"; +import { doBoundsIntersect, type Bounds } from "@excalidraw/element"; -import { isPoint, pointDistance, pointFrom } from "./point"; -import { rectangle, rectangleIntersectLineSegment } from "./rectangle"; -import { vector } from "./vector"; +import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; +import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -105,16 +104,15 @@ export function curveIntersectLineSegment< Point extends GlobalPoint | LocalPoint, >(c: Curve, l: LineSegment): Point[] { // Optimize by doing a cheap bounding box check first - const bounds = curveBounds(c); - if ( - rectangleIntersectLineSegment( - rectangle( - pointFrom(bounds[0], bounds[1]), - pointFrom(bounds[2], bounds[3]), - ), - l, - ).length === 0 - ) { + const b1 = curveBounds(c); + const b2 = [ + Math.min(l[0][0], l[1][0]), + Math.min(l[0][1], l[1][1]), + Math.max(l[0][0], l[1][0]), + Math.max(l[0][1], l[1][1]), + ] as Bounds; + + if (!doBoundsIntersect(b1, b2)) { return []; } @@ -303,3 +301,108 @@ function curveBounds( const y = [P0[1], P1[1], P2[1], P3[1]]; return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)]; } + +export function curveCatmullRomQuadraticApproxPoints( + points: GlobalPoint[], + tension = 0.5, +) { + if (points.length < 2) { + return; + } + + const pointSets: [GlobalPoint, GlobalPoint][] = []; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i - 1 < 0 ? 0 : i - 1]; + const p1 = points[i]; + const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; + const cpX = p1[0] + ((p2[0] - p0[0]) * tension) / 2; + const cpY = p1[1] + ((p2[1] - p0[1]) * tension) / 2; + + pointSets.push([ + pointFrom(cpX, cpY), + pointFrom(p2[0], p2[1]), + ]); + } + + return pointSets; +} + +export function curveCatmullRomCubicApproxPoints< + Point extends GlobalPoint | LocalPoint, +>(points: Point[], tension = 0.5) { + if (points.length < 2) { + return; + } + + const pointSets: Curve[] = []; + for (let i = 0; i < points.length - 1; i++) { + const p0 = points[i - 1 < 0 ? 0 : i - 1]; + const p1 = points[i]; + const p2 = points[i + 1 >= points.length ? points.length - 1 : i + 1]; + const p3 = points[i + 2 >= points.length ? points.length - 1 : i + 2]; + const tangent1 = [(p2[0] - p0[0]) * tension, (p2[1] - p0[1]) * tension]; + const tangent2 = [(p3[0] - p1[0]) * tension, (p3[1] - p1[1]) * tension]; + const cp1x = p1[0] + tangent1[0] / 3; + const cp1y = p1[1] + tangent1[1] / 3; + const cp2x = p2[0] - tangent2[0] / 3; + const cp2y = p2[1] - tangent2[1] / 3; + + pointSets.push( + curve( + pointFrom(p1[0], p1[1]), + pointFrom(cp1x, cp1y), + pointFrom(cp2x, cp2y), + pointFrom(p2[0], p2[1]), + ), + ); + } + + return pointSets; +} + +export function curveOffsetPoints( + [p0, p1, p2, p3]: Curve, + offset: number, + steps = 50, +) { + const offsetPoints = []; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const c = curve(p0, p1, p2, p3); + const point = bezierEquation(c, t); + const tangent = vectorNormalize(curveTangent(c, t)); + const normal = vectorNormal(tangent); + + offsetPoints.push(pointFromVector(vectorScale(normal, offset), point)); + } + + return offsetPoints; +} + +export function offsetPointsForQuadraticBezier( + p0: GlobalPoint, + p1: GlobalPoint, + p2: GlobalPoint, + offsetDist: number, + steps = 50, +) { + const offsetPoints = []; + + for (let i = 0; i <= steps; i++) { + const t = i / steps; + const t1 = 1 - t; + const point = pointFrom( + t1 * t1 * p0[0] + 2 * t1 * t * p1[0] + t * t * p2[0], + t1 * t1 * p0[1] + 2 * t1 * t * p1[1] + t * t * p2[1], + ); + const tangentX = 2 * (1 - t) * (p1[0] - p0[0]) + 2 * t * (p2[0] - p1[0]); + const tangentY = 2 * (1 - t) * (p1[1] - p0[1]) + 2 * t * (p2[1] - p1[1]); + const tangent = vectorNormalize(vector(tangentX, tangentY)); + const normal = vectorNormal(tangent); + + offsetPoints.push(pointFromVector(vectorScale(normal, offsetDist), point)); + } + + return offsetPoints; +} diff --git a/packages/math/src/index.ts b/packages/math/src/index.ts index d00ab469d7..e487ac3336 100644 --- a/packages/math/src/index.ts +++ b/packages/math/src/index.ts @@ -1,5 +1,6 @@ export * from "./angle"; export * from "./curve"; +export * from "./ellipse"; export * from "./line"; export * from "./point"; export * from "./polygon"; diff --git a/packages/math/src/vector.ts b/packages/math/src/vector.ts index 12682fcd9f..c520fce244 100644 --- a/packages/math/src/vector.ts +++ b/packages/math/src/vector.ts @@ -21,13 +21,23 @@ export function vector( * * @param p The point to turn into a vector * @param origin The origin point in a given coordiante system - * @returns The created vector from the point and the origin + * @param threshold The threshold to consider the vector as 'undefined' + * @param defaultValue The default value to return if the vector is 'undefined' + * @returns The created vector from the point and the origin or default */ export function vectorFromPoint( p: Point, origin: Point = [0, 0] as Point, + threshold?: number, + defaultValue: Vector = [0, 1] as Vector, ): Vector { - return vector(p[0] - origin[0], p[1] - origin[1]); + const vec = vector(p[0] - origin[0], p[1] - origin[1]); + + if (threshold && vectorMagnitudeSq(vec) < threshold * threshold) { + return defaultValue; + } + + return vec; } /** diff --git a/packages/utils/src/collision.ts b/packages/utils/src/collision.ts deleted file mode 100644 index b7c155f663..0000000000 --- a/packages/utils/src/collision.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { - lineSegment, - pointFrom, - polygonIncludesPoint, - pointOnLineSegment, - pointOnPolygon, - polygonFromPoints, - type GlobalPoint, - type LocalPoint, - type Polygon, -} from "@excalidraw/math"; - -import type { Curve } from "@excalidraw/math"; - -import { pointInEllipse, pointOnEllipse } from "./shape"; - -import type { Polycurve, Polyline, GeometricShape } from "./shape"; - -// check if the given point is considered on the given shape's border -export const isPointOnShape = ( - point: Point, - shape: GeometricShape, - tolerance = 0, -) => { - // get the distance from the given point to the given element - // check if the distance is within the given epsilon range - switch (shape.type) { - case "polygon": - return pointOnPolygon(point, shape.data, tolerance); - case "ellipse": - return pointOnEllipse(point, shape.data, tolerance); - case "line": - return pointOnLineSegment(point, shape.data, tolerance); - case "polyline": - return pointOnPolyline(point, shape.data, tolerance); - case "curve": - return pointOnCurve(point, shape.data, tolerance); - case "polycurve": - return pointOnPolycurve(point, shape.data, tolerance); - default: - throw Error(`shape ${shape} is not implemented`); - } -}; - -// check if the given point is considered inside the element's border -export const isPointInShape = ( - point: Point, - shape: GeometricShape, -) => { - switch (shape.type) { - case "polygon": - return polygonIncludesPoint(point, shape.data); - case "line": - return false; - case "curve": - return false; - case "ellipse": - return pointInEllipse(point, shape.data); - case "polyline": { - const polygon = polygonFromPoints(shape.data.flat()); - return polygonIncludesPoint(point, polygon); - } - case "polycurve": { - return false; - } - default: - throw Error(`shape ${shape} is not implemented`); - } -}; - -// check if the given element is in the given bounds -export const isPointInBounds = ( - point: Point, - bounds: Polygon, -) => { - return polygonIncludesPoint(point, bounds); -}; - -const pointOnPolycurve = ( - point: Point, - polycurve: Polycurve, - tolerance: number, -) => { - return polycurve.some((curve) => pointOnCurve(point, curve, tolerance)); -}; - -const cubicBezierEquation = ( - curve: Curve, -) => { - const [p0, p1, p2, p3] = curve; - // B(t) = p0 * (1-t)^3 + 3p1 * t * (1-t)^2 + 3p2 * t^2 * (1-t) + p3 * t^3 - return (t: number, idx: number) => - Math.pow(1 - t, 3) * p3[idx] + - 3 * t * Math.pow(1 - t, 2) * p2[idx] + - 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + - p0[idx] * Math.pow(t, 3); -}; - -const polyLineFromCurve = ( - curve: Curve, - segments = 10, -): Polyline => { - const equation = cubicBezierEquation(curve); - let startingPoint = [equation(0, 0), equation(0, 1)] as Point; - const lineSegments: Polyline = []; - let t = 0; - const increment = 1 / segments; - - for (let i = 0; i < segments; i++) { - t += increment; - if (t <= 1) { - const nextPoint: Point = pointFrom(equation(t, 0), equation(t, 1)); - lineSegments.push(lineSegment(startingPoint, nextPoint)); - startingPoint = nextPoint; - } - } - - return lineSegments; -}; - -export const pointOnCurve = ( - point: Point, - curve: Curve, - threshold: number, -) => { - return pointOnPolyline(point, polyLineFromCurve(curve), threshold); -}; - -export const pointOnPolyline = ( - point: Point, - polyline: Polyline, - threshold = 10e-5, -) => { - return polyline.some((line) => pointOnLineSegment(point, line, threshold)); -}; diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 2c22874a9b..8307e5a543 100644 --- a/packages/utils/tests/__snapshots__/export.test.ts.snap +++ b/packages/utils/tests/__snapshots__/export.test.ts.snap @@ -24,7 +24,7 @@ exports[`exportToSvg > with default arguments 1`] = ` "currentItemFontSize": 20, "currentItemOpacity": 100, "currentItemRoughness": 1, - "currentItemRoundness": "round", + "currentItemRoundness": "sharp", "currentItemStartArrowhead": null, "currentItemStrokeColor": "#1e1e1e", "currentItemStrokeStyle": "solid", diff --git a/packages/utils/tests/collision.test.ts b/packages/utils/tests/collision.test.ts deleted file mode 100644 index 35bc28b34e..0000000000 --- a/packages/utils/tests/collision.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { - curve, - degreesToRadians, - lineSegment, - lineSegmentRotate, - pointFrom, - pointRotateDegs, -} from "@excalidraw/math"; - -import type { Curve, Degrees, GlobalPoint } from "@excalidraw/math"; - -import { pointOnCurve, pointOnPolyline } from "../src/collision"; - -import type { Polyline } from "../src/shape"; - -describe("point and curve", () => { - const c: Curve = curve( - pointFrom(1.4, 1.65), - pointFrom(1.9, 7.9), - pointFrom(5.9, 1.65), - pointFrom(6.44, 4.84), - ); - - it("point on curve", () => { - expect(pointOnCurve(c[0], c, 10e-5)).toBe(true); - expect(pointOnCurve(c[3], c, 10e-5)).toBe(true); - - expect(pointOnCurve(pointFrom(2, 4), c, 0.1)).toBe(true); - expect(pointOnCurve(pointFrom(4, 4.4), c, 0.1)).toBe(true); - expect(pointOnCurve(pointFrom(5.6, 3.85), c, 0.1)).toBe(true); - - expect(pointOnCurve(pointFrom(5.6, 4), c, 0.1)).toBe(false); - expect(pointOnCurve(c[1], c, 0.1)).toBe(false); - expect(pointOnCurve(c[2], c, 0.1)).toBe(false); - }); -}); - -describe("point and polylines", () => { - const polyline: Polyline = [ - lineSegment(pointFrom(1, 0), pointFrom(1, 2)), - lineSegment(pointFrom(1, 2), pointFrom(2, 2)), - lineSegment(pointFrom(2, 2), pointFrom(2, 1)), - lineSegment(pointFrom(2, 1), pointFrom(3, 1)), - ]; - - it("point on the line", () => { - expect(pointOnPolyline(pointFrom(1, 0), polyline)).toBe(true); - expect(pointOnPolyline(pointFrom(1, 2), polyline)).toBe(true); - expect(pointOnPolyline(pointFrom(2, 2), polyline)).toBe(true); - expect(pointOnPolyline(pointFrom(2, 1), polyline)).toBe(true); - expect(pointOnPolyline(pointFrom(3, 1), polyline)).toBe(true); - - expect(pointOnPolyline(pointFrom(1, 1), polyline)).toBe(true); - expect(pointOnPolyline(pointFrom(2, 1.5), polyline)).toBe(true); - expect(pointOnPolyline(pointFrom(2.5, 1), polyline)).toBe(true); - - expect(pointOnPolyline(pointFrom(0, 1), polyline)).toBe(false); - expect(pointOnPolyline(pointFrom(2.1, 1.5), polyline)).toBe(false); - }); - - it("point on the line with rotation", () => { - const truePoints = [ - pointFrom(1, 0), - pointFrom(1, 2), - pointFrom(2, 2), - pointFrom(2, 1), - pointFrom(3, 1), - ]; - - truePoints.forEach((p) => { - const rotation = (Math.random() * 360) as Degrees; - const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation); - const rotatedPolyline = polyline.map((line) => - lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)), - ); - expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(true); - }); - - const falsePoints = [pointFrom(0, 1), pointFrom(2.1, 1.5)]; - - falsePoints.forEach((p) => { - const rotation = (Math.random() * 360) as Degrees; - const rotatedPoint = pointRotateDegs(p, pointFrom(0, 0), rotation); - const rotatedPolyline = polyline.map((line) => - lineSegmentRotate(line, degreesToRadians(rotation), pointFrom(0, 0)), - ); - expect(pointOnPolyline(rotatedPoint, rotatedPolyline)).toBe(false); - }); - }); -}); From 469caadb878709980375569332d53d89c605eee2 Mon Sep 17 00:00:00 2001 From: cheapster Date: Sat, 7 Jun 2025 20:38:35 +0530 Subject: [PATCH 02/22] fix: prevent double-click to edit/create text scenarios on line (#9597) * fix : double click on line enables line editor * fix : prevent double-click to edit/create text when inside line editor * refactor: use lineCheck instead of arrowCheck in doubleClick handler to align with updated logic * fix: replace negative arrowCheck with lineCheck in dbl click handler and fix double-click bind text test in linearElementEditor tests * clean up test * simplify check * add tests * prevent text editing on dblclick when inside arrow editor --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/typeChecks.ts | 9 +++ .../tests/linearElementEditor.test.tsx | 56 ++++++++++--- packages/excalidraw/components/App.tsx | 80 +++++++++++-------- 3 files changed, 104 insertions(+), 41 deletions(-) diff --git a/packages/element/src/typeChecks.ts b/packages/element/src/typeChecks.ts index 37974f1f55..ab7a1935f5 100644 --- a/packages/element/src/typeChecks.ts +++ b/packages/element/src/typeChecks.ts @@ -129,6 +129,15 @@ export const isElbowArrow = ( return isArrowElement(element) && element.elbowed; }; +/** + * sharp or curved arrow, but not elbow + */ +export const isSimpleArrow = ( + element?: ExcalidrawElement, +): element is ExcalidrawArrowElement => { + return isArrowElement(element) && !element.elbowed; +}; + export const isSharpArrow = ( element?: ExcalidrawElement, ): element is ExcalidrawArrowElement => { diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index 35b1447801..f2fac51a76 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -1,6 +1,5 @@ import { pointCenter, pointFrom } from "@excalidraw/math"; import { act, queryByTestId, queryByText } from "@testing-library/react"; -import React from "react"; import { vi } from "vitest"; import { @@ -33,6 +32,8 @@ import { getBoundTextElementPosition, getBoundTextMaxWidth } from "../src"; import { LinearElementEditor } from "../src"; import { newArrowElement } from "../src"; +import { getTextEditor } from "../../excalidraw/tests/queries/dom"; + import type { ExcalidrawElement, ExcalidrawLinearElement, @@ -252,7 +253,17 @@ describe("Test Linear Elements", () => { expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); }); - it("should enter line editor when using double clicked with ctrl key", () => { + it("should enter line editor on ctrl+dblclick (simple arrow)", () => { + createTwoPointerLinearElement("arrow"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + Keyboard.withModifierKeys({ ctrl: true }, () => { + mouse.doubleClick(); + }); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + }); + + it("should enter line editor on ctrl+dblclick (line)", () => { createTwoPointerLinearElement("line"); expect(h.state.editingLinearElement?.elementId).toBeUndefined(); @@ -262,6 +273,39 @@ describe("Test Linear Elements", () => { expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); }); + it("should enter line editor on dblclick (line)", () => { + createTwoPointerLinearElement("line"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + mouse.doubleClick(); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + }); + + it("should not enter line editor on dblclick (arrow)", async () => { + createTwoPointerLinearElement("arrow"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + mouse.doubleClick(); + expect(h.state.editingLinearElement).toEqual(null); + await getTextEditor(".excalidraw-textEditorContainer > textarea"); + }); + + it("shouldn't create text element on double click in line editor (arrow)", async () => { + createTwoPointerLinearElement("arrow"); + const arrow = h.elements[0] as ExcalidrawLinearElement; + enterLineEditingMode(arrow); + + expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id); + + mouse.doubleClick(); + expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id); + expect(h.elements.length).toEqual(1); + + expect( + document.querySelector(".excalidraw-textEditorContainer > textarea"), + ).toBe(null); + }); + describe("Inside editor", () => { it("should not drag line and add midpoint when dragged irrespective of threshold", () => { createTwoPointerLinearElement("line"); @@ -1063,13 +1107,7 @@ describe("Test Linear Elements", () => { expect(h.elements.length).toBe(1); mouse.doubleClickAt(line.x, line.y); - - expect(h.elements.length).toBe(2); - - const text = h.elements[1] as ExcalidrawTextElementWithContainer; - expect(text.type).toBe("text"); - expect(text.containerId).toBeNull(); - expect(line.boundElements).toBeNull(); + expect(h.elements.length).toBe(1); }); // TODO fix #7029 and rewrite this test diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index b1ec1aebff..211fb10b1c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -230,6 +230,8 @@ import { CaptureUpdateAction, type ElementUpdate, hitElementBoundingBox, + isLineElement, + isSimpleArrow, } from "@excalidraw/element"; import type { LocalPoint, Radians } from "@excalidraw/math"; @@ -5438,17 +5440,17 @@ class App extends React.Component { ); if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { + const selectedLinearElement: ExcalidrawLinearElement = + selectedElements[0]; if ( - event[KEYS.CTRL_OR_CMD] && - (!this.state.editingLinearElement || - this.state.editingLinearElement.elementId !== - selectedElements[0].id) && - !isElbowArrow(selectedElements[0]) + ((event[KEYS.CTRL_OR_CMD] && isSimpleArrow(selectedLinearElement)) || + isLineElement(selectedLinearElement)) && + this.state.editingLinearElement?.elementId !== selectedLinearElement.id ) { this.store.scheduleCapture(); this.setState({ editingLinearElement: new LinearElementEditor( - selectedElements[0], + selectedLinearElement, this.scene.getNonDeletedElementsMap(), ), }); @@ -5515,6 +5517,13 @@ class App extends React.Component { return; } + } else if ( + this.state.editingLinearElement && + this.state.editingLinearElement.elementId === + selectedLinearElement.id && + isLineElement(selectedLinearElement) + ) { + return; } } @@ -5563,36 +5572,43 @@ class App extends React.Component { return; } - const container = this.getTextBindableContainerAtPosition(sceneX, sceneY); + // shouldn't edit/create text when inside line editor (often false positive) - if (container) { - if ( - hasBoundTextElement(container) || - !isTransparent(container.backgroundColor) || - hitElementItself({ - point: pointFrom(sceneX, sceneY), - element: container, - elementsMap: this.scene.getNonDeletedElementsMap(), - threshold: this.getElementHitThreshold(container), - }) - ) { - const midPoint = getContainerCenter( - container, - this.state, - this.scene.getNonDeletedElementsMap(), - ); + if (!this.state.editingLinearElement) { + const container = this.getTextBindableContainerAtPosition( + sceneX, + sceneY, + ); - sceneX = midPoint.x; - sceneY = midPoint.y; + if (container) { + if ( + hasBoundTextElement(container) || + !isTransparent(container.backgroundColor) || + hitElementItself({ + point: pointFrom(sceneX, sceneY), + element: container, + elementsMap: this.scene.getNonDeletedElementsMap(), + threshold: this.getElementHitThreshold(container), + }) + ) { + const midPoint = getContainerCenter( + container, + this.state, + this.scene.getNonDeletedElementsMap(), + ); + + sceneX = midPoint.x; + sceneY = midPoint.y; + } } - } - this.startTextEditing({ - sceneX, - sceneY, - insertAtParentCenter: !event.altKey, - container, - }); + this.startTextEditing({ + sceneX, + sceneY, + insertAtParentCenter: !event.altKey, + container, + }); + } } }; From 08cd4c4f9ae3a02a8592de2c6e93764e6fcb1bb9 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 7 Jun 2025 17:45:37 +0200 Subject: [PATCH 03/22] test: improve getTextEditor test helper (#9629) * test: improve getTextEditor test helper * fix test --- packages/element/tests/binding.test.tsx | 22 ++--- .../tests/linearElementEditor.test.tsx | 29 +++---- .../components/Stats/stats.test.tsx | 6 +- .../tests/MermaidToExcalidraw.test.tsx | 4 +- .../excalidraw/tests/elementLocking.test.tsx | 28 +++---- packages/excalidraw/tests/flip.test.tsx | 11 +-- packages/excalidraw/tests/helpers/ui.ts | 7 +- packages/excalidraw/tests/queries/dom.ts | 28 +++++-- packages/excalidraw/tests/test-utils.ts | 24 ++++-- .../excalidraw/wysiwyg/textWysiwyg.test.tsx | 84 +++++++++---------- 10 files changed, 123 insertions(+), 120 deletions(-) diff --git a/packages/element/tests/binding.test.tsx b/packages/element/tests/binding.test.tsx index bfc34af28c..69f4e6dded 100644 --- a/packages/element/tests/binding.test.tsx +++ b/packages/element/tests/binding.test.tsx @@ -11,6 +11,10 @@ import { UI, Pointer, Keyboard } from "@excalidraw/excalidraw/tests/helpers/ui"; import { fireEvent, render } from "@excalidraw/excalidraw/tests/test-utils"; import { getTransformHandles } from "../src/transformHandles"; +import { + getTextEditor, + TEXT_EDITOR_SELECTOR, +} from "../../excalidraw/tests/queries/dom"; const { h } = window; @@ -244,18 +248,12 @@ describe("element binding", () => { mouse.clickAt(text.x + 50, text.y + 50); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; - - expect(editor).not.toBe(null); + const editor = await getTextEditor(); fireEvent.change(editor, { target: { value: "" } }); fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); - expect( - document.querySelector(".excalidraw-textEditorContainer > textarea"), - ).toBe(null); + expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null); expect(arrow.endBinding).toBe(null); }); @@ -285,18 +283,14 @@ describe("element binding", () => { UI.clickTool("text"); mouse.clickAt(text.x + 50, text.y + 50); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); expect(editor).not.toBe(null); fireEvent.change(editor, { target: { value: "asdasdasdasdas" } }); fireEvent.keyDown(editor, { key: KEYS.ESCAPE }); - expect( - document.querySelector(".excalidraw-textEditorContainer > textarea"), - ).toBe(null); + expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null); expect(arrow.endBinding?.elementId).toBe(text.id); }); diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index f2fac51a76..a0f43188d9 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -32,7 +32,10 @@ import { getBoundTextElementPosition, getBoundTextMaxWidth } from "../src"; import { LinearElementEditor } from "../src"; import { newArrowElement } from "../src"; -import { getTextEditor } from "../../excalidraw/tests/queries/dom"; +import { + getTextEditor, + TEXT_EDITOR_SELECTOR, +} from "../../excalidraw/tests/queries/dom"; import type { ExcalidrawElement, @@ -287,7 +290,7 @@ describe("Test Linear Elements", () => { mouse.doubleClick(); expect(h.state.editingLinearElement).toEqual(null); - await getTextEditor(".excalidraw-textEditorContainer > textarea"); + await getTextEditor(); }); it("shouldn't create text element on double click in line editor (arrow)", async () => { @@ -301,9 +304,7 @@ describe("Test Linear Elements", () => { expect(h.state.editingLinearElement?.elementId).toEqual(arrow.id); expect(h.elements.length).toEqual(1); - expect( - document.querySelector(".excalidraw-textEditorContainer > textarea"), - ).toBe(null); + expect(document.querySelector(TEXT_EDITOR_SELECTOR)).toBe(null); }); describe("Inside editor", () => { @@ -1034,12 +1035,10 @@ describe("Test Linear Elements", () => { }); }); - it("should match styles for text editor", () => { + it("should match styles for text editor", async () => { createTwoPointerLinearElement("arrow"); Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); expect(editor).toMatchSnapshot(); }); @@ -1056,9 +1055,7 @@ describe("Test Linear Elements", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(arrow.id); mouse.down(); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); fireEvent.change(editor, { target: { value: DEFAULT_TEXT }, @@ -1086,9 +1083,7 @@ describe("Test Linear Elements", () => { const textElement = h.elements[1] as ExcalidrawTextElementWithContainer; expect(textElement.type).toBe("text"); expect(textElement.containerId).toBe(arrow.id); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); fireEvent.change(editor, { target: { value: DEFAULT_TEXT }, @@ -1272,9 +1267,7 @@ describe("Test Linear Elements", () => { mouse.select(arrow); Keyboard.keyPress(KEYS.ENTER); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); fireEvent.change(editor, { target: { value: DEFAULT_TEXT } }); Keyboard.exitTextEditor(editor); diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index 05163a32f3..902848f97e 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -381,8 +381,7 @@ describe("stats for a non-generic element", () => { it("text element", async () => { UI.clickTool("text"); mouse.clickAt(20, 30); - const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello!"); act(() => { editor.blur(); @@ -575,8 +574,7 @@ describe("stats for multiple elements", () => { // text, rectangle, frame UI.clickTool("text"); mouse.clickAt(20, 30); - const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello!"); act(() => { editor.blur(); diff --git a/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx index cedb704872..6043fc0aeb 100644 --- a/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx +++ b/packages/excalidraw/tests/MermaidToExcalidraw.test.tsx @@ -102,14 +102,14 @@ describe("Test ", () => { expect(dialog).not.toBeNull(); const selector = ".ttd-dialog-input"; - let editor = await getTextEditor(selector, true); + let editor = await getTextEditor({ selector, waitForEditor: true }); expect(dialog.querySelector('[data-testid="mermaid-error"]')).toBeNull(); expect(editor.textContent).toMatchSnapshot(); updateTextEditor(editor, "flowchart TD1"); - editor = await getTextEditor(selector, false); + editor = await getTextEditor({ selector, waitForEditor: false }); expect(editor.textContent).toBe("flowchart TD1"); expect( diff --git a/packages/excalidraw/tests/elementLocking.test.tsx b/packages/excalidraw/tests/elementLocking.test.tsx index 17b06bc206..ba9cfd7c43 100644 --- a/packages/excalidraw/tests/elementLocking.test.tsx +++ b/packages/excalidraw/tests/elementLocking.test.tsx @@ -1,5 +1,3 @@ -import React from "react"; - import { KEYS } from "@excalidraw/common"; import { actionSelectAll } from "../actions"; @@ -10,6 +8,8 @@ import { API } from "../tests/helpers/api"; import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; import { render, unmountComponent } from "../tests/test-utils"; +import { getTextEditor } from "./queries/dom"; + unmountComponent(); const mouse = new Pointer("mouse"); @@ -245,7 +245,7 @@ describe("element locking", () => { expect(h.state.editingTextElement?.id).toBe(h.elements[1].id); }); - it("should ignore locked text under cursor when clicked with text tool", () => { + it("should ignore locked text under cursor when clicked with text tool", async () => { const text = API.createElement({ type: "text", text: "ola", @@ -258,16 +258,14 @@ describe("element locking", () => { API.setElements([text]); UI.clickTool("text"); mouse.clickAt(text.x + 50, text.y + 50); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(2); expect(h.state.editingTextElement?.id).toBe(h.elements[1].id); }); - it("should ignore text under cursor when double-clicked with selection tool", () => { + it("should ignore text under cursor when double-clicked with selection tool", async () => { const text = API.createElement({ type: "text", text: "ola", @@ -280,9 +278,7 @@ describe("element locking", () => { API.setElements([text]); UI.clickTool("selection"); mouse.doubleClickAt(text.x + 50, text.y + 50); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(2); @@ -328,7 +324,7 @@ describe("element locking", () => { ]); }); - it("bound text shouldn't be editable via double-click", () => { + it("bound text shouldn't be editable via double-click", async () => { const container = API.createElement({ type: "rectangle", width: 100, @@ -353,16 +349,14 @@ describe("element locking", () => { UI.clickTool("selection"); mouse.doubleClickAt(container.width / 2, container.height / 2); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(3); expect(h.state.editingTextElement?.id).toBe(h.elements[2].id); }); - it("bound text shouldn't be editable via text tool", () => { + it("bound text shouldn't be editable via text tool", async () => { const container = API.createElement({ type: "rectangle", width: 100, @@ -387,9 +381,7 @@ describe("element locking", () => { UI.clickTool("text"); mouse.clickAt(container.width / 2, container.height / 2); - const editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - ) as HTMLTextAreaElement; + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).not.toBe(text.id); expect(h.elements.length).toBe(3); diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index d9b2731cc5..79a935068c 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -1,4 +1,3 @@ -import React from "react"; import { vi } from "vitest"; import { ROUNDNESS, KEYS, arrayToMap, cloneJSON } from "@excalidraw/common"; @@ -37,6 +36,8 @@ import { waitFor, } from "./test-utils"; +import { getTextEditor } from "./queries/dom"; + import type { NormalizedZoomValue } from "../types"; const { h } = window; @@ -846,9 +847,7 @@ describe("mutliple elements", () => { }); Keyboard.keyPress(KEYS.ENTER); - let editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - )!; + let editor = await getTextEditor(); fireEvent.input(editor, { target: { value: "arrow" } }); Keyboard.exitTextEditor(editor); @@ -860,9 +859,7 @@ describe("mutliple elements", () => { }); Keyboard.keyPress(KEYS.ENTER); - editor = document.querySelector( - ".excalidraw-textEditorContainer > textarea", - )!; + editor = await getTextEditor(); fireEvent.input(editor, { target: { value: "rect\ntext" } }); Keyboard.exitTextEditor(editor); diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index 9a37bf6a0b..3188c5ada6 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -36,7 +36,7 @@ import type { } from "@excalidraw/element/types"; import { createTestHook } from "../../components/App"; -import { getTextEditor } from "../queries/dom"; +import { getTextEditor, TEXT_EDITOR_SELECTOR } from "../queries/dom"; import { act, fireEvent, GlobalTestState, screen } from "../test-utils"; import { API } from "./api"; @@ -549,16 +549,15 @@ export class UI { static async editText< T extends ExcalidrawTextElement | ExcalidrawTextContainer, >(element: T, text: string) { - const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; const openedEditor = - document.querySelector(textEditorSelector); + document.querySelector(TEXT_EDITOR_SELECTOR); if (!openedEditor) { mouse.select(element); Keyboard.keyPress(KEYS.ENTER); } - const editor = await getTextEditor(textEditorSelector); + const editor = await getTextEditor(); if (!editor) { throw new Error("Can't find wysiwyg text editor in the dom"); } diff --git a/packages/excalidraw/tests/queries/dom.ts b/packages/excalidraw/tests/queries/dom.ts index e1515c8b4e..2e9fed62c2 100644 --- a/packages/excalidraw/tests/queries/dom.ts +++ b/packages/excalidraw/tests/queries/dom.ts @@ -1,13 +1,31 @@ import { waitFor } from "@testing-library/dom"; import { fireEvent } from "@testing-library/react"; -export const getTextEditor = async (selector: string, waitForEditor = true) => { - const query = () => document.querySelector(selector) as HTMLTextAreaElement; - if (waitForEditor) { - await waitFor(() => expect(query()).not.toBe(null)); +import { + stripIgnoredNodesFromErrorMessage, + trimErrorStack, +} from "../test-utils"; + +export const TEXT_EDITOR_SELECTOR = + ".excalidraw-textEditorContainer > textarea"; + +export const getTextEditor = async ({ + selector = TEXT_EDITOR_SELECTOR, + waitForEditor = true, +}: { selector?: string; waitForEditor?: boolean } = {}) => { + const error = trimErrorStack(new Error()); + try { + const query = () => document.querySelector(selector) as HTMLTextAreaElement; + if (waitForEditor) { + await waitFor(() => expect(query()).not.toBe(null)); + return query(); + } return query(); + } catch (err: any) { + stripIgnoredNodesFromErrorMessage(err); + err.stack = error.stack; + throw err; } - return query(); }; export const updateTextEditor = ( diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts index bc137a1d85..56af57c80b 100644 --- a/packages/excalidraw/tests/test-utils.ts +++ b/packages/excalidraw/tests/test-utils.ts @@ -421,11 +421,7 @@ export const assertElements = >( .join(", ")}]\n`, )}`; - const error = new Error(errStr); - const stack = err.stack.split("\n"); - stack.splice(1, 1); - error.stack = stack.join("\n"); - throw error; + throw trimErrorStack(new Error(errStr), 1); } expect(mappedActualElements).toEqual( @@ -476,3 +472,21 @@ export const checkpointHistory = (history: History, name: string) => { })), ).toMatchSnapshot(`[${name}] redo stack`); }; + +/** + * removes one or more leading stack trace lines (leading to files) from the + * error stack trace + */ +export const trimErrorStack = (error: Error, range = 1) => { + const stack = error.stack?.split("\n"); + if (stack) { + stack.splice(1, range); + error.stack = stack.join("\n"); + } + return error; +}; + +export const stripIgnoredNodesFromErrorMessage = (error: Error) => { + error.message = error.message.replace(/\s+Ignored nodes:[\s\S]+/, ""); + return error; +}; diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index d3ec968efc..c1a2f33094 100644 --- a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx +++ b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx @@ -38,8 +38,6 @@ unmountComponent(); const tab = " "; const mouse = new Pointer("mouse"); -const textEditorSelector = ".excalidraw-textEditorContainer > textarea"; - describe("textWysiwyg", () => { describe("start text editing", () => { const { h } = window; @@ -201,7 +199,7 @@ describe("textWysiwyg", () => { mouse.clickAt(text.x + 50, text.y + 50); - const editor = await getTextEditor(textEditorSelector, false); + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).toBe(text.id); @@ -223,7 +221,7 @@ describe("textWysiwyg", () => { mouse.doubleClickAt(text.x + 50, text.y + 50); - const editor = await getTextEditor(textEditorSelector, false); + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).toBe(text.id); @@ -293,7 +291,7 @@ describe("textWysiwyg", () => { // edit text UI.clickTool("selection"); mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2); - const editor = await getTextEditor(textEditorSelector); + const editor = await getTextEditor(); expect(editor).not.toBe(null); expect(h.state.editingTextElement?.id).toBe(text.id); expect(h.elements.length).toBe(1); @@ -326,7 +324,7 @@ describe("textWysiwyg", () => { // enter text editing mode UI.clickTool("selection"); mouse.doubleClickAt(text.x + text.width / 2, text.y + text.height / 2); - const editor = await getTextEditor(textEditorSelector); + const editor = await getTextEditor(); Keyboard.exitTextEditor(editor); // restore after unwrapping UI.resize(text, "e", [40, 0]); @@ -372,7 +370,7 @@ describe("textWysiwyg", () => { textElement = UI.createElement("text"); mouse.clickOn(textElement); - textarea = await getTextEditor(textEditorSelector, true); + textarea = await getTextEditor(); }); afterAll(() => { @@ -560,7 +558,7 @@ describe("textWysiwyg", () => { UI.clickTool("text"); mouse.click(0, 0); - textarea = await getTextEditor(textEditorSelector, true); + textarea = await getTextEditor(); updateTextEditor( textarea, "Excalidraw is an opensource virtual collaborative whiteboard for sketching hand-drawn like diagrams!", @@ -612,7 +610,7 @@ describe("textWysiwyg", () => { { id: text.id, type: "text" }, ]); mouse.down(); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -639,7 +637,7 @@ describe("textWysiwyg", () => { ]); expect(text.angle).toBe(rectangle.angle); mouse.down(); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -665,7 +663,7 @@ describe("textWysiwyg", () => { API.setSelectedElements([diamond]); Keyboard.keyPress(KEYS.ENTER); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); const value = new Array(1000).fill("1").join("\n"); @@ -699,7 +697,7 @@ describe("textWysiwyg", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(null); mouse.down(); - let editor = await getTextEditor(textEditorSelector, true); + let editor = await getTextEditor(); Keyboard.exitTextEditor(editor); mouse.doubleClickAt( @@ -713,7 +711,7 @@ describe("textWysiwyg", () => { expect(text.containerId).toBe(rectangle.id); mouse.down(); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); @@ -734,7 +732,7 @@ describe("textWysiwyg", () => { const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.type).toBe("text"); expect(text.containerId).toBe(rectangle.id); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); @@ -767,7 +765,7 @@ describe("textWysiwyg", () => { { id: text.id, type: "text" }, ]); mouse.down(); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); @@ -791,7 +789,7 @@ describe("textWysiwyg", () => { freedraw.y + freedraw.height / 2, ); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); @@ -825,7 +823,7 @@ describe("textWysiwyg", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(null); mouse.down(); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -839,7 +837,7 @@ describe("textWysiwyg", () => { UI.clickTool("text"); mouse.clickAt(20, 30); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor( editor, @@ -882,13 +880,13 @@ describe("textWysiwyg", () => { ); const text = h.elements[1] as ExcalidrawTextElementWithContainer; - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); - expect(await getTextEditor(textEditorSelector, false)).toBe(null); + expect(await getTextEditor({ waitForEditor: false })).toBe(null); expect(h.state.editingTextElement).toBe(null); @@ -922,7 +920,7 @@ describe("textWysiwyg", () => { Keyboard.keyDown(KEYS.ENTER); let text = h.elements[1] as ExcalidrawTextElementWithContainer; - let editor = await getTextEditor(textEditorSelector, true); + let editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -942,7 +940,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); @@ -969,7 +967,7 @@ describe("textWysiwyg", () => { const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); @@ -1004,7 +1002,7 @@ describe("textWysiwyg", () => { // Bind first text const text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ @@ -1024,7 +1022,7 @@ describe("textWysiwyg", () => { it("should respect text alignment when resizing", async () => { Keyboard.keyPress(KEYS.ENTER); - let editor = await getTextEditor(textEditorSelector, true); + let editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); @@ -1040,7 +1038,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); editor.select(); @@ -1059,7 +1057,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); editor.select(); @@ -1095,7 +1093,7 @@ describe("textWysiwyg", () => { expect(text.type).toBe("text"); expect(text.containerId).toBe(rectangle.id); mouse.down(); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -1109,7 +1107,7 @@ describe("textWysiwyg", () => { it("should scale font size correctly when resizing using shift", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); const textElement = h.elements[1] as ExcalidrawTextElement; @@ -1128,7 +1126,7 @@ describe("textWysiwyg", () => { it("should bind text correctly when container duplicated with alt-drag", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); expect(h.elements.length).toBe(2); @@ -1159,7 +1157,7 @@ describe("textWysiwyg", () => { it("undo should work", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); expect(rectangle.boundElements).toStrictEqual([ @@ -1195,7 +1193,7 @@ describe("textWysiwyg", () => { it("should not allow bound text with only whitespaces", async () => { Keyboard.keyPress(KEYS.ENTER); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, " "); Keyboard.exitTextEditor(editor); @@ -1249,7 +1247,7 @@ describe("textWysiwyg", () => { it("should reset the container height cache when resizing", async () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); - let editor = await getTextEditor(textEditorSelector, true); + let editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); @@ -1260,7 +1258,7 @@ describe("textWysiwyg", () => { mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); Keyboard.exitTextEditor(editor); expect(rectangle.height).toBeCloseTo(155, 8); @@ -1275,7 +1273,7 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); @@ -1300,7 +1298,7 @@ describe("textWysiwyg", () => { Keyboard.keyPress(KEYS.ENTER); expect(getOriginalContainerHeightFromCache(rectangle.id)).toBe(75); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); Keyboard.exitTextEditor(editor); expect( @@ -1332,12 +1330,12 @@ describe("textWysiwyg", () => { beforeEach(async () => { Keyboard.keyPress(KEYS.ENTER); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); updateTextEditor(editor, "Hello"); Keyboard.exitTextEditor(editor); mouse.select(rectangle); Keyboard.keyPress(KEYS.ENTER); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); editor.select(); }); @@ -1448,7 +1446,7 @@ describe("textWysiwyg", () => { it("should wrap text in a container when wrap text in container triggered from context menu", async () => { UI.clickTool("text"); mouse.clickAt(20, 30); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor( editor, @@ -1532,7 +1530,7 @@ describe("textWysiwyg", () => { // Bind first text let text = h.elements[1] as ExcalidrawTextElementWithContainer; expect(text.containerId).toBe(rectangle.id); - let editor = await getTextEditor(textEditorSelector, true); + let editor = await getTextEditor(); updateTextEditor(editor, "Hello!"); expect( (h.elements[1] as ExcalidrawTextElementWithContainer).verticalAlign, @@ -1555,7 +1553,7 @@ describe("textWysiwyg", () => { rectangle.x + rectangle.width / 2, rectangle.y + rectangle.height / 2, ); - editor = await getTextEditor(textEditorSelector, true); + editor = await getTextEditor(); updateTextEditor(editor, "Excalidraw"); Keyboard.exitTextEditor(editor); @@ -1630,7 +1628,7 @@ describe("textWysiwyg", () => { arrow.y + arrow.height / 2, ); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -1655,7 +1653,7 @@ describe("textWysiwyg", () => { rectangle.y + rectangle.height / 2, ); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); From d4e85a94805206176b7ae0af834d8145c1b6d693 Mon Sep 17 00:00:00 2001 From: David Luzar <5153846+dwelle@users.noreply.github.com> Date: Sat, 7 Jun 2025 18:05:20 +0200 Subject: [PATCH 04/22] feat: use `enter` to edit line points & update hints (#9630) feat: use enter to edit line points & update hints --- .../tests/linearElementEditor.test.tsx | 32 +++++++++++++++++++ packages/excalidraw/components/App.tsx | 5 ++- packages/excalidraw/components/HintViewer.tsx | 5 ++- packages/excalidraw/locales/en.json | 1 + 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index a0f43188d9..8618154aba 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -256,6 +256,38 @@ describe("Test Linear Elements", () => { expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); }); + it("should enter line editor via enter (line)", () => { + createTwoPointerLinearElement("line"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + mouse.clickAt(midpoint[0], midpoint[1]); + Keyboard.keyPress(KEYS.ENTER); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + }); + + // ctrl+enter alias (to align with arrows) + it("should enter line editor via ctrl+enter (line)", () => { + createTwoPointerLinearElement("line"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + mouse.clickAt(midpoint[0], midpoint[1]); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.ENTER); + }); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + }); + + it("should enter line editor via ctrl+enter (arrow)", () => { + createTwoPointerLinearElement("arrow"); + expect(h.state.editingLinearElement?.elementId).toBeUndefined(); + + mouse.clickAt(midpoint[0], midpoint[1]); + Keyboard.withModifierKeys({ ctrl: true }, () => { + Keyboard.keyPress(KEYS.ENTER); + }); + expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); + }); + it("should enter line editor on ctrl+dblclick (simple arrow)", () => { createTwoPointerLinearElement("arrow"); expect(h.state.editingLinearElement?.elementId).toBeUndefined(); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 211fb10b1c..7206737b38 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -4430,12 +4430,11 @@ class App extends React.Component { const selectedElements = this.scene.getSelectedElements(this.state); if (selectedElements.length === 1) { const selectedElement = selectedElements[0]; - if (event[KEYS.CTRL_OR_CMD]) { + if (event[KEYS.CTRL_OR_CMD] || isLineElement(selectedElement)) { if (isLinearElement(selectedElement)) { if ( !this.state.editingLinearElement || - this.state.editingLinearElement.elementId !== - selectedElements[0].id + this.state.editingLinearElement.elementId !== selectedElement.id ) { this.store.scheduleCapture(); if (!isElbowArrow(selectedElement)) { diff --git a/packages/excalidraw/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx index 017fccf8e3..f0cef544bb 100644 --- a/packages/excalidraw/components/HintViewer.tsx +++ b/packages/excalidraw/components/HintViewer.tsx @@ -4,6 +4,7 @@ import { isFlowchartNodeElement, isImageElement, isLinearElement, + isLineElement, isTextBindableContainer, isTextElement, } from "@excalidraw/element"; @@ -138,7 +139,9 @@ const getHints = ({ ? t("hints.lineEditor_pointSelected") : t("hints.lineEditor_nothingSelected"); } - return t("hints.lineEditor_info"); + return isLineElement(selectedElements[0]) + ? t("hints.lineEditor_line_info") + : t("hints.lineEditor_info"); } if ( !appState.newElement && diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index 736c417225..5a887482c9 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -344,6 +344,7 @@ "resizeImage": "You can resize freely by holding SHIFT,\nhold ALT to resize from the center", "rotate": "You can constrain angles by holding SHIFT while rotating", "lineEditor_info": "Hold CtrlOrCmd and Double-click or press CtrlOrCmd + Enter to edit points", + "lineEditor_line_info": "Double-click or press Enter to edit points", "lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move", "lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points", "placeImage": "Click to place the image, or click and drag to set its size manually", From d108053351a0c92e6a451f364b6c226c18cac3e4 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Mon, 9 Jun 2025 09:55:35 +0200 Subject: [PATCH 05/22] feat: various delta improvements (#9571) --- excalidraw-app/tests/collab.test.tsx | 75 +- packages/common/src/utils.ts | 4 +- packages/element/src/delta.ts | 341 ++++++-- packages/element/src/fractionalIndex.ts | 44 +- packages/element/src/mutateElement.ts | 10 +- packages/element/src/store.ts | 126 +-- packages/element/tests/duplicate.test.tsx | 2 - packages/excalidraw/components/App.tsx | 28 +- packages/excalidraw/history.ts | 153 +++- .../__snapshots__/contextmenu.test.tsx.snap | 142 +++- .../tests/__snapshots__/history.test.tsx.snap | 783 ++++++++++-------- .../tests/__snapshots__/move.test.tsx.snap | 18 +- .../multiPointCreate.test.tsx.snap | 4 +- .../regressionTests.test.tsx.snap | 354 +++++++- packages/excalidraw/tests/test-utils.ts | 23 +- packages/excalidraw/types.ts | 3 + 16 files changed, 1423 insertions(+), 687 deletions(-) diff --git a/excalidraw-app/tests/collab.test.tsx b/excalidraw-app/tests/collab.test.tsx index ed19c96e8a..4998c007aa 100644 --- a/excalidraw-app/tests/collab.test.tsx +++ b/excalidraw-app/tests/collab.test.tsx @@ -205,6 +205,7 @@ describe("collaboration", () => { // with explicit undo (as addition) we expect our item to be restored from the snapshot! await waitFor(() => { expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(1); expect(API.getSnapshot()).toEqual([ expect.objectContaining(rect1Props), expect.objectContaining({ ...rect2Props, isDeleted: false }), @@ -247,79 +248,5 @@ describe("collaboration", () => { expect.objectContaining({ ...rect2Props, isDeleted: true }), ]); }); - - act(() => h.app.actionManager.executeAction(undoAction)); - - // simulate local update - API.updateScene({ - elements: syncInvalidIndices([ - h.elements[0], - newElementWith(h.elements[1], { x: 100 }), - ]), - captureUpdate: CaptureUpdateAction.IMMEDIATELY, - }); - - await waitFor(() => { - expect(API.getUndoStack().length).toBe(2); - expect(API.getRedoStack().length).toBe(0); - expect(API.getSnapshot()).toEqual([ - expect.objectContaining(rect1Props), - expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }), - ]); - expect(h.elements).toEqual([ - expect.objectContaining(rect1Props), - expect.objectContaining({ ...rect2Props, isDeleted: false, x: 100 }), - ]); - }); - - act(() => h.app.actionManager.executeAction(undoAction)); - - // we expect to iterate the stack to the first visible change - await waitFor(() => { - expect(API.getUndoStack().length).toBe(1); - expect(API.getRedoStack().length).toBe(1); - expect(API.getSnapshot()).toEqual([ - expect.objectContaining(rect1Props), - expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }), - ]); - expect(h.elements).toEqual([ - expect.objectContaining(rect1Props), - expect.objectContaining({ ...rect2Props, isDeleted: false, x: 0 }), - ]); - }); - - // simulate force deleting the element remotely - API.updateScene({ - elements: syncInvalidIndices([rect1]), - captureUpdate: CaptureUpdateAction.NEVER, - }); - - // snapshot was correctly updated and marked the element as deleted - await waitFor(() => { - expect(API.getUndoStack().length).toBe(1); - expect(API.getRedoStack().length).toBe(1); - expect(API.getSnapshot()).toEqual([ - expect.objectContaining(rect1Props), - expect.objectContaining({ ...rect2Props, isDeleted: true, x: 0 }), - ]); - expect(h.elements).toEqual([expect.objectContaining(rect1Props)]); - }); - - act(() => h.app.actionManager.executeAction(redoAction)); - - // with explicit redo (as update) we again restored the element from the snapshot! - await waitFor(() => { - expect(API.getUndoStack().length).toBe(2); - expect(API.getRedoStack().length).toBe(0); - expect(API.getSnapshot()).toEqual([ - expect.objectContaining({ id: "A", isDeleted: false }), - expect.objectContaining({ id: "B", isDeleted: true, x: 100 }), - ]); - expect(h.history.isRedoStackEmpty).toBeTruthy(); - expect(h.elements).toEqual([ - expect.objectContaining({ id: "A", isDeleted: false }), - expect.objectContaining({ id: "B", isDeleted: true, x: 100 }), - ]); - }); }); }); diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 39e2e9149c..824e88b636 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -714,8 +714,8 @@ export const arrayToObject = ( array: readonly T[], groupBy?: (value: T) => string | number, ) => - array.reduce((acc, value) => { - acc[groupBy ? groupBy(value) : String(value)] = value; + array.reduce((acc, value, idx) => { + acc[groupBy ? groupBy(value) : idx] = value; return acc; }, {} as { [key: string]: T }); diff --git a/packages/element/src/delta.ts b/packages/element/src/delta.ts index 45b1fc3487..17d9f49ee5 100644 --- a/packages/element/src/delta.ts +++ b/packages/element/src/delta.ts @@ -5,11 +5,12 @@ import { isDevEnv, isShallowEqual, isTestEnv, + randomInteger, } from "@excalidraw/common"; import type { ExcalidrawElement, - ExcalidrawImageElement, + ExcalidrawFreeDrawElement, ExcalidrawLinearElement, ExcalidrawTextElement, NonDeleted, @@ -18,7 +19,12 @@ import type { SceneElementsMap, } from "@excalidraw/element/types"; -import type { DTO, SubtypeOf, ValueOf } from "@excalidraw/common/utility-types"; +import type { + DTO, + Mutable, + SubtypeOf, + ValueOf, +} from "@excalidraw/common/utility-types"; import type { AppState, @@ -51,6 +57,8 @@ import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex"; import { Scene } from "./Scene"; +import { StoreSnapshot } from "./store"; + import type { BindableProp, BindingProp } from "./binding"; import type { ElementUpdate } from "./mutateElement"; @@ -73,13 +81,20 @@ export class Delta { public static create( deleted: Partial, inserted: Partial, - modifier?: (delta: Partial) => Partial, - modifierOptions?: "deleted" | "inserted", + modifier?: ( + delta: Partial, + partialType: "deleted" | "inserted", + ) => Partial, + modifierOptions?: "deleted" | "inserted" | "both", ) { const modifiedDeleted = - modifier && modifierOptions !== "inserted" ? modifier(deleted) : deleted; + modifier && modifierOptions !== "inserted" + ? modifier(deleted, "deleted") + : deleted; const modifiedInserted = - modifier && modifierOptions !== "deleted" ? modifier(inserted) : inserted; + modifier && modifierOptions !== "deleted" + ? modifier(inserted, "inserted") + : inserted; return new Delta(modifiedDeleted, modifiedInserted); } @@ -113,11 +128,7 @@ export class Delta { // - we do this only on previously detected changed elements // - we do shallow compare only on the first level of properties (not going any deeper) // - # of properties is reasonably small - for (const key of this.distinctKeysIterator( - "full", - prevObject, - nextObject, - )) { + for (const key of this.getDifferences(prevObject, nextObject)) { deleted[key as keyof T] = prevObject[key]; inserted[key as keyof T] = nextObject[key]; } @@ -256,12 +267,14 @@ export class Delta { arrayToObject(deletedArray, groupBy), arrayToObject(insertedArray, groupBy), ), + (x) => x, ); const insertedDifferences = arrayToObject( Delta.getRightDifferences( arrayToObject(deletedArray, groupBy), arrayToObject(insertedArray, groupBy), ), + (x) => x, ); if ( @@ -320,6 +333,42 @@ export class Delta { return !!anyDistinctKey; } + /** + * Compares if shared properties of object1 and object2 contain any different value (aka inner join). + */ + public static isInnerDifferent( + object1: T, + object2: T, + skipShallowCompare = false, + ): boolean { + const anyDistinctKey = !!this.distinctKeysIterator( + "inner", + object1, + object2, + skipShallowCompare, + ).next().value; + + return !!anyDistinctKey; + } + + /** + * Compares if any properties of object1 and object2 contain any different value (aka full join). + */ + public static isDifferent( + object1: T, + object2: T, + skipShallowCompare = false, + ): boolean { + const anyDistinctKey = !!this.distinctKeysIterator( + "full", + object1, + object2, + skipShallowCompare, + ).next().value; + + return !!anyDistinctKey; + } + /** * Returns sorted object1 keys that have distinct values. */ @@ -346,6 +395,32 @@ export class Delta { ).sort(); } + /** + * Returns sorted keys of shared object1 and object2 properties that have distinct values (aka inner join). + */ + public static getInnerDifferences( + object1: T, + object2: T, + skipShallowCompare = false, + ) { + return Array.from( + this.distinctKeysIterator("inner", object1, object2, skipShallowCompare), + ).sort(); + } + + /** + * Returns sorted keys that have distinct values between object1 and object2 (aka full join). + */ + public static getDifferences( + object1: T, + object2: T, + skipShallowCompare = false, + ) { + return Array.from( + this.distinctKeysIterator("full", object1, object2, skipShallowCompare), + ).sort(); + } + /** * Iterator comparing values of object properties based on the passed joining strategy. * @@ -354,7 +429,7 @@ export class Delta { * WARN: it's based on shallow compare performed only on the first level and doesn't go deeper than that. */ private static *distinctKeysIterator( - join: "left" | "right" | "full", + join: "left" | "right" | "inner" | "full", object1: T, object2: T, skipShallowCompare = false, @@ -369,6 +444,8 @@ export class Delta { keys = Object.keys(object1); } else if (join === "right") { keys = Object.keys(object2); + } else if (join === "inner") { + keys = Object.keys(object1).filter((key) => key in object2); } else if (join === "full") { keys = Array.from( new Set([...Object.keys(object1), ...Object.keys(object2)]), @@ -382,17 +459,17 @@ export class Delta { } for (const key of keys) { - const object1Value = object1[key as keyof T]; - const object2Value = object2[key as keyof T]; + const value1 = object1[key as keyof T]; + const value2 = object2[key as keyof T]; - if (object1Value !== object2Value) { + if (value1 !== value2) { if ( !skipShallowCompare && - typeof object1Value === "object" && - typeof object2Value === "object" && - object1Value !== null && - object2Value !== null && - isShallowEqual(object1Value, object2Value) + typeof value1 === "object" && + typeof value2 === "object" && + value1 !== null && + value2 !== null && + isShallowEqual(value1, value2) ) { continue; } @@ -858,10 +935,17 @@ export class AppStateDelta implements DeltaContainer { } } -type ElementPartial = Omit< - ElementUpdate>, - "seed" ->; +type ElementPartial = + Omit>, "id" | "updated" | "seed">; + +export type ApplyToOptions = { + excludedProperties: Set; +}; + +type ApplyToFlags = { + containsVisibleDifference: boolean; + containsZindexDifference: boolean; +}; /** * Elements change is a low level primitive to capture a change between two sets of elements. @@ -944,13 +1028,33 @@ export class ElementsDelta implements DeltaContainer { inserted, }: Delta) => !!deleted.isDeleted === !!inserted.isDeleted; + private static satisfiesCommmonInvariants = ({ + deleted, + inserted, + }: Delta) => + !!( + deleted.version && + inserted.version && + // versions are required integers + Number.isInteger(deleted.version) && + Number.isInteger(inserted.version) && + // versions should be positive, zero included + deleted.version >= 0 && + inserted.version >= 0 && + // versions should never be the same + deleted.version !== inserted.version + ); + private static validate( elementsDelta: ElementsDelta, type: "added" | "removed" | "updated", - satifies: (delta: Delta) => boolean, + satifiesSpecialInvariants: (delta: Delta) => boolean, ) { for (const [id, delta] of Object.entries(elementsDelta[type])) { - if (!satifies(delta)) { + if ( + !this.satisfiesCommmonInvariants(delta) || + !satifiesSpecialInvariants(delta) + ) { console.error( `Broken invariant for "${type}" delta, element "${id}", delta:`, delta, @@ -986,7 +1090,12 @@ export class ElementsDelta implements DeltaContainer { if (!nextElement) { const deleted = { ...prevElement, isDeleted: false } as ElementPartial; - const inserted = { isDeleted: true } as ElementPartial; + + const inserted = { + isDeleted: true, + version: prevElement.version + 1, + versionNonce: randomInteger(), + } as ElementPartial; const delta = Delta.create( deleted, @@ -1002,7 +1111,12 @@ export class ElementsDelta implements DeltaContainer { const prevElement = prevElements.get(nextElement.id); if (!prevElement) { - const deleted = { isDeleted: true } as ElementPartial; + const deleted = { + isDeleted: true, + version: nextElement.version - 1, + versionNonce: randomInteger(), + } as ElementPartial; + const inserted = { ...nextElement, isDeleted: false, @@ -1087,16 +1201,40 @@ export class ElementsDelta implements DeltaContainer { /** * Update delta/s based on the existing elements. * - * @param elements current elements + * @param nextElements current elements * @param modifierOptions defines which of the delta (`deleted` or `inserted`) will be updated * @returns new instance with modified delta/s */ public applyLatestChanges( - elements: SceneElementsMap, - modifierOptions: "deleted" | "inserted", + prevElements: SceneElementsMap, + nextElements: SceneElementsMap, + modifierOptions?: "deleted" | "inserted", ): ElementsDelta { const modifier = - (element: OrderedExcalidrawElement) => (partial: ElementPartial) => { + ( + prevElement: OrderedExcalidrawElement | undefined, + nextElement: OrderedExcalidrawElement | undefined, + ) => + (partial: ElementPartial, partialType: "deleted" | "inserted") => { + let element: OrderedExcalidrawElement | undefined; + + switch (partialType) { + case "deleted": + element = prevElement; + break; + case "inserted": + element = nextElement; + break; + } + + // the element wasn't found -> don't update the partial + if (!element) { + console.error( + `Element not found when trying to apply latest changes`, + ); + return partial; + } + const latestPartial: { [key: string]: unknown } = {}; for (const key of Object.keys(partial) as Array) { @@ -1120,19 +1258,25 @@ export class ElementsDelta implements DeltaContainer { const modifiedDeltas: Record> = {}; for (const [id, delta] of Object.entries(deltas)) { - const existingElement = elements.get(id); + const prevElement = prevElements.get(id); + const nextElement = nextElements.get(id); - if (existingElement) { - const modifiedDelta = Delta.create( + let latestDelta: Delta | null = null; + + if (prevElement || nextElement) { + latestDelta = Delta.create( delta.deleted, delta.inserted, - modifier(existingElement), + modifier(prevElement, nextElement), modifierOptions, ); - - modifiedDeltas[id] = modifiedDelta; } else { - modifiedDeltas[id] = delta; + latestDelta = delta; + } + + // it might happen that after applying latest changes the delta itself does not contain any changes + if (Delta.isInnerDifferent(latestDelta.deleted, latestDelta.inserted)) { + modifiedDeltas[id] = latestDelta; } } @@ -1150,12 +1294,15 @@ export class ElementsDelta implements DeltaContainer { public applyTo( elements: SceneElementsMap, - elementsSnapshot: Map, + snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements, + options: ApplyToOptions = { + excludedProperties: new Set(), + }, ): [SceneElementsMap, boolean] { let nextElements = new Map(elements) as SceneElementsMap; let changedElements: Map; - const flags = { + const flags: ApplyToFlags = { containsVisibleDifference: false, containsZindexDifference: false, }; @@ -1164,13 +1311,14 @@ export class ElementsDelta implements DeltaContainer { try { const applyDeltas = ElementsDelta.createApplier( nextElements, - elementsSnapshot, + snapshot, + options, flags, ); - const addedElements = applyDeltas("added", this.added); - const removedElements = applyDeltas("removed", this.removed); - const updatedElements = applyDeltas("updated", this.updated); + const addedElements = applyDeltas(this.added); + const removedElements = applyDeltas(this.removed); + const updatedElements = applyDeltas(this.updated); const affectedElements = this.resolveConflicts(elements, nextElements); @@ -1229,18 +1377,12 @@ export class ElementsDelta implements DeltaContainer { private static createApplier = ( nextElements: SceneElementsMap, - snapshot: Map, - flags: { - containsVisibleDifference: boolean; - containsZindexDifference: boolean; - }, + snapshot: StoreSnapshot["elements"], + options: ApplyToOptions, + flags: ApplyToFlags, ) => - ( - type: "added" | "removed" | "updated", - deltas: Record>, - ) => { + (deltas: Record>) => { const getElement = ElementsDelta.createGetter( - type, nextElements, snapshot, flags, @@ -1250,7 +1392,13 @@ export class ElementsDelta implements DeltaContainer { const element = getElement(id, delta.inserted); if (element) { - const newElement = ElementsDelta.applyDelta(element, delta, flags); + const newElement = ElementsDelta.applyDelta( + element, + delta, + options, + flags, + ); + nextElements.set(newElement.id, newElement); acc.set(newElement.id, newElement); } @@ -1261,13 +1409,9 @@ export class ElementsDelta implements DeltaContainer { private static createGetter = ( - type: "added" | "removed" | "updated", elements: SceneElementsMap, - snapshot: Map, - flags: { - containsVisibleDifference: boolean; - containsZindexDifference: boolean; - }, + snapshot: StoreSnapshot["elements"], + flags: ApplyToFlags, ) => (id: string, partial: ElementPartial) => { let element = elements.get(id); @@ -1281,10 +1425,7 @@ export class ElementsDelta implements DeltaContainer { flags.containsZindexDifference = true; // as the element was force deleted, we need to check if adding it back results in a visible change - if ( - partial.isDeleted === false || - (partial.isDeleted !== true && element.isDeleted === false) - ) { + if (!partial.isDeleted || (partial.isDeleted && !element.isDeleted)) { flags.containsVisibleDifference = true; } } else { @@ -1304,16 +1445,28 @@ export class ElementsDelta implements DeltaContainer { private static applyDelta( element: OrderedExcalidrawElement, delta: Delta, - flags: { - containsVisibleDifference: boolean; - containsZindexDifference: boolean; - } = { - // by default we don't care about about the flags - containsVisibleDifference: true, - containsZindexDifference: true, - }, + options: ApplyToOptions, + flags: ApplyToFlags, ) { - const { boundElements, ...directlyApplicablePartial } = delta.inserted; + const directlyApplicablePartial: Mutable = {}; + + // some properties are not directly applicable, such as: + // - boundElements which contains only diff) + // - version & versionNonce, if we don't want to return to previous versions + for (const key of Object.keys(delta.inserted) as Array< + keyof typeof delta.inserted + >) { + if (key === "boundElements") { + continue; + } + + if (options.excludedProperties.has(key)) { + continue; + } + + const value = delta.inserted[key]; + Reflect.set(directlyApplicablePartial, key, value); + } if ( delta.deleted.boundElements?.length || @@ -1331,19 +1484,6 @@ export class ElementsDelta implements DeltaContainer { }); } - // TODO: this looks wrong, shouldn't be here - if (element.type === "image") { - const _delta = delta as Delta>; - // we want to override `crop` only if modified so that we don't reset - // when undoing/redoing unrelated change - if (_delta.deleted.crop || _delta.inserted.crop) { - Object.assign(directlyApplicablePartial, { - // apply change verbatim - crop: _delta.inserted.crop ?? null, - }); - } - } - if (!flags.containsVisibleDifference) { // strip away fractional index, as even if it would be different, it doesn't have to result in visible change const { index, ...rest } = directlyApplicablePartial; @@ -1650,6 +1790,29 @@ export class ElementsDelta implements DeltaContainer { ): [ElementPartial, ElementPartial] { try { Delta.diffArrays(deleted, inserted, "boundElements", (x) => x.id); + + // don't diff the points as: + // - we can't ensure the multiplayer order consistency without fractional index on each point + // - we prefer to not merge the points, as it might just lead to unexpected / incosistent results + const deletedPoints = + ( + deleted as ElementPartial< + ExcalidrawFreeDrawElement | ExcalidrawLinearElement + > + ).points ?? []; + + const insertedPoints = + ( + inserted as ElementPartial< + ExcalidrawFreeDrawElement | ExcalidrawLinearElement + > + ).points ?? []; + + if (!Delta.isDifferent(deletedPoints, insertedPoints)) { + // delete the points from delta if there is no difference, otherwise leave them as they were captured due to consistency + Reflect.deleteProperty(deleted, "points"); + Reflect.deleteProperty(inserted, "points"); + } } catch (e) { // if postprocessing fails, it does not make sense to bubble up, but let's make sure we know about it console.error(`Couldn't postprocess elements delta.`); @@ -1665,7 +1828,7 @@ export class ElementsDelta implements DeltaContainer { private static stripIrrelevantProps( partial: Partial, ): ElementPartial { - const { id, updated, version, versionNonce, ...strippedPartial } = partial; + const { id, updated, ...strippedPartial } = partial; return strippedPartial; } diff --git a/packages/element/src/fractionalIndex.ts b/packages/element/src/fractionalIndex.ts index 84505365ec..44ca523c80 100644 --- a/packages/element/src/fractionalIndex.ts +++ b/packages/element/src/fractionalIndex.ts @@ -2,7 +2,7 @@ import { generateNKeysBetween } from "fractional-indexing"; import { arrayToMap } from "@excalidraw/common"; -import { mutateElement } from "./mutateElement"; +import { mutateElement, newElementWith } from "./mutateElement"; import { getBoundTextElement } from "./textElement"; import { hasBoundTextElement } from "./typeChecks"; @@ -11,6 +11,7 @@ import type { ExcalidrawElement, FractionalIndex, OrderedExcalidrawElement, + SceneElementsMap, } from "./types"; export class InvalidFractionalIndexError extends Error { @@ -161,9 +162,15 @@ export const syncMovedIndices = ( // try generatating indices, throws on invalid movedElements const elementsUpdates = generateIndices(elements, indicesGroups); - const elementsCandidates = elements.map((x) => - elementsUpdates.has(x) ? { ...x, ...elementsUpdates.get(x) } : x, - ); + const elementsCandidates = elements.map((x) => { + const elementUpdates = elementsUpdates.get(x); + + if (elementUpdates) { + return { ...x, index: elementUpdates.index }; + } + + return x; + }); // ensure next indices are valid before mutation, throws on invalid ones validateFractionalIndices( @@ -177,8 +184,8 @@ export const syncMovedIndices = ( ); // split mutation so we don't end up in an incosistent state - for (const [element, update] of elementsUpdates) { - mutateElement(element, elementsMap, update); + for (const [element, { index }] of elementsUpdates) { + mutateElement(element, elementsMap, { index }); } } catch (e) { // fallback to default sync @@ -189,7 +196,7 @@ export const syncMovedIndices = ( }; /** - * Synchronizes all invalid fractional indices with the array order by mutating passed elements. + * Synchronizes all invalid fractional indices within the array order by mutating elements in the passed array. * * WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself. */ @@ -200,13 +207,32 @@ export const syncInvalidIndices = ( const indicesGroups = getInvalidIndicesGroups(elements); const elementsUpdates = generateIndices(elements, indicesGroups); - for (const [element, update] of elementsUpdates) { - mutateElement(element, elementsMap, update); + for (const [element, { index }] of elementsUpdates) { + mutateElement(element, elementsMap, { index }); } return elements as OrderedExcalidrawElement[]; }; +/** + * Synchronizes all invalid fractional indices within the array order by creating new instances of elements with corrected indices. + * + * WARN: in edge cases it could modify the elements which were not moved, as it's impossible to guess the actually moved elements from the elements array itself. + */ +export const syncInvalidIndicesImmutable = ( + elements: readonly ExcalidrawElement[], +): SceneElementsMap | undefined => { + const syncedElements = arrayToMap(elements); + const indicesGroups = getInvalidIndicesGroups(elements); + const elementsUpdates = generateIndices(elements, indicesGroups); + + for (const [element, { index }] of elementsUpdates) { + syncedElements.set(element.id, newElementWith(element, { index })); + } + + return syncedElements as SceneElementsMap; +}; + /** * Get contiguous groups of indices of passed moved elements. * diff --git a/packages/element/src/mutateElement.ts b/packages/element/src/mutateElement.ts index 84785c31c9..4b5526917f 100644 --- a/packages/element/src/mutateElement.ts +++ b/packages/element/src/mutateElement.ts @@ -23,7 +23,7 @@ import type { export type ElementUpdate = Omit< Partial, - "id" | "version" | "versionNonce" | "updated" + "id" | "updated" >; /** @@ -137,8 +137,8 @@ export const mutateElement = >( ShapeCache.delete(element); } - element.version++; - element.versionNonce = randomInteger(); + element.version = updates.version ?? element.version + 1; + element.versionNonce = updates.versionNonce ?? randomInteger(); element.updated = getUpdatedTimestamp(); return element; @@ -172,9 +172,9 @@ export const newElementWith = ( return { ...element, ...updates, + version: updates.version ?? element.version + 1, + versionNonce: updates.versionNonce ?? randomInteger(), updated: getUpdatedTimestamp(), - version: element.version + 1, - versionNonce: randomInteger(), }; }; diff --git a/packages/element/src/store.ts b/packages/element/src/store.ts index fb8926d88b..da7352e3ee 100644 --- a/packages/element/src/store.ts +++ b/packages/element/src/store.ts @@ -19,9 +19,17 @@ import { newElementWith } from "./mutateElement"; import { ElementsDelta, AppStateDelta, Delta } from "./delta"; -import { hashElementsVersion, hashString } from "./index"; +import { + syncInvalidIndicesImmutable, + hashElementsVersion, + hashString, +} from "./index"; -import type { OrderedExcalidrawElement, SceneElementsMap } from "./types"; +import type { + ExcalidrawElement, + OrderedExcalidrawElement, + SceneElementsMap, +} from "./types"; export const CaptureUpdateAction = { /** @@ -105,7 +113,7 @@ export class Store { params: | { action: CaptureUpdateActionType; - elements: SceneElementsMap | undefined; + elements: readonly ExcalidrawElement[] | undefined; appState: AppState | ObservedAppState | undefined; } | { @@ -133,9 +141,15 @@ export class Store { this.app.scene.getElementsMapIncludingDeleted(), this.app.state, ); + const scheduledSnapshot = currentSnapshot.maybeClone( action, - params.elements, + // let's sync invalid indices first, so that we could detect this change + // also have the synced elements immutable, so that we don't mutate elements, + // that are already in the scene, otherwise we wouldn't see any change + params.elements + ? syncInvalidIndicesImmutable(params.elements) + : undefined, params.appState, ); @@ -213,16 +227,7 @@ export class Store { // using the same instance, since in history we have a check against `HistoryEntry`, so that we don't re-record the same delta again storeDelta = delta; } else { - // calculate the deltas based on the previous and next snapshot - const elementsDelta = snapshot.metadata.didElementsChange - ? ElementsDelta.calculate(prevSnapshot.elements, snapshot.elements) - : ElementsDelta.empty(); - - const appStateDelta = snapshot.metadata.didAppStateChange - ? AppStateDelta.calculate(prevSnapshot.appState, snapshot.appState) - : AppStateDelta.empty(); - - storeDelta = StoreDelta.create(elementsDelta, appStateDelta); + storeDelta = StoreDelta.calculate(prevSnapshot, snapshot); } if (!storeDelta.isEmpty()) { @@ -505,6 +510,24 @@ export class StoreDelta { return new this(opts.id, elements, appState); } + /** + * Calculate the delta between the previous and next snapshot. + */ + public static calculate( + prevSnapshot: StoreSnapshot, + nextSnapshot: StoreSnapshot, + ) { + const elementsDelta = nextSnapshot.metadata.didElementsChange + ? ElementsDelta.calculate(prevSnapshot.elements, nextSnapshot.elements) + : ElementsDelta.empty(); + + const appStateDelta = nextSnapshot.metadata.didAppStateChange + ? AppStateDelta.calculate(prevSnapshot.appState, nextSnapshot.appState) + : AppStateDelta.empty(); + + return this.create(elementsDelta, appStateDelta); + } + /** * Restore a store delta instance from a DTO. */ @@ -524,9 +547,7 @@ export class StoreDelta { id, elements: { added, removed, updated }, }: DTO) { - const elements = ElementsDelta.create(added, removed, updated, { - shouldRedistribute: false, - }); + const elements = ElementsDelta.create(added, removed, updated); return new this(id, elements, AppStateDelta.empty()); } @@ -534,27 +555,10 @@ export class StoreDelta { /** * Inverse store delta, creates new instance of `StoreDelta`. */ - public static inverse(delta: StoreDelta): StoreDelta { + public static inverse(delta: StoreDelta) { return this.create(delta.elements.inverse(), delta.appState.inverse()); } - /** - * Apply latest (remote) changes to the delta, creates new instance of `StoreDelta`. - */ - public static applyLatestChanges( - delta: StoreDelta, - elements: SceneElementsMap, - modifierOptions: "deleted" | "inserted", - ): StoreDelta { - return this.create( - delta.elements.applyLatestChanges(elements, modifierOptions), - delta.appState, - { - id: delta.id, - }, - ); - } - /** * Apply the delta to the passed elements and appState, does not modify the snapshot. */ @@ -562,12 +566,9 @@ export class StoreDelta { delta: StoreDelta, elements: SceneElementsMap, appState: AppState, - prevSnapshot: StoreSnapshot = StoreSnapshot.empty(), ): [SceneElementsMap, AppState, boolean] { - const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo( - elements, - prevSnapshot.elements, - ); + const [nextElements, elementsContainVisibleChange] = + delta.elements.applyTo(elements); const [nextAppState, appStateContainsVisibleChange] = delta.appState.applyTo(appState, nextElements); @@ -578,6 +579,28 @@ export class StoreDelta { return [nextElements, nextAppState, appliedVisibleChanges]; } + /** + * Apply latest (remote) changes to the delta, creates new instance of `StoreDelta`. + */ + public static applyLatestChanges( + delta: StoreDelta, + prevElements: SceneElementsMap, + nextElements: SceneElementsMap, + modifierOptions?: "deleted" | "inserted", + ): StoreDelta { + return this.create( + delta.elements.applyLatestChanges( + prevElements, + nextElements, + modifierOptions, + ), + delta.appState, + { + id: delta.id, + }, + ); + } + public isEmpty() { return this.elements.isEmpty() && this.appState.isEmpty(); } @@ -687,11 +710,10 @@ export class StoreSnapshot { nextElements.set(id, changedElement); } - const nextAppState = Object.assign( - {}, - this.appState, - change.appState, - ) as ObservedAppState; + const nextAppState = getObservedAppState({ + ...this.appState, + ...change.appState, + }); return StoreSnapshot.create(nextElements, nextAppState, { // by default we assume that change is different from what we have in the snapshot @@ -944,18 +966,26 @@ const getDefaultObservedAppState = (): ObservedAppState => { }; }; -export const getObservedAppState = (appState: AppState): ObservedAppState => { +export const getObservedAppState = ( + appState: AppState | ObservedAppState, +): ObservedAppState => { const observedAppState = { name: appState.name, editingGroupId: appState.editingGroupId, viewBackgroundColor: appState.viewBackgroundColor, selectedElementIds: appState.selectedElementIds, selectedGroupIds: appState.selectedGroupIds, - editingLinearElementId: appState.editingLinearElement?.elementId || null, - selectedLinearElementId: appState.selectedLinearElement?.elementId || null, croppingElementId: appState.croppingElementId, activeLockedId: appState.activeLockedId, lockedMultiSelections: appState.lockedMultiSelections, + editingLinearElementId: + (appState as AppState).editingLinearElement?.elementId ?? // prefer app state, as it's likely newer + (appState as ObservedAppState).editingLinearElementId ?? // fallback to observed app state, as it's likely older coming from a previous snapshot + null, + selectedLinearElementId: + (appState as AppState).selectedLinearElement?.elementId ?? + (appState as ObservedAppState).selectedLinearElementId ?? + null, }; Reflect.defineProperty(observedAppState, hiddenObservedAppStateProp, { diff --git a/packages/element/tests/duplicate.test.tsx b/packages/element/tests/duplicate.test.tsx index ed25083904..10b9346a6c 100644 --- a/packages/element/tests/duplicate.test.tsx +++ b/packages/element/tests/duplicate.test.tsx @@ -505,8 +505,6 @@ describe("group-related duplication", () => { mouse.up(frame.x + frame.width + 50, frame.y + frame.height + 50); }); - // console.log(h.elements); - assertElements(h.elements, [ { id: frame.id }, { id: rectangle1.id, frameId: frame.id }, diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 7206737b38..c6231415a2 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -103,6 +103,7 @@ import { } from "@excalidraw/common"; import { + getObservedAppState, getCommonBounds, getElementAbsoluteCoords, bindOrUnbindLinearElements, @@ -260,7 +261,6 @@ import type { ExcalidrawNonSelectionElement, ExcalidrawArrowElement, ExcalidrawElbowArrowElement, - SceneElementsMap, } from "@excalidraw/element/types"; import type { Mutable, ValueOf } from "@excalidraw/common/utility-types"; @@ -702,6 +702,8 @@ class App extends React.Component { addFiles: this.addFiles, resetScene: this.resetScene, getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted, + getSceneElementsMapIncludingDeleted: + this.getSceneElementsMapIncludingDeleted, history: { clear: this.resetHistory, }, @@ -3909,22 +3911,18 @@ class App extends React.Component { }) => { const { elements, appState, collaborators, captureUpdate } = sceneData; - const nextElements = elements ? syncInvalidIndices(elements) : undefined; - if (captureUpdate) { - const nextElementsMap = elements - ? (arrayToMap(nextElements ?? []) as SceneElementsMap) - : undefined; - - const nextAppState = appState - ? // new instance, with partial appstate applied to previously captured one, including hidden prop inside `prevCommittedAppState` - Object.assign({}, this.store.snapshot.appState, appState) + const observedAppState = appState + ? getObservedAppState({ + ...this.store.snapshot.appState, + ...appState, + }) : undefined; this.store.scheduleMicroAction({ action: captureUpdate, - elements: nextElementsMap, - appState: nextAppState, + elements: elements ?? [], + appState: observedAppState, }); } @@ -3932,8 +3930,8 @@ class App extends React.Component { this.setState(appState); } - if (nextElements) { - this.scene.replaceAllElements(nextElements); + if (elements) { + this.scene.replaceAllElements(elements); } if (collaborators) { @@ -10550,7 +10548,7 @@ class App extends React.Component { // otherwise we would end up with duplicated fractional indices on undo this.store.scheduleMicroAction({ action: CaptureUpdateAction.NEVER, - elements: arrayToMap(elements) as SceneElementsMap, + elements, appState: undefined, }); diff --git a/packages/excalidraw/history.ts b/packages/excalidraw/history.ts index cd9dcffe23..7250dd600a 100644 --- a/packages/excalidraw/history.ts +++ b/packages/excalidraw/history.ts @@ -4,14 +4,81 @@ import { CaptureUpdateAction, StoreChange, StoreDelta, - type Store, } from "@excalidraw/element"; +import type { StoreSnapshot, Store } from "@excalidraw/element"; + import type { SceneElementsMap } from "@excalidraw/element/types"; import type { AppState } from "./types"; -class HistoryEntry extends StoreDelta {} +export class HistoryDelta extends StoreDelta { + /** + * Apply the delta to the passed elements and appState, does not modify the snapshot. + */ + public applyTo( + elements: SceneElementsMap, + appState: AppState, + snapshot: StoreSnapshot, + ): [SceneElementsMap, AppState, boolean] { + const [nextElements, elementsContainVisibleChange] = this.elements.applyTo( + elements, + // used to fallback into local snapshot in case we couldn't apply the delta + // due to a missing (force deleted) elements in the scene + snapshot.elements, + // we don't want to apply the `version` and `versionNonce` properties for history + // as we always need to end up with a new version due to collaboration, + // approaching each undo / redo as a new user action + { + excludedProperties: new Set(["version", "versionNonce"]), + }, + ); + + const [nextAppState, appStateContainsVisibleChange] = this.appState.applyTo( + appState, + nextElements, + ); + + const appliedVisibleChanges = + elementsContainVisibleChange || appStateContainsVisibleChange; + + return [nextElements, nextAppState, appliedVisibleChanges]; + } + + /** + * Overriding once to avoid type casting everywhere. + */ + public static override calculate( + prevSnapshot: StoreSnapshot, + nextSnapshot: StoreSnapshot, + ) { + return super.calculate(prevSnapshot, nextSnapshot) as HistoryDelta; + } + + /** + * Overriding once to avoid type casting everywhere. + */ + public static override inverse(delta: StoreDelta): HistoryDelta { + return super.inverse(delta) as HistoryDelta; + } + + /** + * Overriding once to avoid type casting everywhere. + */ + public static override applyLatestChanges( + delta: StoreDelta, + prevElements: SceneElementsMap, + nextElements: SceneElementsMap, + modifierOptions?: "deleted" | "inserted", + ) { + return super.applyLatestChanges( + delta, + prevElements, + nextElements, + modifierOptions, + ) as HistoryDelta; + } +} export class HistoryChangedEvent { constructor( @@ -25,8 +92,8 @@ export class History { [HistoryChangedEvent] >(); - public readonly undoStack: HistoryEntry[] = []; - public readonly redoStack: HistoryEntry[] = []; + public readonly undoStack: HistoryDelta[] = []; + public readonly redoStack: HistoryDelta[] = []; public get isUndoStackEmpty() { return this.undoStack.length === 0; @@ -48,16 +115,16 @@ export class History { * Do not re-record history entries, which were already pushed to undo / redo stack, as part of history action. */ public record(delta: StoreDelta) { - if (delta.isEmpty() || delta instanceof HistoryEntry) { + if (delta.isEmpty() || delta instanceof HistoryDelta) { return; } // construct history entry, so once it's emitted, it's not recorded again - const entry = HistoryEntry.inverse(delta); + const historyDelta = HistoryDelta.inverse(delta); - this.undoStack.push(entry); + this.undoStack.push(historyDelta); - if (!entry.elements.isEmpty()) { + if (!historyDelta.elements.isEmpty()) { // don't reset redo stack on local appState changes, // as a simple click (unselect) could lead to losing all the redo entries // only reset on non empty elements changes! @@ -74,7 +141,7 @@ export class History { elements, appState, () => History.pop(this.undoStack), - (entry: HistoryEntry) => History.push(this.redoStack, entry, elements), + (entry: HistoryDelta) => History.push(this.redoStack, entry), ); } @@ -83,20 +150,20 @@ export class History { elements, appState, () => History.pop(this.redoStack), - (entry: HistoryEntry) => History.push(this.undoStack, entry, elements), + (entry: HistoryDelta) => History.push(this.undoStack, entry), ); } private perform( elements: SceneElementsMap, appState: AppState, - pop: () => HistoryEntry | null, - push: (entry: HistoryEntry) => void, + pop: () => HistoryDelta | null, + push: (entry: HistoryDelta) => void, ): [SceneElementsMap, AppState] | void { try { - let historyEntry = pop(); + let historyDelta = pop(); - if (historyEntry === null) { + if (historyDelta === null) { return; } @@ -108,41 +175,47 @@ export class History { let nextAppState = appState; let containsVisibleChange = false; - // iterate through the history entries in case they result in no visible changes - while (historyEntry) { + // iterate through the history entries in case ;they result in no visible changes + while (historyDelta) { try { [nextElements, nextAppState, containsVisibleChange] = - StoreDelta.applyTo( - historyEntry, - nextElements, - nextAppState, - prevSnapshot, - ); + historyDelta.applyTo(nextElements, nextAppState, prevSnapshot); + const prevElements = prevSnapshot.elements; const nextSnapshot = prevSnapshot.maybeClone( action, nextElements, nextAppState, ); - // schedule immediate capture, so that it's emitted for the sync purposes - this.store.scheduleMicroAction({ - action, - change: StoreChange.create(prevSnapshot, nextSnapshot), - delta: historyEntry, - }); + const change = StoreChange.create(prevSnapshot, nextSnapshot); + const delta = HistoryDelta.applyLatestChanges( + historyDelta, + prevElements, + nextElements, + ); + + if (!delta.isEmpty()) { + // schedule immediate capture, so that it's emitted for the sync purposes + this.store.scheduleMicroAction({ + action, + change, + delta, + }); + + historyDelta = delta; + } prevSnapshot = nextSnapshot; } finally { - // make sure to always push, even if the delta is corrupted - push(historyEntry); + push(historyDelta); } if (containsVisibleChange) { break; } - historyEntry = pop(); + historyDelta = pop(); } return [nextElements, nextAppState]; @@ -155,7 +228,7 @@ export class History { } } - private static pop(stack: HistoryEntry[]): HistoryEntry | null { + private static pop(stack: HistoryDelta[]): HistoryDelta | null { if (!stack.length) { return null; } @@ -169,18 +242,8 @@ export class History { return null; } - private static push( - stack: HistoryEntry[], - entry: HistoryEntry, - prevElements: SceneElementsMap, - ) { - const inversedEntry = HistoryEntry.inverse(entry); - const updatedEntry = HistoryEntry.applyLatestChanges( - inversedEntry, - prevElements, - "inserted", - ); - - return stack.push(updatedEntry); + private static push(stack: HistoryDelta[], entry: HistoryDelta) { + const inversedEntry = HistoryDelta.inverse(entry); + return stack.push(inversedEntry); } } diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 54fce16156..079e82da93 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -1269,12 +1269,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -20, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1420,14 +1422,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -1459,7 +1461,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 915032327, "width": 20, "x": -10, "y": 0, @@ -1511,12 +1513,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1563,12 +1567,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1598,9 +1604,11 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "id0": { "deleted": { "index": "a2", + "version": 4, }, "inserted": { "index": "a0", + "version": 3, }, }, }, @@ -1745,14 +1753,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -1784,7 +1792,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 915032327, "width": 20, "x": -10, "y": 0, @@ -1836,12 +1844,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1888,12 +1898,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1923,9 +1935,11 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "id0": { "deleted": { "index": "a2", + "version": 4, }, "inserted": { "index": "a0", + "version": 3, }, }, }, @@ -2131,12 +2145,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -20, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2287,7 +2303,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1116226695, + "versionNonce": 1014066025, "width": 10, "x": -20, "y": -10, @@ -2339,12 +2355,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -20, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2370,9 +2388,11 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "id0": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "isDeleted": false, + "version": 3, }, }, }, @@ -2551,14 +2571,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 10, "x": -10, "y": 0, @@ -2610,12 +2630,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -20, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2662,12 +2684,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 10, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -2827,7 +2851,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 493213705, + "versionNonce": 81784553, "width": 20, "x": -10, "y": 0, @@ -2854,14 +2878,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 747212839, "width": 20, "x": 20, "y": 30, @@ -2913,12 +2937,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2965,12 +2991,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3020,9 +3048,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -3030,9 +3060,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -3186,7 +3218,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 908564423, + "versionNonce": 1359939303, "width": 20, "x": -10, "y": 0, @@ -3211,14 +3243,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "opacity": 60, "roughness": 2, "roundness": null, - "seed": 1315507081, + "seed": 640725609, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 9, - "versionNonce": 406373543, + "versionNonce": 908564423, "width": 20, "x": 20, "y": 30, @@ -3270,12 +3302,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3322,12 +3356,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3349,9 +3385,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "strokeColor": "#e03131", + "version": 4, }, "inserted": { "strokeColor": "#1e1e1e", + "version": 3, }, }, }, @@ -3372,9 +3410,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "backgroundColor": "#a5d8ff", + "version": 5, }, "inserted": { "backgroundColor": "transparent", + "version": 4, }, }, }, @@ -3395,9 +3435,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "fillStyle": "cross-hatch", + "version": 6, }, "inserted": { "fillStyle": "solid", + "version": 5, }, }, }, @@ -3418,9 +3460,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "strokeStyle": "dotted", + "version": 7, }, "inserted": { "strokeStyle": "solid", + "version": 6, }, }, }, @@ -3441,9 +3485,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "roughness": 2, + "version": 8, }, "inserted": { "roughness": 1, + "version": 7, }, }, }, @@ -3464,9 +3510,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "opacity": 60, + "version": 9, }, "inserted": { "opacity": 100, + "version": 8, }, }, }, @@ -3500,6 +3548,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roughness": 2, "strokeColor": "#e03131", "strokeStyle": "dotted", + "version": 4, }, "inserted": { "backgroundColor": "transparent", @@ -3508,6 +3557,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roughness": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", + "version": 3, }, }, }, @@ -3652,14 +3702,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1604849351, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -3743,12 +3793,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3795,12 +3847,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3822,9 +3876,11 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "id3": { "deleted": { "index": "Zz", + "version": 4, }, "inserted": { "index": "a1", + "version": 3, }, }, }, @@ -3969,14 +4025,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1014066025, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 915032327, "width": 20, "x": 20, "y": 30, @@ -4060,12 +4116,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4112,12 +4170,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4139,9 +4199,11 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "id3": { "deleted": { "index": "Zz", + "version": 4, }, "inserted": { "index": "a1", + "version": 3, }, }, }, @@ -4296,7 +4358,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 1723083209, + "versionNonce": 1006504105, "width": 20, "x": -10, "y": 0, @@ -4321,14 +4383,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "opacity": 100, "roughness": 1, "roundness": null, - "seed": 238820263, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 760410951, + "versionNonce": 289600103, "width": 20, "x": 20, "y": 30, @@ -4380,12 +4442,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4432,12 +4496,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4487,9 +4553,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -4497,9 +4565,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -4526,21 +4596,25 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "id0": { "deleted": { "groupIds": [], + "version": 5, }, "inserted": { "groupIds": [ "id9", ], + "version": 4, }, }, "id3": { "deleted": { "groupIds": [], + "version": 5, }, "inserted": { "groupIds": [ "id9", ], + "version": 4, }, }, }, @@ -5594,14 +5668,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1604849351, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 493213705, + "versionNonce": 915032327, "width": 10, "x": 12, "y": 0, @@ -5653,12 +5727,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5705,12 +5781,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 12, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6786,7 +6864,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 81784553, + "versionNonce": 1723083209, "width": 10, "x": -10, "y": 0, @@ -6813,14 +6891,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "opacity": 100, "roughness": 1, "roundness": null, - "seed": 238820263, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 747212839, + "versionNonce": 760410951, "width": 10, "x": 12, "y": 0, @@ -6872,12 +6950,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6924,12 +7004,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 12, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7001,9 +7083,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -7011,9 +7095,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -9822,12 +9908,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] un "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -20, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 0dc6e525ba..191add45d8 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -284,9 +284,18 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { + "id687": { + "deleted": { + "version": 17, + }, + "inserted": { + "version": 15, + }, + }, "id688": { "deleted": { "boundElements": [], + "version": 9, }, "inserted": { "boundElements": [ @@ -295,6 +304,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 8, }, }, "id691": { @@ -308,7 +318,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": 0, "gap": 1, }, - "height": "68.55969", + "height": "68.58402", "points": [ [ 0, @@ -316,7 +326,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 98, - "68.55969", + "68.58402", ], ], "startBinding": { @@ -324,6 +334,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02970", "gap": 1, }, + "version": 35, }, "inserted": { "endBinding": { @@ -347,6 +358,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02000", "gap": 1, }, + "version": 32, }, }, "id702": { @@ -357,9 +369,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 12, }, "inserted": { "boundElements": [], + "version": 11, }, }, }, @@ -380,6 +394,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id687": { "deleted": { "boundElements": [], + "version": 18, }, "inserted": { "boundElements": [ @@ -388,6 +403,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 17, }, }, "id691": { @@ -404,6 +420,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], ], "startBinding": null, + "version": 37, "y": 0, }, "inserted": { @@ -423,9 +440,18 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02970", "gap": 1, }, + "version": 35, "y": "35.82151", }, }, + "id702": { + "deleted": { + "version": 14, + }, + "inserted": { + "version": 12, + }, + }, }, }, "id": "id710", @@ -466,12 +492,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id688": { @@ -495,12 +523,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -565,12 +595,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 4, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -783,7 +815,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, + 0, 0, ], ], @@ -799,7 +831,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", "updated": 1, "version": 33, - "width": 100, + "width": 0, "x": 149, "y": 0, } @@ -822,9 +854,18 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { + "id664": { + "deleted": { + "version": 18, + }, + "inserted": { + "version": 16, + }, + }, "id665": { "deleted": { "boundElements": [], + "version": 9, }, "inserted": { "boundElements": [ @@ -833,21 +874,13 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 8, }, }, "id668": { "deleted": { "endBinding": null, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], + "version": 32, }, "inserted": { "endBinding": { @@ -855,16 +888,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": -0, "gap": 1, }, - "points": [ - [ - 0, - 0, - ], - [ - 0, - 0, - ], - ], + "version": 30, }, }, }, @@ -885,6 +909,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id664": { "deleted": { "boundElements": [], + "version": 19, }, "inserted": { "boundElements": [ @@ -893,38 +918,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 18, }, }, "id668": { "deleted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": null, + "version": 33, }, "inserted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": { "elementId": "id664", "focus": 0, "gap": 1, }, + "version": 32, }, }, }, @@ -967,12 +975,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id665": { @@ -996,12 +1006,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -1066,12 +1078,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 4, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -1368,12 +1382,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 6, }, }, "id712": { @@ -1397,12 +1413,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, @@ -1427,10 +1445,12 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": 0, "gap": 1, }, + "version": 11, }, "inserted": { "endBinding": null, "startBinding": null, + "version": 8, }, }, }, @@ -1725,7 +1745,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "11.63758", + "height": "1.36342", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1739,7 +1759,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], [ 98, - "11.63758", + "1.36342", ], ], "roughness": 1, @@ -1758,12 +1778,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 11, "width": 98, "x": 1, "y": 0, }, "inserted": { "isDeleted": true, + "version": 8, }, }, }, @@ -1776,9 +1798,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 12, }, "inserted": { "boundElements": [], + "version": 9, }, }, "id721": { @@ -1789,9 +1813,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 11, }, "inserted": { "boundElements": [], + "version": 8, }, }, }, @@ -2015,12 +2041,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id733": { @@ -2044,12 +2072,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -2339,12 +2369,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id737": { @@ -2368,12 +2400,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -2415,7 +2449,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 0, + "height": "370.26975", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -2428,8 +2462,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, - 0, + "498.00000", + "-370.26975", ], ], "roughness": 1, @@ -2446,12 +2480,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": 100, - "x": 0, - "y": 0, + "version": 10, + "width": "498.00000", + "x": 1, + "y": "-37.92697", }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -2464,9 +2500,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 7, }, "inserted": { "boundElements": [], + "version": 4, }, }, "id737": { @@ -2477,9 +2515,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "type": "arrow", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 5, }, }, }, @@ -2632,7 +2672,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 7, + "version": 6, "width": 100, "x": 10, "y": 10, @@ -2709,7 +2749,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "textAlign": "left", "type": "text", "updated": 1, - "version": 11, + "version": 7, "verticalAlign": "top", "width": 30, "x": 15, @@ -2734,41 +2774,12 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id613": { + "id618": { "deleted": { - "isDeleted": false, + "version": 7, }, "inserted": { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "customData": undefined, - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 100, - "index": "a0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 100, - "x": 10, - "y": 10, - }, - }, - "id614": { - "deleted": { - "containerId": null, - }, - "inserted": { - "containerId": null, + "version": 5, }, }, }, @@ -2923,7 +2934,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 11, + "version": 7, "width": 100, "x": 10, "y": 10, @@ -3000,7 +3011,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "textAlign": "left", "type": "text", "updated": 1, - "version": 11, + "version": 5, "verticalAlign": "top", "width": 30, "x": 15, @@ -3027,29 +3038,17 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "deleted": { "containerId": "id623", "isDeleted": true, + "version": 9, }, "inserted": { "containerId": null, "isDeleted": false, + "version": 8, }, }, }, "removed": {}, - "updated": { - "id623": { - "deleted": { - "boundElements": [], - }, - "inserted": { - "boundElements": [ - { - "id": "id624", - "type": "text", - }, - ], - }, - }, - }, + "updated": {}, }, "id": "id632", }, @@ -3311,6 +3310,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "type": "text", }, ], + "version": 10, }, "inserted": { "boundElements": [ @@ -3319,22 +3319,27 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "type": "text", }, ], + "version": 9, }, }, "id578": { "deleted": { "containerId": null, + "version": 9, }, "inserted": { "containerId": "id577", + "version": 8, }, }, "id582": { "deleted": { "containerId": "id577", + "version": 7, }, "inserted": { "containerId": null, + "version": 6, }, }, }, @@ -3586,6 +3591,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "id587": { "deleted": { "boundElements": [], + "version": 8, }, "inserted": { "boundElements": [ @@ -3594,14 +3600,17 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "type": "text", }, ], + "version": 7, }, }, "id588": { "deleted": { "containerId": "id592", + "version": 13, }, "inserted": { "containerId": "id587", + "version": 11, }, }, "id592": { @@ -3612,9 +3621,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "type": "text", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 7, }, }, }, @@ -3830,6 +3841,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "id568": { "deleted": { "boundElements": [], + "version": 9, }, "inserted": { "boundElements": [ @@ -3838,14 +3850,17 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "type": "text", }, ], + "version": 8, }, }, "id569": { "deleted": { "containerId": null, + "version": 10, }, "inserted": { "containerId": "id568", + "version": 9, }, }, }, @@ -4085,12 +4100,14 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, @@ -4098,9 +4115,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "id598": { "deleted": { "containerId": "id597", + "version": 12, }, "inserted": { "containerId": null, + "version": 9, }, }, }, @@ -4346,6 +4365,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "text": "que pasa", "textAlign": "left", "type": "text", + "version": 8, "verticalAlign": "top", "width": 80, "x": 15, @@ -4353,6 +4373,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -4365,9 +4386,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "type": "text", }, ], + "version": 11, }, "inserted": { "boundElements": [], + "version": 8, }, }, }, @@ -4586,11 +4609,13 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "id658": { "deleted": { "angle": 0, + "version": 5, "x": 15, "y": 15, }, "inserted": { "angle": 0, + "version": 7, "x": 15, "y": 15, }, @@ -4815,11 +4840,13 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "id649": { "deleted": { "angle": 90, + "version": 8, "x": 200, "y": 200, }, "inserted": { "angle": 0, + "version": 7, "x": 10, "y": 10, }, @@ -5037,6 +5064,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "deleted": { "boundElements": [], "isDeleted": false, + "version": 8, }, "inserted": { "boundElements": [ @@ -5046,6 +5074,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, ], "isDeleted": true, + "version": 7, }, }, }, @@ -5267,10 +5296,12 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "deleted": { "containerId": null, "isDeleted": false, + "version": 8, }, "inserted": { "containerId": "id641", "isDeleted": true, + "version": 7, }, }, }, @@ -5496,12 +5527,14 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 8, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -5522,10 +5555,10 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "updated": { "id746": { "deleted": { - "frameId": "id745", + "version": 10, }, "inserted": { - "frameId": null, + "version": 8, }, }, }, @@ -5674,7 +5707,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 5, + "version": 4, "width": 100, "x": 0, "y": 0, @@ -5706,7 +5739,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, + "version": 3, "width": 100, "x": 100, "y": 100, @@ -5742,70 +5775,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": {}, - "updated": { - "id468": { - "deleted": { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "customData": undefined, - "fillStyle": "solid", - "frameId": null, - "groupIds": [ - "A", - ], - "height": 100, - "index": "a0", - "isDeleted": true, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 100, - "x": 0, - "y": 0, - }, - "inserted": { - "isDeleted": true, - }, - }, - "id469": { - "deleted": { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "customData": undefined, - "fillStyle": "solid", - "frameId": null, - "groupIds": [ - "A", - ], - "height": 100, - "index": "a1", - "isDeleted": true, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 100, - "x": 100, - "y": 100, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, "id": "id481", }, @@ -6109,12 +6079,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6140,37 +6112,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": {}, - "updated": { - "id413": { - "deleted": { - "angle": 0, - "backgroundColor": "#ffc9c9", - "boundElements": null, - "customData": undefined, - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 10, - "index": "a1", - "isDeleted": true, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 10, - "x": 20, - "y": 0, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, "id": "id428", }, @@ -6188,9 +6130,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id413": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 7, }, "inserted": { "backgroundColor": "transparent", + "version": 6, }, }, }, @@ -6215,37 +6159,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": {}, - "updated": { - "id418": { - "deleted": { - "angle": 0, - "backgroundColor": "#ffc9c9", - "boundElements": null, - "customData": undefined, - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 10, - "index": "a2", - "isDeleted": true, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 10, - "x": 50, - "y": 50, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, "id": "id430", }, @@ -6262,10 +6176,12 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "updated": { "id418": { "deleted": { + "version": 7, "x": 50, "y": 50, }, "inserted": { + "version": 6, "x": 30, "y": 30, }, @@ -6529,12 +6445,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id433": { @@ -6558,12 +6476,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id434": { @@ -6587,12 +6507,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 30, "y": 30, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -7188,12 +7110,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 6, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -7424,37 +7348,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": {}, - "updated": { - "id401": { - "deleted": { - "angle": 0, - "backgroundColor": "#ffec99", - "boundElements": null, - "customData": undefined, - "fillStyle": "solid", - "frameId": null, - "groupIds": [], - "height": 10, - "index": "a0", - "isDeleted": true, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": null, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 10, - "x": 10, - "y": 0, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, "id": "id408", }, @@ -7472,9 +7366,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id401": { "deleted": { "backgroundColor": "#ffec99", + "version": 7, }, "inserted": { "backgroundColor": "transparent", + "version": 6, }, }, }, @@ -7706,9 +7602,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id514": { "deleted": { "index": "a1", + "version": 7, }, "inserted": { "index": "a3", + "version": 6, }, }, }, @@ -7733,6 +7631,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id513": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -7754,6 +7653,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 10, "y": 10, @@ -7762,6 +7662,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id514": { "deleted": { "isDeleted": true, + "version": 8, }, "inserted": { "angle": 0, @@ -7772,7 +7673,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "index": "a3", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -7783,6 +7684,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 100, "x": 20, "y": 20, @@ -7791,6 +7693,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id515": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -7812,6 +7715,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 30, "y": 30, @@ -8050,9 +7954,11 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id502": { "deleted": { "index": "a1", + "version": 6, }, "inserted": { "index": "Zz", + "version": 5, }, }, }, @@ -8077,6 +7983,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id501": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -8098,6 +8005,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 10, "y": 10, @@ -8106,6 +8014,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id502": { "deleted": { "isDeleted": true, + "version": 7, }, "inserted": { "angle": 0, @@ -8116,7 +8025,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "index": "Zz", + "index": "a1", "isDeleted": false, "link": null, "locked": false, @@ -8127,6 +8036,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 6, "width": 100, "x": 20, "y": 20, @@ -8135,6 +8045,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "id503": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -8156,6 +8067,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 30, "y": 30, @@ -8425,12 +8337,14 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 8, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -8477,12 +8391,14 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 8, "width": 10, "x": 30, "y": 30, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -8545,20 +8461,24 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "updated": { "id542": { "deleted": { + "version": 9, "x": 90, "y": 90, }, "inserted": { + "version": 8, "x": 10, "y": 10, }, }, "id545": { "deleted": { + "version": 9, "x": 110, "y": 110, }, "inserted": { + "version": 8, "x": 30, "y": 30, }, @@ -8839,12 +8759,14 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "strokeStyle": "solid", "strokeWidth": 2, "type": "freedraw", + "version": 7, "width": 50, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, @@ -9074,12 +8996,14 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 8, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -9101,10 +9025,12 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "id531": { "deleted": { "height": 90, + "version": 9, "width": 90, }, "inserted": { "height": 10, + "version": 8, "width": 10, }, }, @@ -9309,9 +9235,11 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "id333": { "deleted": { "backgroundColor": "transparent", + "version": 7, }, "inserted": { "backgroundColor": "#ffc9c9", + "version": 6, }, }, }, @@ -9360,12 +9288,14 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -9565,12 +9495,14 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -9592,9 +9524,11 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "id342": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 7, }, "inserted": { "backgroundColor": "transparent", + "version": 6, }, }, }, @@ -9874,9 +9808,11 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "A", "B", ], + "version": 6, }, "inserted": { "groupIds": [], + "version": 5, }, }, "id372": { @@ -9885,9 +9821,11 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "A", "B", ], + "version": 6, }, "inserted": { "groupIds": [], + "version": 5, }, }, }, @@ -10140,12 +10078,14 @@ exports[`history > multiplayer undo/redo > should override remotely added points "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 12, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 11, }, }, }, @@ -10193,6 +10133,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points 20, ], ], + "version": 13, "width": 30, }, "inserted": { @@ -10211,6 +10152,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points 10, ], ], + "version": 12, "width": 10, }, }, @@ -10426,12 +10368,14 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, @@ -10455,16 +10399,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "elements": { "added": {}, "removed": {}, - "updated": { - "id392": { - "deleted": { - "isDeleted": false, - }, - "inserted": { - "isDeleted": false, - }, - }, - }, + "updated": {}, }, "id": "id400", }, @@ -10733,6 +10668,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "6Rm4g567UQM4WjLwej2Vc": { "deleted": { "isDeleted": true, + "version": 3, }, "inserted": { "angle": 0, @@ -10793,6 +10729,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 2, "width": "178.90000", "x": 1035, "y": "274.90000", @@ -10804,6 +10741,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "KPrBI4g_v9qUB1XxYLgSz": { "deleted": { "boundElements": [], + "version": 3, }, "inserted": { "boundElements": [ @@ -10812,11 +10750,13 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "type": "arrow", }, ], + "version": 2, }, }, "u2JGnnmoJ0VATV4vCNJE5": { "deleted": { "boundElements": [], + "version": 3, }, "inserted": { "boundElements": [ @@ -10825,6 +10765,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "type": "arrow", }, ], + "version": 2, }, }, }, @@ -10843,6 +10784,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "KPrBI4g_v9qUB1XxYLgSz": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -10864,14 +10806,16 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 157, - "x": 600, - "y": 0, + "x": 873, + "y": 212, }, }, "u2JGnnmoJ0VATV4vCNJE5": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -10893,6 +10837,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", + "version": 3, "width": 124, "x": 1152, "y": 516, @@ -11073,9 +11018,11 @@ exports[`history > multiplayer undo/redo > should update history entries after r "id349": { "deleted": { "backgroundColor": "#d0bfff", + "version": 12, }, "inserted": { "backgroundColor": "#ffec99", + "version": 11, }, }, }, @@ -11096,9 +11043,11 @@ exports[`history > multiplayer undo/redo > should update history entries after r "id349": { "deleted": { "backgroundColor": "transparent", + "version": 13, }, "inserted": { "backgroundColor": "#d0bfff", + "version": 12, }, }, }, @@ -11147,12 +11096,14 @@ exports[`history > multiplayer undo/redo > should update history entries after r "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11359,6 +11310,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "id329": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -11380,6 +11332,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, @@ -11615,12 +11568,14 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 20, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11926,12 +11881,14 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": -10, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12014,12 +11971,14 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "strokeStyle": "solid", "strokeWidth": 2, "type": "freedraw", + "version": 4, "width": 50, "x": 130, "y": -30, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -12228,9 +12187,11 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "A": { "deleted": { "isDeleted": true, + "version": 5, }, "inserted": { "isDeleted": false, + "version": 4, }, }, }, @@ -12255,12 +12216,14 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -12490,12 +12453,14 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -12725,12 +12690,14 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -12949,6 +12916,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "id53": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -12970,6 +12938,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, @@ -13205,12 +13174,14 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13297,12 +13268,14 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 10, "x": 20, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -13716,12 +13689,14 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id5": { @@ -13747,12 +13722,14 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -13977,12 +13954,14 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id11": { @@ -14006,12 +13985,14 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -14188,9 +14169,11 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "id29": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 10, }, "inserted": { "backgroundColor": "#a5d8ff", + "version": 9, }, }, }, @@ -14211,9 +14194,11 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "id29": { "deleted": { "backgroundColor": "transparent", + "version": 11, }, "inserted": { "backgroundColor": "#ffc9c9", + "version": 10, }, }, }, @@ -14282,12 +14267,14 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -14772,29 +14759,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "id243": { "deleted": { "isDeleted": false, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], + "version": 10, }, "inserted": { "isDeleted": true, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], + "version": 7, }, }, }, @@ -14807,9 +14776,19 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 5, + }, + }, + "id231": { + "deleted": { + "version": 6, + }, + "inserted": { + "version": 4, }, }, "id232": { @@ -14820,9 +14799,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 7, }, "inserted": { "boundElements": [], + "version": 4, }, }, }, @@ -14865,12 +14846,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id231": { @@ -14902,6 +14885,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -14909,6 +14893,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id232": { @@ -14932,12 +14917,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -15010,9 +14997,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, "id231": { @@ -15020,6 +15009,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": "id230", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -15029,6 +15019,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15106,12 +15097,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 6, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -15124,9 +15117,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 4, }, "inserted": { "boundElements": [], + "version": 3, }, }, "id232": { @@ -15137,9 +15132,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, }, @@ -15473,12 +15470,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id213": { @@ -15510,6 +15509,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15517,6 +15517,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id214": { @@ -15540,12 +15541,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -15618,9 +15621,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, "id213": { @@ -15628,6 +15633,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": "id212", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -15637,6 +15643,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15696,7 +15703,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + 98, 0, ], ], @@ -15714,12 +15721,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": 100, - "x": 0, + "version": 10, + "width": 98, + "x": 1, "y": 0, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -15732,9 +15741,19 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 5, + }, + }, + "id213": { + "deleted": { + "version": 8, + }, + "inserted": { + "version": 6, }, }, "id214": { @@ -15745,9 +15764,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 7, }, "inserted": { "boundElements": [], + "version": 4, }, }, }, @@ -16081,12 +16102,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 8, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 7, }, }, "id250": { @@ -16118,6 +16141,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "text": "ola", "textAlign": "left", "type": "text", + "version": 9, "verticalAlign": "top", "width": 100, "x": -200, @@ -16125,6 +16149,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 8, }, }, "id251": { @@ -16148,12 +16173,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 6, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -16226,9 +16253,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "text", }, ], + "version": 9, }, "inserted": { "boundElements": [], + "version": 8, }, }, "id250": { @@ -16236,6 +16265,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": "id249", "height": 25, "textAlign": "center", + "version": 10, "verticalAlign": "middle", "width": 30, "x": -65, @@ -16245,6 +16275,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 9, "verticalAlign": "top", "width": 100, "x": -200, @@ -16304,7 +16335,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + 98, 0, ], ], @@ -16322,12 +16353,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": 100, - "x": 0, + "version": 10, + "width": 98, + "x": 1, "y": 0, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, @@ -16340,9 +16373,19 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 12, }, "inserted": { "boundElements": [], + "version": 9, + }, + }, + "id250": { + "deleted": { + "version": 12, + }, + "inserted": { + "version": 10, }, }, "id251": { @@ -16353,9 +16396,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 9, }, "inserted": { "boundElements": [], + "version": 6, }, }, }, @@ -16673,51 +16718,45 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "id275": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, "id276": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, "updated": { + "id277": { + "deleted": { + "version": 5, + }, + "inserted": { + "version": 3, + }, + }, "id288": { "deleted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": { "elementId": "id275", "focus": 0, "gap": 1, }, + "version": 10, }, "inserted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": null, + "version": 7, }, }, }, @@ -16760,12 +16799,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id276": { @@ -16797,6 +16838,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -16804,6 +16846,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id277": { @@ -16827,12 +16870,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -16905,9 +16950,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, "id276": { @@ -16915,6 +16962,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": "id275", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -16924,6 +16972,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -17001,12 +17050,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 6, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -17019,9 +17070,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 4, }, "inserted": { "boundElements": [], + "version": 3, }, }, "id277": { @@ -17032,9 +17085,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, }, @@ -17380,25 +17435,31 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "id297": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, "id298": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, "id299": { "deleted": { "isDeleted": false, + "version": 5, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -17410,35 +17471,17 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "focus": -0, "gap": 1, }, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": { "elementId": "id297", "focus": 0, "gap": 1, }, + "version": 11, }, "inserted": { "endBinding": null, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": null, + "version": 8, }, }, }, @@ -17481,12 +17524,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id298": { @@ -17518,6 +17563,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -17525,6 +17571,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id299": { @@ -17548,12 +17595,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -17626,9 +17675,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, "id298": { @@ -17636,6 +17687,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": "id297", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -17645,6 +17697,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -17722,12 +17775,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 6, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -17740,9 +17795,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 4, }, "inserted": { "boundElements": [], + "version": 3, }, }, "id299": { @@ -17753,9 +17810,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "type": "arrow", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, }, @@ -18061,12 +18120,14 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -18113,12 +18174,14 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -18165,12 +18228,14 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 40, "y": 40, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -18192,9 +18257,11 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "id195": { "deleted": { "index": "a0V", + "version": 6, }, "inserted": { "index": "a2", + "version": 5, }, }, }, @@ -18257,17 +18324,21 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "id189": { "deleted": { "index": "a2", + "version": 7, }, "inserted": { "index": "Zz", + "version": 6, }, }, "id195": { "deleted": { "index": "a3", + "version": 10, }, "inserted": { "index": "a0", + "version": 9, }, }, }, @@ -18640,12 +18711,14 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id161": { @@ -18671,12 +18744,14 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -18733,12 +18808,14 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 4, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, "id186": { @@ -18764,12 +18841,14 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 4, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -19029,12 +19108,14 @@ exports[`history > singleplayer undo/redo > should support element creation, del "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -19081,12 +19162,14 @@ exports[`history > singleplayer undo/redo > should support element creation, del "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, @@ -19133,12 +19216,14 @@ exports[`history > singleplayer undo/redo > should support element creation, del "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 10, "x": 40, "y": 40, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, @@ -19207,17 +19292,21 @@ exports[`history > singleplayer undo/redo > should support element creation, del "id93": { "deleted": { "isDeleted": true, + "version": 8, }, "inserted": { "isDeleted": false, + "version": 7, }, }, "id96": { "deleted": { "isDeleted": true, + "version": 8, }, "inserted": { "isDeleted": false, + "version": 7, }, }, }, @@ -19466,12 +19555,14 @@ exports[`history > singleplayer undo/redo > should support linear element creati "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 13, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 12, }, }, }, @@ -19510,6 +19601,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati 0, ], ], + "version": 14, "width": 20, }, "inserted": { @@ -19527,6 +19619,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati 10, ], ], + "version": 13, "width": 10, }, }, @@ -19598,6 +19691,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati 20, ], ], + "version": 15, }, "inserted": { "height": 10, @@ -19615,6 +19709,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati 0, ], ], + "version": 14, }, }, }, diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index ddbb76c273..52614ed5f4 100644 --- a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap @@ -25,7 +25,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = ` "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 1505387817, + "versionNonce": 23633383, "width": 30, "x": 30, "y": 20, @@ -50,14 +50,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = ` "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1604849351, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 7, - "versionNonce": 915032327, + "versionNonce": 81784553, "width": 30, "x": -10, "y": 60, @@ -89,7 +89,7 @@ exports[`move element > rectangle 5`] = ` "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1116226695, + "versionNonce": 1014066025, "width": 30, "x": 0, "y": 40, @@ -126,7 +126,7 @@ exports[`move element > rectangles with binding arrow 5`] = ` "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1723083209, + "versionNonce": 1006504105, "width": 100, "x": 0, "y": 0, @@ -156,14 +156,14 @@ exports[`move element > rectangles with binding arrow 6`] = ` "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 7, - "versionNonce": 1051383431, + "versionNonce": 1984422985, "width": 300, "x": 201, "y": 2, @@ -208,7 +208,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` "roundness": { "type": 2, }, - "seed": 1604849351, + "seed": 23633383, "startArrowhead": null, "startBinding": { "elementId": "id0", @@ -221,7 +221,7 @@ exports[`move element > rectangles with binding arrow 7`] = ` "type": "arrow", "updated": 1, "version": 11, - "versionNonce": 1996028265, + "versionNonce": 1573789895, "width": "81.00000", "x": "110.00000", "y": 50, diff --git a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap index 1b0092757a..ee3f024903 100644 --- a/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/multiPointCreate.test.tsx.snap @@ -50,7 +50,7 @@ exports[`multi point mode in linear elements > arrow 3`] = ` "type": "arrow", "updated": 1, "version": 8, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 70, "x": 30, "y": 30, @@ -105,7 +105,7 @@ exports[`multi point mode in linear elements > line 3`] = ` "type": "line", "updated": 1, "version": 8, - "versionNonce": 400692809, + "versionNonce": 1604849351, "width": 70, "x": 30, "y": 30, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index 60fe9249fc..d90fe92914 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -169,12 +169,14 @@ exports[`given element A and group of elements B and given both are selected whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -221,12 +223,14 @@ exports[`given element A and group of elements B and given both are selected whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -273,12 +277,14 @@ exports[`given element A and group of elements B and given both are selected whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 60, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -351,10 +357,12 @@ exports[`given element A and group of elements B and given both are selected whe "id15", ], "index": "a2", + "version": 5, }, "inserted": { "groupIds": [], "index": "a0", + "version": 3, }, }, "id6": { @@ -363,10 +371,12 @@ exports[`given element A and group of elements B and given both are selected whe "id15", ], "index": "a3", + "version": 5, }, "inserted": { "groupIds": [], "index": "a2", + "version": 3, }, }, }, @@ -587,12 +597,14 @@ exports[`given element A and group of elements B and given both are selected whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -639,12 +651,14 @@ exports[`given element A and group of elements B and given both are selected whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -691,12 +705,14 @@ exports[`given element A and group of elements B and given both are selected whe "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 100, "x": 220, "y": 220, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -747,10 +763,12 @@ exports[`given element A and group of elements B and given both are selected whe "id12", ], "index": "a2", + "version": 5, }, "inserted": { "groupIds": [], "index": "a0", + "version": 3, }, }, "id6": { @@ -759,10 +777,12 @@ exports[`given element A and group of elements B and given both are selected whe "id12", ], "index": "a3", + "version": 5, }, "inserted": { "groupIds": [], "index": "a2", + "version": 3, }, }, }, @@ -984,12 +1004,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1036,12 +1058,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1113,9 +1137,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -1123,9 +1149,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -1199,12 +1227,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 60, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1284,11 +1314,13 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "id12", "id28", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, "id19": { @@ -1296,9 +1328,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [ "id28", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -1307,11 +1341,13 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "id12", "id28", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, }, @@ -1534,12 +1570,14 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1560,10 +1598,12 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "updated": { "id0": { "deleted": { + "version": 4, "x": 25, "y": 25, }, "inserted": { + "version": 3, "x": 0, "y": 0, }, @@ -1742,12 +1782,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1794,12 +1836,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1846,12 +1890,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1924,10 +1970,12 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "id15", ], "index": "a2", + "version": 5, }, "inserted": { "groupIds": [], "index": "a0", + "version": 3, }, }, "id6": { @@ -1936,10 +1984,12 @@ exports[`regression tests > adjusts z order when grouping > [end of test] undo s "id15", ], "index": "a3", + "version": 5, }, "inserted": { "groupIds": [], "index": "a2", + "version": 3, }, }, }, @@ -2113,12 +2163,14 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2165,16 +2217,27 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] undo "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 6, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 5, + }, + }, + }, + "updated": { + "id0": { + "deleted": { + "version": 5, + }, + "inserted": { + "version": 3, }, }, }, - "updated": {}, }, "id": "id6", }, @@ -2343,12 +2406,14 @@ exports[`regression tests > arrow keys > [end of test] undo stack 1`] = ` "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2523,12 +2588,14 @@ exports[`regression tests > can drag element that covers another element, while "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 200, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2575,12 +2642,14 @@ exports[`regression tests > can drag element that covers another element, while "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 200, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2627,12 +2696,14 @@ exports[`regression tests > can drag element that covers another element, while "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 350, "x": 300, "y": 300, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2661,10 +2732,12 @@ exports[`regression tests > can drag element that covers another element, while "updated": { "id3": { "deleted": { + "version": 4, "x": 300, "y": 300, }, "inserted": { + "version": 3, "x": 100, "y": 100, }, @@ -2838,12 +2911,14 @@ exports[`regression tests > change the properties of a shape > [end of test] und "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2865,9 +2940,11 @@ exports[`regression tests > change the properties of a shape > [end of test] und "id0": { "deleted": { "backgroundColor": "#ffec99", + "version": 4, }, "inserted": { "backgroundColor": "transparent", + "version": 3, }, }, }, @@ -2888,9 +2965,11 @@ exports[`regression tests > change the properties of a shape > [end of test] und "id0": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 5, }, "inserted": { "backgroundColor": "#ffec99", + "version": 4, }, }, }, @@ -2911,9 +2990,11 @@ exports[`regression tests > change the properties of a shape > [end of test] und "id0": { "deleted": { "strokeColor": "#1971c2", + "version": 6, }, "inserted": { "strokeColor": "#1e1e1e", + "version": 5, }, }, }, @@ -3067,7 +3148,7 @@ exports[`regression tests > click on an element and drag it > [dragged] element "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1116226695, + "versionNonce": 1014066025, "width": 10, "x": 20, "y": 20, @@ -3119,12 +3200,14 @@ exports[`regression tests > click on an element and drag it > [dragged] undo sta "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3145,10 +3228,12 @@ exports[`regression tests > click on an element and drag it > [dragged] undo sta "updated": { "id0": { "deleted": { + "version": 4, "x": 20, "y": 20, }, "inserted": { + "version": 3, "x": 10, "y": 10, }, @@ -3324,12 +3409,14 @@ exports[`regression tests > click on an element and drag it > [end of test] undo "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3350,10 +3437,12 @@ exports[`regression tests > click on an element and drag it > [end of test] undo "updated": { "id0": { "deleted": { + "version": 4, "x": 20, "y": 20, }, "inserted": { + "version": 3, "x": 10, "y": 10, }, @@ -3375,10 +3464,12 @@ exports[`regression tests > click on an element and drag it > [end of test] undo "updated": { "id0": { "deleted": { + "version": 5, "x": 10, "y": 10, }, "inserted": { + "version": 4, "x": 20, "y": 20, }, @@ -3554,12 +3645,14 @@ exports[`regression tests > click to select a shape > [end of test] undo stack 1 "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3606,12 +3699,14 @@ exports[`regression tests > click to select a shape > [end of test] undo stack 1 "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3809,12 +3904,14 @@ exports[`regression tests > click-drag to select a group > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3861,12 +3958,14 @@ exports[`regression tests > click-drag to select a group > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3913,12 +4012,14 @@ exports[`regression tests > click-drag to select a group > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4116,12 +4217,14 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4168,12 +4271,14 @@ exports[`regression tests > deleting last but one element in editing group shoul "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4245,9 +4350,11 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -4255,9 +4362,11 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -4316,9 +4425,11 @@ exports[`regression tests > deleting last but one element in editing group shoul "id0": { "deleted": { "isDeleted": true, + "version": 5, }, "inserted": { "isDeleted": false, + "version": 4, }, }, }, @@ -4489,7 +4600,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1505387817, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4569,12 +4680,14 @@ exports[`regression tests > deselects group of selected elements on pointer down "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4621,12 +4734,14 @@ exports[`regression tests > deselects group of selected elements on pointer down "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4820,12 +4935,14 @@ exports[`regression tests > deselects group of selected elements on pointer up w "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4872,12 +4989,14 @@ exports[`regression tests > deselects group of selected elements on pointer up w "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5039,7 +5158,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1150084233, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5119,12 +5238,14 @@ exports[`regression tests > deselects selected element on pointer down when poin "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5297,12 +5418,14 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5495,12 +5618,14 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5547,12 +5672,14 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5599,12 +5726,14 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5655,9 +5784,11 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -5665,9 +5796,11 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -5675,9 +5808,11 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -5880,12 +6015,14 @@ exports[`regression tests > drags selected elements from point inside common bou "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5932,12 +6069,14 @@ exports[`regression tests > drags selected elements from point inside common bou "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5978,20 +6117,24 @@ exports[`regression tests > drags selected elements from point inside common bou "updated": { "id0": { "deleted": { + "version": 4, "x": 25, "y": 25, }, "inserted": { + "version": 3, "x": 0, "y": 0, }, }, "id3": { "deleted": { + "version": 4, "x": 135, "y": 135, }, "inserted": { + "version": 3, "x": 110, "y": 110, }, @@ -6163,12 +6306,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 10, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6215,12 +6360,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", + "version": 3, "width": 20, "x": 40, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6267,12 +6414,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 20, "x": 70, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6339,12 +6488,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 4, "width": 50, "x": 130, "y": -10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -6409,12 +6560,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 4, "width": 50, "x": 220, "y": -10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -6484,12 +6637,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 6, "width": 50, "x": 310, "y": -10, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -6529,6 +6684,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 20, ], ], + "version": 8, "width": 80, }, "inserted": { @@ -6547,6 +6703,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 10, ], ], + "version": 6, "width": 50, }, }, @@ -6632,12 +6789,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 6, "width": 50, "x": 430, "y": -10, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -6677,6 +6836,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 20, ], ], + "version": 8, "width": 80, }, "inserted": { @@ -6695,6 +6855,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 10, ], ], + "version": 6, "width": 50, }, }, @@ -6799,12 +6960,14 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "freedraw", + "version": 4, "width": 50, "x": 550, "y": -10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -6980,12 +7143,14 @@ exports[`regression tests > given a group of selected elements with an element t "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7032,12 +7197,14 @@ exports[`regression tests > given a group of selected elements with an element t "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7084,12 +7251,14 @@ exports[`regression tests > given a group of selected elements with an element t "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", + "version": 3, "width": 100, "x": 310, "y": 310, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7308,12 +7477,14 @@ exports[`regression tests > given a selected element A and a not selected elemen "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 1000, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7360,12 +7531,14 @@ exports[`regression tests > given a selected element A and a not selected elemen "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 1000, "x": 500, "y": 500, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7582,12 +7755,14 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 1000, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id1": { @@ -7611,12 +7786,14 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 500, "x": 500, "y": 500, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -7813,12 +7990,14 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 1000, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id1": { @@ -7842,12 +8021,14 @@ exports[`regression tests > given selected element A with lower z-index than uns "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 2, "width": 500, "x": 500, "y": 500, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -7868,10 +8049,12 @@ exports[`regression tests > given selected element A with lower z-index than uns "updated": { "id0": { "deleted": { + "version": 3, "x": 100, "y": 100, }, "inserted": { + "version": 2, "x": 0, "y": 0, }, @@ -8045,12 +8228,14 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -8223,12 +8408,14 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] undo stac "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -8401,12 +8588,14 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] undo stac "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -8625,12 +8814,14 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -8847,12 +9038,14 @@ exports[`regression tests > key 6 selects line tool > [end of test] undo stack 1 "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -9041,12 +9234,14 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] undo sta "strokeStyle": "solid", "strokeWidth": 2, "type": "freedraw", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -9265,12 +9460,14 @@ exports[`regression tests > key a selects arrow tool > [end of test] undo stack "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -9443,12 +9640,14 @@ exports[`regression tests > key d selects diamond tool > [end of test] undo stac "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -9665,12 +9864,14 @@ exports[`regression tests > key l selects line tool > [end of test] undo stack 1 "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -9843,12 +10044,14 @@ exports[`regression tests > key o selects ellipse tool > [end of test] undo stac "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10037,12 +10240,14 @@ exports[`regression tests > key p selects freedraw tool > [end of test] undo sta "strokeStyle": "solid", "strokeWidth": 2, "type": "freedraw", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -10215,12 +10420,14 @@ exports[`regression tests > key r selects rectangle tool > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10401,12 +10608,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10453,12 +10662,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10505,12 +10716,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10561,9 +10774,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -10571,9 +10786,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -10581,9 +10798,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -10641,12 +10860,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, }, }, "id18": { @@ -10672,12 +10893,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 10, "x": 40, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, }, }, "id19": { @@ -10703,16 +10926,43 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 10, "x": 60, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, + }, + }, + }, + "updated": { + "id0": { + "deleted": { + "version": 6, + }, + "inserted": { + "version": 4, + }, + }, + "id3": { + "deleted": { + "version": 6, + }, + "inserted": { + "version": 4, + }, + }, + "id6": { + "deleted": { + "version": 6, + }, + "inserted": { + "version": 4, }, }, }, - "updated": {}, }, "id": "id21", }, @@ -10883,12 +11133,14 @@ exports[`regression tests > noop interaction after undo shouldn't create history "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10935,12 +11187,14 @@ exports[`regression tests > noop interaction after undo shouldn't create history "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -11280,12 +11534,14 @@ exports[`regression tests > shift click on selected element should deselect it o "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11482,12 +11738,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11534,12 +11792,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11602,20 +11862,24 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "updated": { "id0": { "deleted": { + "version": 4, "x": 20, "y": 20, }, "inserted": { + "version": 3, "x": 10, "y": 10, }, }, "id3": { "deleted": { + "version": 4, "x": 40, "y": 20, }, "inserted": { + "version": 3, "x": 30, "y": 10, }, @@ -11795,12 +12059,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11847,12 +12113,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11899,12 +12167,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11955,9 +12225,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -11965,9 +12237,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -11975,9 +12249,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -12004,31 +12280,37 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "id0": { "deleted": { "groupIds": [], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, "id3": { "deleted": { "groupIds": [], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, "id6": { "deleted": { "groupIds": [], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, }, @@ -12210,12 +12492,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12262,12 +12546,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12339,9 +12625,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -12349,9 +12637,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -12402,12 +12692,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 50, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12454,12 +12746,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 50, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12531,9 +12825,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id27", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id18": { @@ -12541,9 +12837,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id27", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -12601,11 +12899,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id12", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, "id15": { @@ -12614,11 +12914,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id27", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id27", ], + "version": 4, }, }, "id18": { @@ -12627,11 +12929,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id27", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id27", ], + "version": 4, }, }, "id3": { @@ -12640,11 +12944,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id12", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, }, @@ -12944,12 +13250,14 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 50, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12996,12 +13304,14 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 50, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13048,12 +13358,14 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 50, "x": 200, "y": 200, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13104,9 +13416,11 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -13114,9 +13428,11 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -13124,9 +13440,11 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -13204,12 +13522,14 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "id11", ], "index": "a2", + "version": 6, }, "inserted": { "groupIds": [ "id11", ], "index": "a0", + "version": 4, }, }, "id6": { @@ -13219,12 +13539,14 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "id11", ], "index": "a3", + "version": 6, }, "inserted": { "groupIds": [ "id11", ], "index": "a2", + "version": 4, }, }, }, @@ -13508,7 +13830,7 @@ exports[`regression tests > switches from group of selected elements to another "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1723083209, + "seed": 289600103, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13588,12 +13910,14 @@ exports[`regression tests > switches from group of selected elements to another "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13640,12 +13964,14 @@ exports[`regression tests > switches from group of selected elements to another "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13692,12 +14018,14 @@ exports[`regression tests > switches from group of selected elements to another "strokeStyle": "solid", "strokeWidth": 2, "type": "diamond", + "version": 3, "width": 100, "x": 310, "y": 310, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13840,7 +14168,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "opacity": 100, "roughness": 1, "roundness": null, - "seed": 1604849351, + "seed": 23633383, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13920,12 +14248,14 @@ exports[`regression tests > switches selected element on pointer down > [end of "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13972,12 +14302,14 @@ exports[`regression tests > switches selected element on pointer down > [end of "strokeStyle": "solid", "strokeWidth": 2, "type": "ellipse", + "version": 3, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -14280,6 +14612,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st 10, ], ], + "version": 9, "width": 60, }, "inserted": { @@ -14302,6 +14635,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st 20, ], ], + "version": 8, "width": 100, }, }, @@ -14329,6 +14663,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st "id6": { "deleted": { "isDeleted": true, + "version": 10, }, "inserted": { "angle": 0, @@ -14371,6 +14706,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", + "version": 9, "width": 60, "x": 130, "y": 10, @@ -14424,12 +14760,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 3, "width": 20, "x": 10, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -14476,12 +14814,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] undo st "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 5, "width": 30, "x": 40, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts index 56af57c80b..78e11d1821 100644 --- a/packages/excalidraw/tests/test-utils.ts +++ b/packages/excalidraw/tests/test-utils.ts @@ -431,12 +431,17 @@ export const assertElements = >( expect(h.state.selectedElementIds).toEqual(selectedElementIds); }; -const stripSeed = (deltas: Record) => +const stripProps = ( + deltas: Record, + props: string[], +) => Object.entries(deltas).reduce((acc, curr) => { const { inserted, deleted, ...rest } = curr[1]; - delete inserted.seed; - delete deleted.seed; + for (const prop of props) { + delete inserted[prop]; + delete deleted[prop]; + } acc[curr[0]] = { inserted, @@ -453,9 +458,9 @@ export const checkpointHistory = (history: History, name: string) => { ...x, elements: { ...x.elements, - added: stripSeed(x.elements.added), - removed: stripSeed(x.elements.removed), - updated: stripSeed(x.elements.updated), + added: stripProps(x.elements.added, ["seed", "versionNonce"]), + removed: stripProps(x.elements.removed, ["seed", "versionNonce"]), + updated: stripProps(x.elements.updated, ["seed", "versionNonce"]), }, })), ).toMatchSnapshot(`[${name}] undo stack`); @@ -465,9 +470,9 @@ export const checkpointHistory = (history: History, name: string) => { ...x, elements: { ...x.elements, - added: stripSeed(x.elements.added), - removed: stripSeed(x.elements.removed), - updated: stripSeed(x.elements.updated), + added: stripProps(x.elements.added, ["seed", "versionNonce"]), + removed: stripProps(x.elements.removed, ["seed", "versionNonce"]), + updated: stripProps(x.elements.updated, ["seed", "versionNonce"]), }, })), ).toMatchSnapshot(`[${name}] redo stack`); diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index 6f3fd0efa8..e0f908177b 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -813,6 +813,9 @@ export interface ExcalidrawImperativeAPI { getSceneElementsIncludingDeleted: InstanceType< typeof App >["getSceneElementsIncludingDeleted"]; + getSceneElementsMapIncludingDeleted: InstanceType< + typeof App + >["getSceneElementsMapIncludingDeleted"]; history: { clear: InstanceType["resetHistory"]; }; From 9e77373c81b9454a958b9481f1b47a745af40bc8 Mon Sep 17 00:00:00 2001 From: Sachintha Lakmin <68807141+sachintha-lk@users.noreply.github.com> Date: Tue, 10 Jun 2025 17:13:39 +0530 Subject: [PATCH 06/22] fix: add generic font family fallbacks before Segoe UI Emoji to fix glyph rendering on windows (#9425) --- packages/common/src/constants.ts | 34 +++++++++++++++++-- packages/common/src/utils.ts | 1 - .../linearElementEditor.test.tsx.snap | 2 +- .../tests/__snapshots__/export.test.tsx.snap | 2 +- .../scene/__snapshots__/export.test.ts.snap | 12 +++---- 5 files changed, 40 insertions(+), 11 deletions(-) diff --git a/packages/common/src/constants.ts b/packages/common/src/constants.ts index b9e5661a94..c3c348cebc 100644 --- a/packages/common/src/constants.ts +++ b/packages/common/src/constants.ts @@ -147,19 +147,49 @@ export const FONT_FAMILY = { Assistant: 10, }; +// Segoe UI Emoji fails to properly fallback for some glyphs: ∞, ∫, ≠ +// so we need to have generic font fallback before it +export const SANS_SERIF_GENERIC_FONT = "sans-serif"; +export const MONOSPACE_GENERIC_FONT = "monospace"; + +export const FONT_FAMILY_GENERIC_FALLBACKS = { + [SANS_SERIF_GENERIC_FONT]: 998, + [MONOSPACE_GENERIC_FONT]: 999, +}; + export const FONT_FAMILY_FALLBACKS = { [CJK_HAND_DRAWN_FALLBACK_FONT]: 100, + ...FONT_FAMILY_GENERIC_FALLBACKS, [WINDOWS_EMOJI_FALLBACK_FONT]: 1000, }; +export function getGenericFontFamilyFallback( + fontFamily: number, +): keyof typeof FONT_FAMILY_GENERIC_FALLBACKS { + switch (fontFamily) { + case FONT_FAMILY.Cascadia: + case FONT_FAMILY["Comic Shanns"]: + return MONOSPACE_GENERIC_FONT; + + default: + return SANS_SERIF_GENERIC_FONT; + } +} + export const getFontFamilyFallbacks = ( fontFamily: number, ): Array => { + const genericFallbackFont = getGenericFontFamilyFallback(fontFamily); + switch (fontFamily) { case FONT_FAMILY.Excalifont: - return [CJK_HAND_DRAWN_FALLBACK_FONT, WINDOWS_EMOJI_FALLBACK_FONT]; + return [ + CJK_HAND_DRAWN_FALLBACK_FONT, + genericFallbackFont, + WINDOWS_EMOJI_FALLBACK_FONT, + ]; default: - return [WINDOWS_EMOJI_FALLBACK_FONT]; + return [genericFallbackFont, WINDOWS_EMOJI_FALLBACK_FONT]; } }; diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 824e88b636..2baa434946 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -103,7 +103,6 @@ export const getFontFamilyString = ({ }) => { for (const [fontFamilyString, id] of Object.entries(FONT_FAMILY)) { if (id === fontFamily) { - // TODO: we should fallback first to generic family names first return `${fontFamilyString}${getFontFamilyFallbacks(id) .map((x) => `, ${x}`) .join("")}`; diff --git a/packages/element/tests/__snapshots__/linearElementEditor.test.tsx.snap b/packages/element/tests/__snapshots__/linearElementEditor.test.tsx.snap index 00857987cc..67639e5bde 100644 --- a/packages/element/tests/__snapshots__/linearElementEditor.test.tsx.snap +++ b/packages/element/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -17,7 +17,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo class="excalidraw-wysiwyg" data-type="wysiwyg" dir="auto" - style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, Segoe UI Emoji;" + style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 26.25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Excalifont, Xiaolai, sans-serif, Segoe UI Emoji;" tabindex="0" wrap="off" /> diff --git a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap index 59ee0a3f85..f03bea54d3 100644 --- a/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/export.test.tsx.snap @@ -2,7 +2,7 @@ exports[`export > export svg-embedded scene > svg-embdedded scene export output 1`] = ` "eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nHVTS27bMFx1MDAxMN33XHUwMDE0grItXHUwMDEy2UW68C7NXHUwMDA3zVwiXdRcdTAwMDW6KLpgxLE0ME1cdTAwMTLkKLZrXHUwMDE4yDG661x1MDAxNXOEXGZpVTTlRFx1MDAwMlxi8M3vzZvh7kNRlLS1UM6KXHUwMDEyNrVQKJ1Yl1x1MDAxZlx1MDAwM/5cdTAwMDTOo9Fsmsa7N52ro2dLZGdcdTAwMTdcdTAwMTfKcEBrPM0+VVV1XGJcdTAwMDJcdTAwMDUr0OTZ7Vx1MDAxN9+LYlx1MDAxN0+2oFxmoVfRLVx1MDAwMv/rXHUwMDEybCihXHUwMDFihqrhts1ua5TUMjL5PEAtYNNSjlx03SjIXHUwMDAyPTmzhGujjFx1MDAwYlx1MDAxNc8mXHUwMDEw/lT0UdTLxplOy8GHnNDeXG7HzSS/XHUwMDA1KjWnbczOerBa5ajGz57idIS/XHUwMDE3xUWbVoNcdTAwMGaCTVx1MDAwNtRYUSOF5idV6lwiMLT3Mmr7O3FyYlx1MDAwNfdBXFzdKTXAqCVsxmBssa+WXHUwMDE5PIDMXHUwMDE4pOGfYN+MrnN50d/w3CmmWFxi5SFcdFx1MDAxYlxu3qadyIp2VlxuXHUwMDFh1VWol2M/3rPlXHUwMDFiuePesKIv//4+XHUwMDFmjchomuOfQHBaZeidWKFcbppeZimuXHUwMDE0NqHPUsHiaNTcLCHv92AmY5O15nxcdTAwMDI1uFPhjcNcdTAwMDa1UD/epCc6Mt/BXHUwMDFmXGKS6+C4c/g6bPP59DJcdTAwMWH2fMZZl8LaObFebD28Kd5cdTAwMDeUo1ZcdTAwMGZcdTAwMTiBTW1G6MFIuNXiUY11LJ9cdTAwMTDWX07X/2xcdTAwMTG/nng/godOXHUwMDExznnUNfFcdTAwMWWEee5cdTAwMDK/feTHb1x1MDAwM3po/1xua2IoWiJ9😀" + 😀" `; exports[`export > exporting svg containing transformed images > svg export output 1`] = ` diff --git a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap index fdf514d90c..cef1fd79eb 100644 --- a/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap +++ b/packages/excalidraw/tests/scene/__snapshots__/export.test.ts.snap @@ -86,7 +86,7 @@ exports[`exportToSvg > with a CJK font 1`] = ` direction="ltr" dominant-baseline="alphabetic" fill="#000000" - font-family="Excalifont, Xiaolai, Segoe UI Emoji" + font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" style="white-space: pre;" text-anchor="start" @@ -104,7 +104,7 @@ exports[`exportToSvg > with a CJK font 1`] = ` direction="ltr" dominant-baseline="alphabetic" fill="#000000" - font-family="Nunito, Segoe UI Emoji" + font-family="Nunito, sans-serif, Segoe UI Emoji" font-size="20px" style="white-space: pre;" text-anchor="start" @@ -122,7 +122,7 @@ exports[`exportToSvg > with a CJK font 1`] = ` direction="ltr" dominant-baseline="alphabetic" fill="#000000" - font-family="Excalifont, Xiaolai, Segoe UI Emoji" + font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" style="white-space: pre;" text-anchor="start" @@ -203,7 +203,7 @@ exports[`exportToSvg > with default arguments 1`] = ` direction="ltr" dominant-baseline="alphabetic" fill="#000000" - font-family="Excalifont, Xiaolai, Segoe UI Emoji" + font-family="Excalifont, Xiaolai, sans-serif, Segoe UI Emoji" font-size="20px" style="white-space: pre;" text-anchor="start" @@ -221,7 +221,7 @@ exports[`exportToSvg > with default arguments 1`] = ` direction="ltr" dominant-baseline="alphabetic" fill="#000000" - font-family="Nunito, Segoe UI Emoji" + font-family="Nunito, sans-serif, Segoe UI Emoji" font-size="20px" style="white-space: pre;" text-anchor="start" @@ -247,5 +247,5 @@ exports[`exportToSvg > with exportEmbedScene 1`] = ` @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAI0AA4AAAAABLQAAAHeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPIrgpsY9TYDjGuJ1F+MUed8Q+Do8lDKF1uhErOw/f7/X7tcy+iEk0SUSWRLDGExBCqZYuETCJVS3jlW/rvPv/y4Fra6szKjvjBubd/Nui7lGEMuAbidbw+g/MfFilLa6qnnWkvd4H/FmjEiWUJBzzQH2jiP/jExvFAH6P5ILeGybomYjxExoeP5okC5TpFmecWVieTSw4FIFBuSJ4Vngd7SlWTd4eom7xMLI/zNBO/FJHO0i6T1Zo29kTTT5Ecc9Nf4b9kP/+RT8dQL/MVxGuKXlYhlEpoZMEgy8mRiGYc/001h9DIJhNS9DEhEBBF8QsQokANCJCI5QoKZLddfb/er5n9XZpln/DYYniZkJR+fpjdo1gCwS/pEkByzEzFRPwywYywwWaOVQCAJFDhTCbyMtz5NzKM+ZdJtQeZmfPVzlkWafcoio0oVoJK/QGDNsMiWn6lPh7sAJo3ujXOJwmjmUNvVJIPNzgSgQhtMDoJAmM/EPVmRd2Cwb7gUMZdoCNqiprKqu32bGxtjOs0w8O+gFG9obdhkj7ZHgJXvyi9V1VWUUeciDjE+P07FJBxPiCij0EiQBmINBFCV4aukhKBBImYd0UfgKZEAIBAxeEBHTKU6I9yhpVChXUCAAAA); } @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIsAA4AAAAABLQAAAHYAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wPILgps9xRFkFEihAtyRcm3sWFhmEeo5Dz8tx/rvpmPqESTBE0lkSBySERCJxSLhLR5U7Uk2jzt3GfnB66lLWNU8Qd04uTtzX70HcgYcA3E63iUZs1n9bQz7eUu8N8CjTixwAIOeKA/0MR/8IlNNx7oYzQf5NYwWddEjIfI+PDR3FGg0bAs9+b23pkrpIJSQKDRjCovPE/2jZqu6Di0XVEulttVWotfakj36YLLQQ0bl7KZpEiOueu9/Jf8839UqzEzzv0A8SF9YJxXCPUSunkwzXN3raIbN+dMdwjdfLKQYoILgYCoiV+AEKVoAwESscqBAvl55Pju68dV2/rv+py/wJu+4f2SMlM2UqnaudRKIPgl5Qwg9QWdJuKXJW6EE163fAIAZIEm97KoGvDi38iw4F8mrV7LbNNzvWJZY9CdqDWnXgiaTQYM2gtraOOVJkRwDlg4tn0SkYaxbEMck0ZwSaJRqNABxqZBYOkVojh71KwZ7AsSyrkLbERHXV9Tu92JA4cOLMs0w5N1BWPi0Fs3SZ8sz4GNN5Tea2tq6SLOqCTE+P1HKKCTCBAxxSAVoBxEkQqhDcdYQ4NCg1QMr04ALA0KABQmiQzYkKPBfpRLvBQqrwwDAA==); } @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAIoAA4AAAAABLQAAAHVAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGx4cLgZgP1NUQVREAAQRCAoAKgsEAAE2AiQDBAQgBYQkByAb5wMRFZQHZF8mb0PNXjkIbeHQ4UpbWDTlYMnK5cOfavAw/zzrESo5EbTf6+zdCyBIAqHiUlkJRMJHmIKskVWRFZ4Uelb50Iz7S0IKNNGB0oyDDbgkjnxA+VCvYpk1vog5la7RoxGwSoMFQHNIwf6bvMXbb7uB/zbQEScWUOABB/oDJf6DJ5YWB/oY5UFuDZN1TcR4iIwPH80dBd1mzcrh6sxZnRpoDQTd1qyp8Hzy6v/JPaP30LMHRmG53ZSD/CblWFLgqIbNBWE2y5SSY+mk9V/qr9Nr9rO2mFcC8rUMi6qITgWjClaVl+6tZxG4/ezyvYhiVKOKkiUQBNKW3wCR1gQCConGgYL6Vik5uYHBH+T6E3yc6OHLTplP/6g2k2hXQPgla1sH3lJf6DTJbxPEedV9bQIAqqDHcZWmC+/+j44tH3TR772uDr2oN6zbTDun2m3YKIReywFCxkEb6b7Skm4ghBglR9vyebxlKPLNt+27HjvxxGew/DGCDtQsGvyXTehdSPR6qVWpddg/nU/LMs3gtu7yCJFbt56xvjyH9E/ovVql2tAfnq0bv/9CILBNpk8584BQPeMxltJeuez6zONGyYS47AK4ke1Awmg5eZRnZSUd); } - @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAY8AA8AAAAADAwAAAXkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgkocZAZgP1NUQVREAHwRCAqKAIgNCyAAATYCJAM8BCAFhCQHIBv4CVFUchKQ/TyMjWWPhbGVsLhp9GSH50tTI0kpiVprztv5Fg//7Ue7b76siAPJNIlodGtQN2PVLBMSh9QsiSdPzOBa2yGm7yb77eytNyKhUQpUht7Tv2xOByU3QenAtgPMB7F4fyAboXSW9/+/n6v/TjQN8ThtnIUEjdIpZXKR+wRP5pOEmJw+KE2scl5CrJImJdM0EiorlZ5YdBMRC560xeTjuPbtLy7QCkiSUAq1ViHQbDaL3Q2bEgVwd6KYhuDuYjSLwJ09yP8F4sIpGoEjBEIpaZS81yy5rcQrjzVpEJ1J/k3+gsP/ff8Il5G/2fNPGkF4pUAjvJfNawrz6+6nByVFqa9kYlFhOgoleq5SgazextGgsaNdiThmeeBJXBYr4z15AL5i/ioJvcwoWg7EgX1xjpO37MHbHk9cvNKHBpa8cWVIoeH9ZVoeX1Sk6ykIsL08SRz2FoqiqiH0H80ZBl8mbYqf/59QLgvtScQdiPfpNCbtCqFRQq+nzZp37cyEJfB7ESNYvPedtBiOKeCsCogqfiETZge9IpzwbzwL6HdlV21S19DYFITRWJv8H8yErsk/8lQez2N5NH4HI3koD+Y+IfILkbMOmItxUMQzUoFWr05wQJLEsdEC75K6R820tQUE+AQklUUEZHTU+fxbmEyk+i6LRAJoC/0YmL5Nar15FkGqz7RnmvctjIyMwqOsoReDAVLV0rwVJEhjp+jRUxhr1HGbAUa3IvzK1cwbXoQOVDGwuSytwfn+4SGxQ2JzLAdGRkcXscPyBAQhTdHRueyuHlEQQ6pXy5nLIFXRQ1pZLJUzlwhocf+WFaTAQxtDBLYC2io05Fdg1p8uIoNwsB+MJSr3zYkchu8OAaX+TjFMB6rugYymaXp0oIATKKGzrMDBZYnhJWXlS/KliW0IeizZcjbhgO1oMziJbZFUPCTWtzd6RCoQBNwf5Rk9j/mjgYOLfD84LO6H+PcPLaHXF/AAkl/fHOct//vHPIZeatoOmqbhlS9pepY6lKBzQzm1aH3VPlmNXKsvFgpOwj7P/M5wzdP3A0RUCV+Jhh/5CAZiYonQhTdv314aaKBlOWyyKAlHdorbyYAqXy4dZC4kLxJgacdvLOs6r63voK3kZcIV4l/PcsiCT15U/TMevMcVIg6UbhoHmy7WOT1wXDhEE1Z2cY6E4DErivD4ug6TNFezbFAHTdGQiPT+rAfBAhwrgCPZOdT6SsdeXdHTmEap+SEbKnmYUKl1kF9ixbBTvAjDi8Q7YTIeJvVGXPKVlMwVSfFrRErAQx6USuEfFmB4AXxvPKPOeytqahwT5VCsEgL5NVYIh7RnYQnQZ8M1wmpj5aGh4k8+gIk4brwGIaTM7D1T/l89ALkmOy2yKTb5MBsigGfagSxd+I0mLtr2Goo0qyBK3Se3V1eCadQGKKzhwsmjoL8DbcjsH/TZ2UwJIYLL2cX9CiYCEVZUDVWa5Td7EUMhYQMR+3ReC44nK7341kBYyFC0+J0Zy0R0AJ2+LirQcHa3APhUgt78p9aYOtMXb8kQ/JHUseYN+SQyliokRGB+U+fVV+/elyG/lb4jcj1HRtNAyNBRcYlDEpJLEhIroFYlYR+19qt2YF3+7UU6JVDcHxum1ZyNvFzxG3vxGV4PxIB3izP535BXlNqlEKCWQPBN0h6G1nAEQvSyjvhlccIYEk6RqBT+rLBhq++6mCeaVmSnhRSRLhqn858kQhmckK8KdDho33l762L3oRbXFaJswnP/GldY6NuVtHvpKmyw2lmyXZVhDWdtPtWkoNUMGcLYD5WxmaYg9eyDLXQhSUMDU0sLUawuaKqvbGhqbIKVYjiQaQQL2njT0LoS/m7lCr4lQzTmuxjCoadioqGD8nYpaSnBrVwZcjQGguzd9Lz9OWZlztV+GGmLZo3W0dAyABUGMFy9YkQGraQZyvui5GUQK8TxDTs4JGuoWSSGjZraeB5NvHiPSv+dQ4gKw8agodG7ECucqIZ2GpDaFVWXt4wC); }original textoriginal text" + @font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAY8AA8AAAAADAwAAAXkAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhwbgkocZAZgP1NUQVREAHwRCAqKAIgNCyAAATYCJAM8BCAFhCQHIBv4CVFUchKQ/TyMjWWPhbGVsLhp9GSH50tTI0kpiVprztv5Fg//7Ue7b76siAPJNIlodGtQN2PVLBMSh9QsiSdPzOBa2yGm7yb77eytNyKhUQpUht7Tv2xOByU3QenAtgPMB7F4fyAboXSW9/+/n6v/TjQN8ThtnIUEjdIpZXKR+wRP5pOEmJw+KE2scl5CrJImJdM0EiorlZ5YdBMRC560xeTjuPbtLy7QCkiSUAq1ViHQbDaL3Q2bEgVwd6KYhuDuYjSLwJ09yP8F4sIpGoEjBEIpaZS81yy5rcQrjzVpEJ1J/k3+gsP/ff8Il5G/2fNPGkF4pUAjvJfNawrz6+6nByVFqa9kYlFhOgoleq5SgazextGgsaNdiThmeeBJXBYr4z15AL5i/ioJvcwoWg7EgX1xjpO37MHbHk9cvNKHBpa8cWVIoeH9ZVoeX1Sk6ykIsL08SRz2FoqiqiH0H80ZBl8mbYqf/59QLgvtScQdiPfpNCbtCqFRQq+nzZp37cyEJfB7ESNYvPedtBiOKeCsCogqfiETZge9IpzwbzwL6HdlV21S19DYFITRWJv8H8yErsk/8lQez2N5NH4HI3koD+Y+IfILkbMOmItxUMQzUoFWr05wQJLEsdEC75K6R820tQUE+AQklUUEZHTU+fxbmEyk+i6LRAJoC/0YmL5Nar15FkGqz7RnmvctjIyMwqOsoReDAVLV0rwVJEhjp+jRUxhr1HGbAUa3IvzK1cwbXoQOVDGwuSytwfn+4SGxQ2JzLAdGRkcXscPyBAQhTdHRueyuHlEQQ6pXy5nLIFXRQ1pZLJUzlwhocf+WFaTAQxtDBLYC2io05Fdg1p8uIoNwsB+MJSr3zYkchu8OAaX+TjFMB6rugYymaXp0oIATKKGzrMDBZYnhJWXlS/KliW0IeizZcjbhgO1oMziJbZFUPCTWtzd6RCoQBNwf5Rk9j/mjgYOLfD84LO6H+PcPLaHXF/AAkl/fHOct//vHPIZeatoOmqbhlS9pepY6lKBzQzm1aH3VPlmNXKsvFgpOwj7P/M5wzdP3A0RUCV+Jhh/5CAZiYonQhTdv314aaKBlOWyyKAlHdorbyYAqXy4dZC4kLxJgacdvLOs6r63voK3kZcIV4l/PcsiCT15U/TMevMcVIg6UbhoHmy7WOT1wXDhEE1Z2cY6E4DErivD4ug6TNFezbFAHTdGQiPT+rAfBAhwrgCPZOdT6SsdeXdHTmEap+SEbKnmYUKl1kF9ixbBTvAjDi8Q7YTIeJvVGXPKVlMwVSfFrRErAQx6USuEfFmB4AXxvPKPOeytqahwT5VCsEgL5NVYIh7RnYQnQZ8M1wmpj5aGh4k8+gIk4brwGIaTM7D1T/l89ALkmOy2yKTb5MBsigGfagSxd+I0mLtr2Goo0qyBK3Se3V1eCadQGKKzhwsmjoL8DbcjsH/TZ2UwJIYLL2cX9CiYCEVZUDVWa5Td7EUMhYQMR+3ReC44nK7341kBYyFC0+J0Zy0R0AJ2+LirQcHa3APhUgt78p9aYOtMXb8kQ/JHUseYN+SQyliokRGB+U+fVV+/elyG/lb4jcj1HRtNAyNBRcYlDEpJLEhIroFYlYR+19qt2YF3+7UU6JVDcHxum1ZyNvFzxG3vxGV4PxIB3izP535BXlNqlEKCWQPBN0h6G1nAEQvSyjvhlccIYEk6RqBT+rLBhq++6mCeaVmSnhRSRLhqn858kQhmckK8KdDho33l762L3oRbXFaJswnP/GldY6NuVtHvpKmyw2lmyXZVhDWdtPtWkoNUMGcLYD5WxmaYg9eyDLXQhSUMDU0sLUawuaKqvbGhqbIKVYjiQaQQL2njT0LoS/m7lCr4lQzTmuxjCoadioqGD8nYpaSnBrVwZcjQGguzd9Lz9OWZlztV+GGmLZo3W0dAyABUGMFy9YkQGraQZyvui5GUQK8TxDTs4JGuoWSSGjZraeB5NvHiPSv+dQ4gKw8agodG7ECucqIZ2GpDaFVWXt4wC); }original textoriginal text" `; From 0d4abd1ddc9b2c0156bb40d5aadfcd982958a2f1 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Tue, 10 Jun 2025 14:28:16 +0200 Subject: [PATCH 07/22] fix: add history capture for paste and drop of images and embeds (#9605) --- packages/excalidraw/components/App.tsx | 165 +- .../tests/__snapshots__/history.test.tsx.snap | 2484 +++++++++++------ packages/excalidraw/tests/helpers/api.ts | 10 +- packages/excalidraw/tests/helpers/mocks.ts | 27 + packages/excalidraw/tests/history.test.tsx | 245 +- 5 files changed, 2063 insertions(+), 868 deletions(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index c6231415a2..8aa87b62bd 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -3006,6 +3006,7 @@ class App extends React.Component { } }; + // TODO: this is so spaghetti, we should refactor it and cover it with tests public pasteFromClipboard = withBatchedUpdates( async (event: ClipboardEvent) => { const isPlainPaste = !!IS_PLAIN_PASTE; @@ -3070,6 +3071,7 @@ class App extends React.Component { const imageElement = this.createImageElement({ sceneX, sceneY }); this.insertImageElement(imageElement, file); this.initializeImageDimensions(imageElement); + this.store.scheduleCapture(); this.setState({ selectedElementIds: makeNextSelectedElementIds( { @@ -3180,6 +3182,7 @@ class App extends React.Component { } } if (embeddables.length) { + this.store.scheduleCapture(); this.setState({ selectedElementIds: Object.fromEntries( embeddables.map((embeddable) => [embeddable.id, true]), @@ -3292,11 +3295,10 @@ class App extends React.Component { this.addMissingFiles(opts.files); } - this.store.scheduleCapture(); - const nextElementsToSelect = excludeElementsInFramesFromSelection(duplicatedElements); + this.store.scheduleCapture(); this.setState( { ...this.state, @@ -3530,7 +3532,7 @@ class App extends React.Component { } this.scene.insertElements(textElements); - + this.store.scheduleCapture(); this.setState({ selectedElementIds: makeNextSelectedElementIds( Object.fromEntries(textElements.map((el) => [el.id, true])), @@ -3552,8 +3554,6 @@ class App extends React.Component { }); PLAIN_PASTE_TOAST_SHOWN = true; } - - this.store.scheduleCapture(); } setAppState: React.Component["setState"] = ( @@ -8978,6 +8978,7 @@ class App extends React.Component { ); this.store.scheduleCapture(); + if (hitLockedElement?.locked) { this.setState({ activeLockedId: @@ -9947,13 +9948,9 @@ class App extends React.Component { const dataURL = this.files[fileId]?.dataURL || (await getDataURL(imageFile)); - const imageElement = this.scene.mutateElement( - _imageElement, - { - fileId, - }, - { informMutation: false, isDragging: false }, - ) as NonDeleted; + let imageElement = newElementWith(_imageElement, { + fileId, + }) as NonDeleted; return new Promise>( async (resolve, reject) => { @@ -9967,20 +9964,38 @@ class App extends React.Component { lastRetrieved: Date.now(), }, ]); - const cachedImageData = this.imageCache.get(fileId); + + let cachedImageData = this.imageCache.get(fileId); + if (!cachedImageData) { this.addNewImagesToImageCache(); - await this.updateImageCache([imageElement]); - } - if (cachedImageData?.image instanceof Promise) { - await cachedImageData.image; + + const { updatedFiles } = await this.updateImageCache([ + imageElement, + ]); + + if (updatedFiles.size) { + ShapeCache.delete(_imageElement); + } + + cachedImageData = this.imageCache.get(fileId); } + + const imageHTML = await cachedImageData?.image; + if ( + imageHTML && this.state.pendingImageElementId !== imageElement.id && this.state.newElement?.id !== imageElement.id ) { - this.initializeImageDimensions(imageElement, true); + const naturalDimensions = this.getImageNaturalDimensions( + imageElement, + imageHTML, + ); + + imageElement = newElementWith(imageElement, naturalDimensions); } + resolve(imageElement); } catch (error: any) { console.error(error); @@ -10012,11 +10027,30 @@ class App extends React.Component { this.scene.insertElement(imageElement); try { - return await this.initializeImage({ + const image = await this.initializeImage({ imageFile, imageElement, showCursorImagePreview, }); + + const nextElements = this.scene + .getElementsIncludingDeleted() + .map((element) => { + if (element.id === image.id) { + return image; + } + + return element; + }); + + // schedules an immediate micro action, which will update snapshot, + // but won't be undoable, which is what we want! + this.updateScene({ + captureUpdate: CaptureUpdateAction.NEVER, + elements: nextElements, + }); + + return image; } catch (error: any) { this.scene.mutateElement(imageElement, { isDeleted: true, @@ -10106,6 +10140,7 @@ class App extends React.Component { if (insertOnCanvasDirectly) { this.insertImageElement(imageElement, imageFile); this.initializeImageDimensions(imageElement); + this.store.scheduleCapture(); this.setState( { selectedElementIds: makeNextSelectedElementIds( @@ -10150,20 +10185,18 @@ class App extends React.Component { } }; - initializeImageDimensions = ( - imageElement: ExcalidrawImageElement, - forceNaturalSize = false, - ) => { - const image = + initializeImageDimensions = (imageElement: ExcalidrawImageElement) => { + const imageHTML = isInitializedImageElement(imageElement) && this.imageCache.get(imageElement.fileId)?.image; - if (!image || image instanceof Promise) { + if (!imageHTML || imageHTML instanceof Promise) { if ( imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value ) { const placeholderSize = 100 / this.state.zoom.value; + this.scene.mutateElement(imageElement, { x: imageElement.x - placeholderSize / 2, y: imageElement.y - placeholderSize / 2, @@ -10175,39 +10208,50 @@ class App extends React.Component { return; } + // if user-created bounding box is below threshold, assume the + // intention was to click instead of drag, and use the image's + // intrinsic size if ( - forceNaturalSize || - // if user-created bounding box is below threshold, assume the - // intention was to click instead of drag, and use the image's - // intrinsic size - (imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && - imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value) + imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && + imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value ) { - const minHeight = Math.max(this.state.height - 120, 160); - // max 65% of canvas height, clamped to <300px, vh - 120px> - const maxHeight = Math.min( - minHeight, - Math.floor(this.state.height * 0.5) / this.state.zoom.value, + const naturalDimensions = this.getImageNaturalDimensions( + imageElement, + imageHTML, ); - const height = Math.min(image.naturalHeight, maxHeight); - const width = height * (image.naturalWidth / image.naturalHeight); - - // add current imageElement width/height to account for previous centering - // of the placeholder image - const x = imageElement.x + imageElement.width / 2 - width / 2; - const y = imageElement.y + imageElement.height / 2 - height / 2; - - this.scene.mutateElement(imageElement, { - x, - y, - width, - height, - crop: null, - }); + this.scene.mutateElement(imageElement, naturalDimensions); } }; + private getImageNaturalDimensions = ( + imageElement: ExcalidrawImageElement, + imageHTML: HTMLImageElement, + ) => { + const minHeight = Math.max(this.state.height - 120, 160); + // max 65% of canvas height, clamped to <300px, vh - 120px> + const maxHeight = Math.min( + minHeight, + Math.floor(this.state.height * 0.5) / this.state.zoom.value, + ); + + const height = Math.min(imageHTML.naturalHeight, maxHeight); + const width = height * (imageHTML.naturalWidth / imageHTML.naturalHeight); + + // add current imageElement width/height to account for previous centering + // of the placeholder image + const x = imageElement.x + imageElement.width / 2 - width / 2; + const y = imageElement.y + imageElement.height / 2 - height / 2; + + return { + x, + y, + width, + height, + crop: null, + }; + }; + /** updates image cache, refreshing updated elements and/or setting status to error for images that fail during element creation */ private updateImageCache = async ( @@ -10219,13 +10263,7 @@ class App extends React.Component { fileIds: elements.map((element) => element.fileId), files, }); - if (updatedFiles.size || erroredFiles.size) { - for (const element of elements) { - if (updatedFiles.has(element.fileId)) { - ShapeCache.delete(element); - } - } - } + if (erroredFiles.size) { this.scene.replaceAllElements( this.scene.getElementsIncludingDeleted().map((element) => { @@ -10261,6 +10299,15 @@ class App extends React.Component { uncachedImageElements, files, ); + + if (updatedFiles.size) { + for (const element of uncachedImageElements) { + if (updatedFiles.has(element.fileId)) { + ShapeCache.delete(element); + } + } + } + if (updatedFiles.size) { this.scene.triggerUpdate(); } @@ -10444,6 +10491,7 @@ class App extends React.Component { const imageElement = this.createImageElement({ sceneX, sceneY }); this.insertImageElement(imageElement, file); this.initializeImageDimensions(imageElement); + this.store.scheduleCapture(); this.setState({ selectedElementIds: makeNextSelectedElementIds( { [imageElement.id]: true }, @@ -10494,6 +10542,7 @@ class App extends React.Component { link: normalizeLink(text), }); if (embeddable) { + this.store.scheduleCapture(); this.setState({ selectedElementIds: { [embeddable.id]: true } }); } } diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 191add45d8..ed4ef54ce4 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -81,14 +81,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id691": true, + "id4": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id691": true, + "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -126,7 +126,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id687", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -156,7 +156,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id688", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -185,7 +185,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id702", + "elementId": "id15", "fixedPoint": [ "0.50000", 1, @@ -197,7 +197,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": "99.19972", - "id": "id691", + "id": "id4", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -238,7 +238,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id691", + "id": "id4", "type": "arrow", }, ], @@ -247,7 +247,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 50, - "id": "id702", + "id": "id15", "index": "a3", "isDeleted": false, "link": null, @@ -284,7 +284,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id687": { + "id0": { "deleted": { "version": 17, }, @@ -292,7 +292,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 15, }, }, - "id688": { + "id1": { "deleted": { "boundElements": [], "version": 9, @@ -300,17 +300,32 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "inserted": { "boundElements": [ { - "id": "id691", + "id": "id4", "type": "arrow", }, ], "version": 8, }, }, - "id691": { + "id15": { + "deleted": { + "boundElements": [ + { + "id": "id4", + "type": "arrow", + }, + ], + "version": 12, + }, + "inserted": { + "boundElements": [], + "version": 11, + }, + }, + "id4": { "deleted": { "endBinding": { - "elementId": "id702", + "elementId": "id15", "fixedPoint": [ "0.50000", 1, @@ -330,7 +345,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], ], "startBinding": { - "elementId": "id687", + "elementId": "id0", "focus": "0.02970", "gap": 1, }, @@ -338,7 +353,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "inserted": { "endBinding": { - "elementId": "id688", + "elementId": "id1", "focus": "-0.02000", "gap": 1, }, @@ -354,31 +369,16 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], ], "startBinding": { - "elementId": "id687", + "elementId": "id0", "focus": "0.02000", "gap": 1, }, "version": 32, }, }, - "id702": { - "deleted": { - "boundElements": [ - { - "id": "id691", - "type": "arrow", - }, - ], - "version": 12, - }, - "inserted": { - "boundElements": [], - "version": 11, - }, - }, }, }, - "id": "id709", + "id": "id22", }, { "appState": AppStateDelta { @@ -391,7 +391,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id687": { + "id0": { "deleted": { "boundElements": [], "version": 18, @@ -399,14 +399,22 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "inserted": { "boundElements": [ { - "id": "id691", + "id": "id4", "type": "arrow", }, ], "version": 17, }, }, - "id691": { + "id15": { + "deleted": { + "version": 14, + }, + "inserted": { + "version": 12, + }, + }, + "id4": { "deleted": { "height": "99.19972", "points": [ @@ -436,7 +444,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], ], "startBinding": { - "elementId": "id687", + "elementId": "id0", "focus": "0.02970", "gap": 1, }, @@ -444,17 +452,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "y": "35.82151", }, }, - "id702": { - "deleted": { - "version": 14, - }, - "inserted": { - "version": 12, - }, - }, }, }, - "id": "id710", + "id": "id23", }, ] `; @@ -471,7 +471,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id687": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -502,7 +502,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 1, }, }, - "id688": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -536,16 +536,16 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "updated": {}, }, - "id": "id690", + "id": "id3", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id691": true, + "id4": true, }, - "selectedLinearElementId": "id691", + "selectedLinearElementId": "id4", }, "inserted": { "selectedElementIds": {}, @@ -556,7 +556,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id691": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -608,7 +608,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "updated": {}, }, - "id": "id693", + "id": "id6", }, ] `; @@ -694,14 +694,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id668": true, + "id4": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id668": true, + "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -739,7 +739,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id664", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -769,7 +769,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id665", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -802,7 +802,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 0, - "id": "id668", + "id": "id4", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -854,7 +854,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id664": { + "id0": { "deleted": { "version": 18, }, @@ -862,7 +862,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 16, }, }, - "id665": { + "id1": { "deleted": { "boundElements": [], "version": 9, @@ -870,21 +870,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "inserted": { "boundElements": [ { - "id": "id668", + "id": "id4", "type": "arrow", }, ], "version": 8, }, }, - "id668": { + "id4": { "deleted": { "endBinding": null, "version": 32, }, "inserted": { "endBinding": { - "elementId": "id665", + "elementId": "id1", "focus": -0, "gap": 1, }, @@ -893,7 +893,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, }, - "id": "id685", + "id": "id21", }, { "appState": AppStateDelta { @@ -906,7 +906,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id664": { + "id0": { "deleted": { "boundElements": [], "version": 19, @@ -914,21 +914,21 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "inserted": { "boundElements": [ { - "id": "id668", + "id": "id4", "type": "arrow", }, ], "version": 18, }, }, - "id668": { + "id4": { "deleted": { "startBinding": null, "version": 33, }, "inserted": { "startBinding": { - "elementId": "id664", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -937,7 +937,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, }, - "id": "id686", + "id": "id22", }, ] `; @@ -954,7 +954,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id664": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -985,7 +985,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 1, }, }, - "id665": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1019,16 +1019,16 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "updated": {}, }, - "id": "id667", + "id": "id3", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id668": true, + "id4": true, }, - "selectedLinearElementId": "id668", + "selectedLinearElementId": "id4", }, "inserted": { "selectedElementIds": {}, @@ -1039,7 +1039,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id668": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1091,7 +1091,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "updated": {}, }, - "id": "id670", + "id": "id6", }, ] `; @@ -1220,7 +1220,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": null, "endBinding": { - "elementId": "id712", + "elementId": "id1", "fixedPoint": [ "0.50000", 1, @@ -1232,7 +1232,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": "1.36342", - "id": "id715", + "id": "id4", "index": "Zz", "isDeleted": false, "lastCommittedPoint": null, @@ -1253,7 +1253,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "roundness": null, "startArrowhead": null, "startBinding": { - "elementId": "id711", + "elementId": "id0", "fixedPoint": [ 1, "0.50000", @@ -1279,7 +1279,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id715", + "id": "id4", "type": "arrow", }, ], @@ -1288,7 +1288,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id711", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -1314,7 +1314,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id715", + "id": "id4", "type": "arrow", }, ], @@ -1323,7 +1323,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id712", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -1361,7 +1361,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id711": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1392,7 +1392,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 6, }, }, - "id712": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1425,10 +1425,10 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, "updated": { - "id715": { + "id4": { "deleted": { "endBinding": { - "elementId": "id712", + "elementId": "id1", "fixedPoint": [ "0.50000", 1, @@ -1437,7 +1437,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "gap": 1, }, "startBinding": { - "elementId": "id711", + "elementId": "id0", "fixedPoint": [ 1, "0.50000", @@ -1455,7 +1455,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, }, - "id": "id719", + "id": "id8", }, ] `; @@ -1584,7 +1584,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": null, "endBinding": { - "elementId": "id721", + "elementId": "id1", "fixedPoint": [ 1, "0.50000", @@ -1596,7 +1596,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": "1.36342", - "id": "id725", + "id": "id5", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1617,7 +1617,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "roundness": null, "startArrowhead": null, "startBinding": { - "elementId": "id720", + "elementId": "id0", "fixedPoint": [ "0.50000", 1, @@ -1643,7 +1643,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], @@ -1652,7 +1652,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id720", + "id": "id0", "index": "a0V", "isDeleted": false, "link": null, @@ -1678,7 +1678,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], @@ -1687,7 +1687,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id721", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -1725,7 +1725,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id725": { + "id5": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1734,7 +1734,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": null, "endBinding": { - "elementId": "id721", + "elementId": "id1", "fixedPoint": [ 1, "0.50000", @@ -1766,7 +1766,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "roundness": null, "startArrowhead": null, "startBinding": { - "elementId": "id720", + "elementId": "id0", "fixedPoint": [ "0.50000", 1, @@ -1790,11 +1790,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, "updated": { - "id720": { + "id0": { "deleted": { "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], @@ -1805,11 +1805,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 9, }, }, - "id721": { + "id1": { "deleted": { "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], @@ -1822,7 +1822,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, }, - "id": "id731", + "id": "id11", }, ] `; @@ -1952,7 +1952,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id732", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -1982,7 +1982,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id733", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -2020,7 +2020,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id732": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2051,7 +2051,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 1, }, }, - "id733": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2085,7 +2085,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "updated": {}, }, - "id": "id735", + "id": "id3", }, ] `; @@ -2176,7 +2176,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id740": true, + "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2210,7 +2210,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], @@ -2219,7 +2219,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id736", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -2245,7 +2245,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], @@ -2254,7 +2254,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": 100, - "id": "id737", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -2283,7 +2283,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id737", + "elementId": "id1", "focus": -0, "gap": 1, }, @@ -2291,7 +2291,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "frameId": null, "groupIds": [], "height": "370.26975", - "id": "id740", + "id": "id4", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -2314,7 +2314,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "startArrowhead": null, "startBinding": { - "elementId": "id736", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -2348,7 +2348,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id736": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2379,7 +2379,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 1, }, }, - "id737": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2413,16 +2413,16 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "updated": {}, }, - "id": "id739", + "id": "id3", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id740": true, + "id4": true, }, - "selectedLinearElementId": "id740", + "selectedLinearElementId": "id4", }, "inserted": { "selectedElementIds": {}, @@ -2433,7 +2433,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id740": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2442,7 +2442,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id737", + "elementId": "id1", "focus": -0, "gap": 1, }, @@ -2472,7 +2472,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "startArrowhead": null, "startBinding": { - "elementId": "id736", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -2492,11 +2492,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, "updated": { - "id736": { + "id0": { "deleted": { "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], @@ -2507,11 +2507,11 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "version": 4, }, }, - "id737": { + "id1": { "deleted": { "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], @@ -2524,7 +2524,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, }, }, - "id": "id744", + "id": "id8", }, ] `; @@ -2650,7 +2650,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id618", + "id": "id5", "type": "text", }, ], @@ -2659,7 +2659,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id613", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -2693,7 +2693,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id614", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -2724,7 +2724,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id613", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2732,7 +2732,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id618", + "id": "id5", "index": "a2", "isDeleted": false, "lineHeight": "1.25000", @@ -2774,7 +2774,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id618": { + "id5": { "deleted": { "version": 7, }, @@ -2784,7 +2784,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id622", + "id": "id9", }, ] `; @@ -2912,7 +2912,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id628", + "id": "id5", "type": "text", }, ], @@ -2921,7 +2921,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id623", + "id": "id0", "index": "Zz", "isDeleted": false, "link": null, @@ -2947,7 +2947,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id623", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2955,7 +2955,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id624", + "id": "id1", "index": "a0", "isDeleted": true, "lineHeight": "1.25000", @@ -2986,7 +2986,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id623", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -2994,7 +2994,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id628", + "id": "id5", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -3034,9 +3034,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "elements": { "added": { - "id624": { + "id1": { "deleted": { - "containerId": "id623", + "containerId": "id0", "isDeleted": true, "version": 9, }, @@ -3050,7 +3050,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "removed": {}, "updated": {}, }, - "id": "id632", + "id": "id9", }, ] `; @@ -3178,7 +3178,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id582", + "id": "id5", "type": "text", }, ], @@ -3187,7 +3187,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id577", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -3213,7 +3213,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id577", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -3221,7 +3221,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id582", + "id": "id5", "index": "a0V", "isDeleted": false, "lineHeight": "1.25000", @@ -3260,7 +3260,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id578", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -3302,11 +3302,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id577": { + "id0": { "deleted": { "boundElements": [ { - "id": "id582", + "id": "id5", "type": "text", }, ], @@ -3315,26 +3315,26 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "inserted": { "boundElements": [ { - "id": "id578", + "id": "id1", "type": "text", }, ], "version": 9, }, }, - "id578": { + "id1": { "deleted": { "containerId": null, "version": 9, }, "inserted": { - "containerId": "id577", + "containerId": "id0", "version": 8, }, }, - "id582": { + "id5": { "deleted": { - "containerId": "id577", + "containerId": "id0", "version": 7, }, "inserted": { @@ -3344,7 +3344,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id586", + "id": "id9", }, ] `; @@ -3476,7 +3476,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id587", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -3502,7 +3502,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id588", + "id": "id1", "type": "text", }, ], @@ -3511,7 +3511,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 60, - "id": "id592", + "id": "id5", "index": "a0V", "isDeleted": false, "link": null, @@ -3537,7 +3537,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id592", + "containerId": "id5", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -3545,7 +3545,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 50, - "id": "id588", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -3588,7 +3588,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id587": { + "id0": { "deleted": { "boundElements": [], "version": 8, @@ -3596,28 +3596,28 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "inserted": { "boundElements": [ { - "id": "id588", + "id": "id1", "type": "text", }, ], "version": 7, }, }, - "id588": { + "id1": { "deleted": { - "containerId": "id592", + "containerId": "id5", "version": 13, }, "inserted": { - "containerId": "id587", + "containerId": "id0", "version": 11, }, }, - "id592": { + "id5": { "deleted": { "boundElements": [ { - "id": "id588", + "id": "id1", "type": "text", }, ], @@ -3630,7 +3630,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id596", + "id": "id9", }, ] `; @@ -3762,7 +3762,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id568", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -3796,7 +3796,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id569", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -3838,7 +3838,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id568": { + "id0": { "deleted": { "boundElements": [], "version": 9, @@ -3846,26 +3846,26 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "inserted": { "boundElements": [ { - "id": "id569", + "id": "id1", "type": "text", }, ], "version": 8, }, }, - "id569": { + "id1": { "deleted": { "containerId": null, "version": 10, }, "inserted": { - "containerId": "id568", + "containerId": "id0", "version": 9, }, }, }, }, - "id": "id576", + "id": "id8", }, ] `; @@ -3993,7 +3993,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id598", + "id": "id1", "type": "text", }, ], @@ -4002,7 +4002,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id597", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -4028,7 +4028,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id597", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -4036,7 +4036,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id598", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -4079,7 +4079,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "elements": { "added": {}, "removed": { - "id597": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -4112,9 +4112,9 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, "updated": { - "id598": { + "id1": { "deleted": { - "containerId": "id597", + "containerId": "id0", "version": 12, }, "inserted": { @@ -4124,7 +4124,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id604", + "id": "id7", }, ] `; @@ -4250,7 +4250,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id606", + "id": "id1", "type": "text", }, ], @@ -4259,7 +4259,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id605", + "id": "id0", "index": "Zz", "isDeleted": false, "link": null, @@ -4285,7 +4285,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id605", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -4293,7 +4293,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id606", + "id": "id1", "index": "a0", "isDeleted": false, "lineHeight": "1.25000", @@ -4336,13 +4336,13 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "elements": { "added": {}, "removed": { - "id606": { + "id1": { "deleted": { "angle": 0, "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id605", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -4378,11 +4378,11 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, "updated": { - "id605": { + "id0": { "deleted": { "boundElements": [ { - "id": "id606", + "id": "id1", "type": "text", }, ], @@ -4395,7 +4395,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id612", + "id": "id7", }, ] `; @@ -4521,7 +4521,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id658", + "id": "id1", "type": "text", }, ], @@ -4530,7 +4530,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id657", + "id": "id0", "index": "Zz", "isDeleted": false, "link": null, @@ -4556,7 +4556,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id657", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -4564,7 +4564,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id658", + "id": "id1", "index": "a0", "isDeleted": false, "lineHeight": "1.25000", @@ -4606,7 +4606,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id658": { + "id1": { "deleted": { "angle": 0, "version": 5, @@ -4622,7 +4622,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id663", + "id": "id6", }, ] `; @@ -4750,7 +4750,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id650", + "id": "id1", "type": "text", }, ], @@ -4759,7 +4759,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id649", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -4785,7 +4785,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id649", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -4793,7 +4793,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 25, - "id": "id650", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -4837,7 +4837,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id649": { + "id0": { "deleted": { "angle": 90, "version": 8, @@ -4853,7 +4853,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, }, }, - "id": "id656", + "id": "id7", }, ] `; @@ -4983,7 +4983,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id633", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -5009,7 +5009,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id633", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -5017,7 +5017,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id634", + "id": "id1", "index": "a1", "isDeleted": true, "lineHeight": "1.25000", @@ -5060,7 +5060,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "elements": { "added": {}, "removed": { - "id633": { + "id0": { "deleted": { "boundElements": [], "isDeleted": false, @@ -5069,7 +5069,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "inserted": { "boundElements": [ { - "id": "id634", + "id": "id1", "type": "text", }, ], @@ -5080,7 +5080,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "updated": {}, }, - "id": "id640", + "id": "id7", }, ] `; @@ -5206,7 +5206,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id642", + "id": "id1", "type": "text", }, ], @@ -5215,7 +5215,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id641", + "id": "id0", "index": "Zz", "isDeleted": true, "link": null, @@ -5249,7 +5249,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "frameId": null, "groupIds": [], "height": 100, - "id": "id642", + "id": "id1", "index": "a0", "isDeleted": false, "lineHeight": "1.25000", @@ -5292,14 +5292,14 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "elements": { "added": {}, "removed": { - "id642": { + "id1": { "deleted": { "containerId": null, "isDeleted": false, "version": 8, }, "inserted": { - "containerId": "id641", + "containerId": "id0", "isDeleted": true, "version": 7, }, @@ -5307,7 +5307,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "updated": {}, }, - "id": "id648", + "id": "id7", }, ] `; @@ -5437,7 +5437,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "frameId": null, "groupIds": [], "height": 100, - "id": "id746", + "id": "id1", "index": "Zz", "isDeleted": false, "link": null, @@ -5467,7 +5467,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "frameId": null, "groupIds": [], "height": 500, - "id": "id745", + "id": "id0", "index": "a0", "isDeleted": true, "link": null, @@ -5506,7 +5506,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "elements": { "added": {}, "removed": { - "id746": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -5540,7 +5540,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre }, "updated": {}, }, - "id": "id755", + "id": "id10", }, { "appState": AppStateDelta { @@ -5553,7 +5553,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "added": {}, "removed": {}, "updated": { - "id746": { + "id1": { "deleted": { "version": 10, }, @@ -5563,7 +5563,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre }, }, }, - "id": "id756", + "id": "id11", }, ] `; @@ -5649,7 +5649,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id469": true, + "id1": true, }, "resizingElement": null, "scrollX": 0, @@ -5694,7 +5694,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id468", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -5726,7 +5726,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id469", + "id": "id1", "index": "a1", "isDeleted": true, "link": null, @@ -5759,8 +5759,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id468": true, - "id469": true, + "id0": true, + "id1": true, }, "selectedGroupIds": { "A": true, @@ -5777,7 +5777,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id481", + "id": "id13", }, { "appState": AppStateDelta { @@ -5790,7 +5790,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "inserted": { "editingGroupId": null, "selectedElementIds": { - "id468": true, + "id0": true, }, "selectedGroupIds": { "A": true, @@ -5803,7 +5803,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id482", + "id": "id14", }, { "appState": AppStateDelta { @@ -5815,7 +5815,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "inserted": { "editingGroupId": "A", "selectedElementIds": { - "id469": true, + "id1": true, }, }, }, @@ -5825,7 +5825,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id485", + "id": "id17", }, ] `; @@ -5911,7 +5911,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id418": true, + "id8": true, }, "resizingElement": null, "scrollX": 0, @@ -5954,7 +5954,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 10, - "id": "id410", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -5984,7 +5984,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 10, - "id": "id413", + "id": "id3", "index": "a1", "isDeleted": true, "link": null, @@ -6014,7 +6014,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 10, - "id": "id418", + "id": "id8", "index": "a2", "isDeleted": true, "link": null, @@ -6047,7 +6047,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id410": true, + "id0": true, }, }, "inserted": { @@ -6058,7 +6058,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": { - "id410": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6092,19 +6092,19 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "updated": {}, }, - "id": "id412", + "id": "id2", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id413": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id410": true, + "id0": true, }, }, }, @@ -6114,7 +6114,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id428", + "id": "id18", }, { "appState": AppStateDelta { @@ -6127,7 +6127,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id413": { + "id3": { "deleted": { "backgroundColor": "#ffc9c9", "version": 7, @@ -6139,19 +6139,19 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, }, }, - "id": "id429", + "id": "id19", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id418": true, + "id8": true, }, }, "inserted": { "selectedElementIds": { - "id413": true, + "id3": true, }, }, }, @@ -6161,7 +6161,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id430", + "id": "id20", }, { "appState": AppStateDelta { @@ -6174,7 +6174,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id418": { + "id8": { "deleted": { "version": 7, "x": 50, @@ -6188,7 +6188,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, }, }, - "id": "id431", + "id": "id21", }, ] `; @@ -6274,15 +6274,15 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id433": true, + "id1": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id433": true, - "id434": true, + "id1": true, + "id2": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -6320,7 +6320,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id432", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -6350,7 +6350,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id433", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -6380,7 +6380,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id434", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -6413,7 +6413,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id432": true, + "id0": true, }, }, "inserted": { @@ -6424,7 +6424,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": { - "id432": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6455,7 +6455,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "version": 1, }, }, - "id433": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6486,7 +6486,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "version": 1, }, }, - "id434": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6520,19 +6520,19 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "updated": {}, }, - "id": "id437", + "id": "id5", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id433": true, + "id1": true, }, }, "inserted": { "selectedElementIds": { - "id432": true, + "id0": true, }, }, }, @@ -6542,14 +6542,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id450", + "id": "id18", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id434": true, + "id2": true, }, }, "inserted": { @@ -6562,7 +6562,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id451", + "id": "id19", }, ] `; @@ -6656,10 +6656,10 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id452": true, - "id453": true, - "id454": true, - "id455": true, + "id0": true, + "id1": true, + "id2": true, + "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": { @@ -6702,7 +6702,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id452", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -6734,7 +6734,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id453", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -6766,7 +6766,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "B", ], "height": 100, - "id": "id454", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -6798,7 +6798,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "B", ], "height": 100, - "id": "id455", + "id": "id3", "index": "a3", "isDeleted": false, "link": null, @@ -6831,8 +6831,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id452": true, - "id453": true, + "id0": true, + "id1": true, }, "selectedGroupIds": { "A": true, @@ -6849,15 +6849,15 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id466", + "id": "id14", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id454": true, - "id455": true, + "id2": true, + "id3": true, }, "selectedGroupIds": { "B": true, @@ -6874,7 +6874,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id467", + "id": "id15", }, ] `; @@ -6965,7 +6965,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id486": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -7006,7 +7006,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 10, - "id": "id486", + "id": "id0", "index": "a0", "isDeleted": true, "lastCommittedPoint": [ @@ -7057,7 +7057,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id486": true, + "id0": true, }, }, "inserted": { @@ -7068,7 +7068,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": { - "id486": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -7123,13 +7123,13 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "updated": {}, }, - "id": "id488", + "id": "id2", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "selectedLinearElementId": "id486", + "selectedLinearElementId": "id0", }, "inserted": { "selectedLinearElementId": null, @@ -7141,13 +7141,13 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id498", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "editingLinearElementId": "id486", + "editingLinearElementId": "id0", }, "inserted": { "editingLinearElementId": null, @@ -7159,7 +7159,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id499", + "id": "id13", }, { "appState": AppStateDelta { @@ -7168,7 +7168,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "editingLinearElementId": null, }, "inserted": { - "editingLinearElementId": "id486", + "editingLinearElementId": "id0", }, }, }, @@ -7177,7 +7177,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id500", + "id": "id14", }, ] `; @@ -7304,7 +7304,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 10, - "id": "id401", + "id": "id0", "index": "a0", "isDeleted": true, "link": null, @@ -7337,7 +7337,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id401": true, + "id0": true, }, }, "inserted": { @@ -7350,7 +7350,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id408", + "id": "id7", }, { "appState": AppStateDelta { @@ -7363,7 +7363,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id401": { + "id0": { "deleted": { "backgroundColor": "#ffec99", "version": 7, @@ -7375,7 +7375,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, }, }, - "id": "id409", + "id": "id8", }, ] `; @@ -7502,7 +7502,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id514", + "id": "id1", "index": "a1", "isDeleted": true, "link": null, @@ -7532,7 +7532,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id515", + "id": "id2", "index": "a3V", "isDeleted": true, "link": null, @@ -7562,7 +7562,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id513", + "id": "id0", "index": "a4", "isDeleted": true, "link": null, @@ -7599,7 +7599,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id514": { + "id1": { "deleted": { "index": "a1", "version": 7, @@ -7611,7 +7611,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, }, }, - "id": "id523", + "id": "id10", }, { "appState": AppStateDelta { @@ -7621,14 +7621,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "inserted": { "selectedElementIds": { - "id514": true, + "id1": true, }, }, }, }, "elements": { "added": { - "id513": { + "id0": { "deleted": { "isDeleted": true, "version": 4, @@ -7659,7 +7659,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "y": 10, }, }, - "id514": { + "id1": { "deleted": { "isDeleted": true, "version": 8, @@ -7690,7 +7690,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "y": 20, }, }, - "id515": { + "id2": { "deleted": { "isDeleted": true, "version": 4, @@ -7725,7 +7725,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id524", + "id": "id11", }, ] `; @@ -7854,7 +7854,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id501", + "id": "id0", "index": "Zx", "isDeleted": true, "link": null, @@ -7884,7 +7884,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id503", + "id": "id2", "index": "Zy", "isDeleted": true, "link": null, @@ -7914,7 +7914,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "frameId": null, "groupIds": [], "height": 100, - "id": "id502", + "id": "id1", "index": "a1", "isDeleted": true, "link": null, @@ -7951,7 +7951,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id502": { + "id1": { "deleted": { "index": "a1", "version": 6, @@ -7963,7 +7963,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, }, }, - "id": "id511", + "id": "id10", }, { "appState": AppStateDelta { @@ -7973,14 +7973,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "inserted": { "selectedElementIds": { - "id502": true, + "id1": true, }, }, }, }, "elements": { "added": { - "id501": { + "id0": { "deleted": { "isDeleted": true, "version": 4, @@ -8011,7 +8011,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "y": 10, }, }, - "id502": { + "id1": { "deleted": { "isDeleted": true, "version": 7, @@ -8042,7 +8042,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "y": 20, }, }, - "id503": { + "id2": { "deleted": { "isDeleted": true, "version": 4, @@ -8077,7 +8077,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id512", + "id": "id11", }, ] `; @@ -8165,16 +8165,16 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id542": true, - "id545": true, + "id0": true, + "id3": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id542": true, - "id545": true, + "id0": true, + "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8212,7 +8212,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 10, - "id": "id542", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -8242,7 +8242,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 10, - "id": "id545", + "id": "id3", "index": "a1", "isDeleted": false, "link": null, @@ -8272,7 +8272,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 100, - "id": "id555", + "id": "id13", "index": "a2", "isDeleted": false, "link": null, @@ -8305,7 +8305,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "delta": Delta { "deleted": { "selectedElementIds": { - "id542": true, + "id0": true, }, }, "inserted": { @@ -8316,7 +8316,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id542": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -8350,19 +8350,19 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "updated": {}, }, - "id": "id563", + "id": "id21", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id545": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id542": true, + "id0": true, }, }, }, @@ -8370,7 +8370,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id545": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -8404,19 +8404,19 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "updated": {}, }, - "id": "id564", + "id": "id22", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id542": true, + "id0": true, }, }, "inserted": { "selectedElementIds": { - "id545": true, + "id3": true, }, }, }, @@ -8426,14 +8426,14 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "removed": {}, "updated": {}, }, - "id": "id565", + "id": "id23", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id545": true, + "id3": true, }, }, "inserted": { @@ -8446,7 +8446,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "removed": {}, "updated": {}, }, - "id": "id566", + "id": "id24", }, { "appState": AppStateDelta { @@ -8459,7 +8459,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "added": {}, "removed": {}, "updated": { - "id542": { + "id0": { "deleted": { "version": 9, "x": 90, @@ -8471,7 +8471,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "y": 10, }, }, - "id545": { + "id3": { "deleted": { "version": 9, "x": 110, @@ -8485,7 +8485,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, }, }, - "id": "id567", + "id": "id25", }, ] `; @@ -8612,7 +8612,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 50, - "id": "id525", + "id": "id0", "index": "a0", "isDeleted": false, "lastCommittedPoint": [ @@ -8671,7 +8671,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 100, - "id": "id526", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -8709,7 +8709,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id525": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -8772,7 +8772,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "updated": {}, }, - "id": "id530", + "id": "id5", }, ] `; @@ -8863,7 +8863,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id531": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -8901,7 +8901,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 90, - "id": "id531", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -8931,7 +8931,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "frameId": null, "groupIds": [], "height": 100, - "id": "id535", + "id": "id4", "index": "a1", "isDeleted": false, "link": null, @@ -8964,7 +8964,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "delta": Delta { "deleted": { "selectedElementIds": { - "id531": true, + "id0": true, }, }, "inserted": { @@ -8975,7 +8975,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id531": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9009,7 +9009,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "updated": {}, }, - "id": "id540", + "id": "id9", }, { "appState": AppStateDelta { @@ -9022,7 +9022,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "added": {}, "removed": {}, "updated": { - "id531": { + "id0": { "deleted": { "height": 90, "version": 9, @@ -9036,7 +9036,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, }, }, - "id": "id541", + "id": "id10", }, ] `; @@ -9127,7 +9127,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id333": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9165,7 +9165,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "frameId": null, "groupIds": [], "height": 10, - "id": "id333", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -9195,7 +9195,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "frameId": null, "groupIds": [], "height": 100, - "id": "id338", + "id": "id5", "index": "a1", "isDeleted": false, "link": null, @@ -9232,7 +9232,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "added": {}, "removed": {}, "updated": { - "id333": { + "id0": { "deleted": { "backgroundColor": "transparent", "version": 7, @@ -9244,7 +9244,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, }, }, - "id": "id341", + "id": "id8", }, ] `; @@ -9256,7 +9256,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "delta": Delta { "deleted": { "selectedElementIds": { - "id333": true, + "id0": true, }, }, "inserted": { @@ -9267,7 +9267,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "elements": { "added": {}, "removed": { - "id333": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9301,7 +9301,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "updated": {}, }, - "id": "id335", + "id": "id2", }, ] `; @@ -9392,7 +9392,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id342": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9430,7 +9430,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "frameId": null, "groupIds": [], "height": 10, - "id": "id342", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -9463,7 +9463,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "delta": Delta { "deleted": { "selectedElementIds": { - "id342": true, + "id0": true, }, }, "inserted": { @@ -9474,7 +9474,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "elements": { "added": {}, "removed": { - "id342": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9508,7 +9508,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "updated": {}, }, - "id": "id344", + "id": "id2", }, { "appState": AppStateDelta { @@ -9521,7 +9521,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "added": {}, "removed": {}, "updated": { - "id342": { + "id0": { "deleted": { "backgroundColor": "#ffc9c9", "version": 7, @@ -9533,7 +9533,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, }, }, - "id": "id348", + "id": "id6", }, ] `; @@ -9666,7 +9666,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id371", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -9699,7 +9699,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id372", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -9731,7 +9731,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id375", + "id": "id4", "index": "a2", "isDeleted": false, "link": null, @@ -9763,7 +9763,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id376", + "id": "id5", "index": "a3", "isDeleted": false, "link": null, @@ -9802,7 +9802,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "added": {}, "removed": {}, "updated": { - "id371": { + "id0": { "deleted": { "groupIds": [ "A", @@ -9815,7 +9815,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "version": 5, }, }, - "id372": { + "id1": { "deleted": { "groupIds": [ "A", @@ -9830,7 +9830,7 @@ exports[`history > multiplayer undo/redo > should override remotely added groups }, }, }, - "id": "id378", + "id": "id7", }, ] `; @@ -9921,7 +9921,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id379": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9962,7 +9962,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "frameId": null, "groupIds": [], "height": 30, - "id": "id379", + "id": "id0", "index": "a0", "isDeleted": false, "lastCommittedPoint": [ @@ -10025,7 +10025,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "delta": Delta { "deleted": { "selectedElementIds": { - "id379": true, + "id0": true, }, }, "inserted": { @@ -10036,7 +10036,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "elements": { "added": {}, "removed": { - "id379": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -10091,7 +10091,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points }, "updated": {}, }, - "id": "id389", + "id": "id10", }, { "appState": AppStateDelta { @@ -10104,7 +10104,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "added": {}, "removed": {}, "updated": { - "id379": { + "id0": { "deleted": { "height": 30, "lastCommittedPoint": [ @@ -10158,13 +10158,13 @@ exports[`history > multiplayer undo/redo > should override remotely added points }, }, }, - "id": "id390", + "id": "id11", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "selectedLinearElementId": "id379", + "selectedLinearElementId": "id0", }, "inserted": { "selectedLinearElementId": null, @@ -10176,7 +10176,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "removed": {}, "updated": {}, }, - "id": "id391", + "id": "id12", }, ] `; @@ -10303,7 +10303,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "frameId": null, "groupIds": [], "height": 10, - "id": "id392", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -10336,7 +10336,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "delta": Delta { "deleted": { "selectedElementIds": { - "id392": true, + "id0": true, }, }, "inserted": { @@ -10347,7 +10347,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "elements": { "added": {}, "removed": { - "id392": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "#ffec99", @@ -10381,7 +10381,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme }, "updated": {}, }, - "id": "id399", + "id": "id7", }, { "appState": AppStateDelta { @@ -10391,7 +10391,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme }, "inserted": { "selectedElementIds": { - "id392": true, + "id0": true, }, }, }, @@ -10401,7 +10401,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "removed": {}, "updated": {}, }, - "id": "id400", + "id": "id8", }, ] `; @@ -10770,7 +10770,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o }, }, }, - "id": "id369", + "id": "id7", }, { "appState": AppStateDelta { @@ -10847,7 +10847,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "removed": {}, "updated": {}, }, - "id": "id370", + "id": "id8", }, ] `; @@ -10940,7 +10940,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id349": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -10978,7 +10978,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "frameId": null, "groupIds": [], "height": 10, - "id": "id349", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -11015,7 +11015,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "added": {}, "removed": {}, "updated": { - "id349": { + "id0": { "deleted": { "backgroundColor": "#d0bfff", "version": 12, @@ -11027,7 +11027,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r }, }, }, - "id": "id360", + "id": "id11", }, { "appState": AppStateDelta { @@ -11040,7 +11040,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "added": {}, "removed": {}, "updated": { - "id349": { + "id0": { "deleted": { "backgroundColor": "transparent", "version": 13, @@ -11052,7 +11052,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r }, }, }, - "id": "id361", + "id": "id12", }, ] `; @@ -11064,7 +11064,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "delta": Delta { "deleted": { "selectedElementIds": { - "id349": true, + "id0": true, }, }, "inserted": { @@ -11075,7 +11075,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "elements": { "added": {}, "removed": { - "id349": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -11109,7 +11109,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r }, "updated": {}, }, - "id": "id351", + "id": "id2", }, ] `; @@ -11266,7 +11266,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "frameId": null, "groupIds": [], "height": 10, - "id": "id329", + "id": "id1", "index": "a1", "isDeleted": true, "link": null, @@ -11300,14 +11300,14 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should }, "inserted": { "selectedElementIds": { - "id329": true, + "id1": true, }, }, }, }, "elements": { "added": { - "id329": { + "id1": { "deleted": { "isDeleted": true, "version": 4, @@ -11342,7 +11342,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "removed": {}, "updated": {}, }, - "id": "id332", + "id": "id4", }, ] `; @@ -11435,7 +11435,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id50": true, + "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11473,7 +11473,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "frameId": null, "groupIds": [], "height": 10, - "id": "id46", + "id": "id0", "index": "a0", "isDeleted": true, "link": null, @@ -11503,7 +11503,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "frameId": null, "groupIds": [], "height": 10, - "id": "id50", + "id": "id4", "index": "a1", "isDeleted": false, "link": null, @@ -11536,7 +11536,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "delta": Delta { "deleted": { "selectedElementIds": { - "id50": true, + "id4": true, }, }, "inserted": { @@ -11547,7 +11547,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "elements": { "added": {}, "removed": { - "id50": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -11581,7 +11581,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme }, "updated": {}, }, - "id": "id52", + "id": "id6", }, ] `; @@ -11708,7 +11708,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "frameId": null, "groupIds": [], "height": 10, - "id": "id148", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -11738,7 +11738,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "frameId": null, "groupIds": [], "height": 10, - "id": "id153", + "id": "id5", "index": "a1", "isDeleted": true, "lastCommittedPoint": [ @@ -11792,7 +11792,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "frameId": null, "groupIds": [], "height": 10, - "id": "id157", + "id": "id9", "index": "a2", "isDeleted": false, "lastCommittedPoint": [ @@ -11849,7 +11849,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "delta": Delta { "deleted": { "selectedElementIds": { - "id148": true, + "id0": true, }, }, "inserted": { @@ -11860,7 +11860,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "elements": { "added": {}, "removed": { - "id148": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -11894,7 +11894,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f }, "updated": {}, }, - "id": "id150", + "id": "id2", }, { "appState": AppStateDelta { @@ -11904,7 +11904,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f }, "inserted": { "selectedElementIds": { - "id148": true, + "id0": true, }, }, }, @@ -11914,7 +11914,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "removed": {}, "updated": {}, }, - "id": "id152", + "id": "id4", }, { "appState": AppStateDelta { @@ -11926,7 +11926,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "elements": { "added": {}, "removed": { - "id157": { + "id9": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -11984,7 +11984,875 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f }, "updated": {}, }, - "id": "id159", + "id": "id11", + }, +] +`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeLockedId": null, + "activeTool": { + "customType": null, + "fromSelection": false, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "sharp", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": "Couldn't load invalid file", + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "lockedMultiSelections": {}, + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": null, + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 315, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": "https://www.youtube.com/watch?v=gkGMXY0wekg", + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "embeddable", + "updated": 1, + "version": 4, + "width": 560, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] redo stack 1`] = `[]`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] undo stack 1`] = ` +[ + { + "appState": AppStateDelta { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elements": { + "added": {}, + "removed": { + "id0": { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 315, + "index": "a0", + "isDeleted": false, + "link": "https://www.youtube.com/watch?v=gkGMXY0wekg", + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "embeddable", + "version": 4, + "width": 560, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + "version": 3, + }, + }, + }, + "updated": {}, + }, + "id": "id4", + }, +] +`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link paste > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeLockedId": null, + "activeTool": { + "customType": null, + "fromSelection": false, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "sharp", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "lockedMultiSelections": {}, + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": null, + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link paste > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 315, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": "https://www.youtube.com/watch?v=gkGMXY0wekg", + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "embeddable", + "updated": 1, + "version": 4, + "width": 560, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link paste > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link paste > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link paste > [end of test] redo stack 1`] = `[]`; + +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link paste > [end of test] undo stack 1`] = ` +[ + { + "appState": AppStateDelta { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elements": { + "added": {}, + "removed": { + "id0": { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 315, + "index": "a0", + "isDeleted": false, + "link": "https://www.youtube.com/watch?v=gkGMXY0wekg", + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "embeddable", + "version": 4, + "width": 560, + "x": 0, + "y": 0, + }, + "inserted": { + "isDeleted": true, + "version": 3, + }, + }, + }, + "updated": {}, + }, + "id": "id4", + }, +] +`; + +exports[`history > singleplayer undo/redo > should create new history entry on image drag&drop > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeLockedId": null, + "activeTool": { + "customType": null, + "fromSelection": false, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "sharp", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 1000, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "lockedMultiSelections": {}, + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": null, + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on image drag&drop > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "crop": null, + "customData": undefined, + "fileId": "fileId", + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 335, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "scale": [ + 1, + 1, + ], + "status": "pending", + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "image", + "updated": 1, + "version": 7, + "width": 318, + "x": -159, + "y": "-167.50000", +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on image drag&drop > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should create new history entry on image drag&drop > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should create new history entry on image drag&drop > [end of test] redo stack 1`] = `[]`; + +exports[`history > singleplayer undo/redo > should create new history entry on image drag&drop > [end of test] undo stack 1`] = ` +[ + { + "appState": AppStateDelta { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elements": { + "added": {}, + "removed": { + "id0": { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "crop": null, + "customData": undefined, + "fileId": "fileId", + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 335, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "scale": [ + 1, + 1, + ], + "status": "pending", + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "image", + "version": 7, + "width": 318, + "x": -159, + "y": "-167.50000", + }, + "inserted": { + "isDeleted": true, + "version": 6, + }, + }, + }, + "updated": {}, + }, + "id": "id4", + }, +] +`; + +exports[`history > singleplayer undo/redo > should create new history entry on image paste > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeLockedId": null, + "activeTool": { + "customType": null, + "fromSelection": false, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "sharp", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 1000, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "lockedMultiSelections": {}, + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": null, + "selectedElementIds": { + "id0": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": true, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on image paste > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "crop": null, + "customData": undefined, + "fileId": "fileId", + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 77, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "scale": [ + 1, + 1, + ], + "status": "pending", + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "image", + "updated": 1, + "version": 7, + "width": 56, + "x": -28, + "y": "-38.50000", +} +`; + +exports[`history > singleplayer undo/redo > should create new history entry on image paste > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should create new history entry on image paste > [end of test] number of renders 1`] = `7`; + +exports[`history > singleplayer undo/redo > should create new history entry on image paste > [end of test] redo stack 1`] = `[]`; + +exports[`history > singleplayer undo/redo > should create new history entry on image paste > [end of test] undo stack 1`] = ` +[ + { + "appState": AppStateDelta { + "delta": Delta { + "deleted": { + "selectedElementIds": { + "id0": true, + }, + }, + "inserted": { + "selectedElementIds": {}, + }, + }, + }, + "elements": { + "added": {}, + "removed": { + "id0": { + "deleted": { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "crop": null, + "customData": undefined, + "fileId": "fileId", + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 77, + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "scale": [ + 1, + 1, + ], + "status": "pending", + "strokeColor": "transparent", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "image", + "version": 7, + "width": 56, + "x": -28, + "y": "-38.50000", + }, + "inserted": { + "isDeleted": true, + "version": 6, + }, + }, + }, + "updated": {}, + }, + "id": "id4", }, ] `; @@ -12229,7 +13097,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on s }, "updated": {}, }, - "id": "id80", + "id": "id5", }, ] `; @@ -12320,7 +13188,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id323": true, + "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12388,7 +13256,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "frameId": null, "groupIds": [], "height": 10, - "id": "id323", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -12421,7 +13289,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "delta": Delta { "deleted": { "selectedElementIds": { - "id323": true, + "id1": true, }, }, "inserted": { @@ -12432,7 +13300,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "elements": { "added": {}, "removed": { - "id323": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -12466,7 +13334,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe }, "updated": {}, }, - "id": "id327", + "id": "id5", }, ] `; @@ -12557,7 +13425,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id70": true, + "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12625,7 +13493,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "frameId": null, "groupIds": [], "height": 10, - "id": "id70", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -12658,7 +13526,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "delta": Delta { "deleted": { "selectedElementIds": { - "id70": true, + "id1": true, }, }, "inserted": { @@ -12669,7 +13537,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "elements": { "added": {}, "removed": { - "id70": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -12703,7 +13571,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry }, "updated": {}, }, - "id": "id74", + "id": "id5", }, ] `; @@ -12789,7 +13657,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id53": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, @@ -12832,7 +13700,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "frameId": null, "groupIds": [], "height": 10, - "id": "id53", + "id": "id0", "index": "a0", "isDeleted": true, "link": null, @@ -12863,7 +13731,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "delta": Delta { "deleted": { "selectedElementIds": { - "id53": true, + "id0": true, }, }, "inserted": { @@ -12876,14 +13744,14 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "removed": {}, "updated": {}, }, - "id": "id66", + "id": "id13", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id53": true, + "id0": true, }, }, "inserted": { @@ -12896,7 +13764,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "removed": {}, "updated": {}, }, - "id": "id67", + "id": "id14", }, { "appState": AppStateDelta { @@ -12906,14 +13774,14 @@ exports[`history > singleplayer undo/redo > should iterate through the history w }, "inserted": { "selectedElementIds": { - "id53": true, + "id0": true, }, }, }, }, "elements": { "added": { - "id53": { + "id0": { "deleted": { "isDeleted": true, "version": 4, @@ -12948,7 +13816,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "removed": {}, "updated": {}, }, - "id": "id68", + "id": "id15", }, ] `; @@ -13041,7 +13909,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id18": true, + "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -13079,7 +13947,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "frameId": null, "groupIds": [], "height": 10, - "id": "id15", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -13109,7 +13977,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "frameId": null, "groupIds": [], "height": 10, - "id": "id18", + "id": "id3", "index": "a1", "isDeleted": false, "link": null, @@ -13142,7 +14010,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "delta": Delta { "deleted": { "selectedElementIds": { - "id15": true, + "id0": true, }, }, "inserted": { @@ -13153,7 +14021,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "elements": { "added": {}, "removed": { - "id15": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13187,7 +14055,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "updated": {}, }, - "id": "id17", + "id": "id2", }, { "appState": AppStateDelta { @@ -13197,7 +14065,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "inserted": { "selectedElementIds": { - "id15": true, + "id0": true, }, }, }, @@ -13207,14 +14075,14 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "removed": {}, "updated": {}, }, - "id": "id24", + "id": "id9", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id15": true, + "id0": true, }, }, "inserted": { @@ -13227,19 +14095,19 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "removed": {}, "updated": {}, }, - "id": "id27", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id18": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id15": true, + "id0": true, }, }, }, @@ -13247,7 +14115,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "elements": { "added": {}, "removed": { - "id18": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13281,7 +14149,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "updated": {}, }, - "id": "id28", + "id": "id13", }, ] `; @@ -13542,8 +14410,8 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id4": true, - "id5": true, + "id0": true, + "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": { @@ -13585,7 +14453,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "A", ], "height": 100, - "id": "id4", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -13617,7 +14485,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "A", ], "height": 100, - "id": "id5", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -13650,8 +14518,8 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "delta": Delta { "deleted": { "selectedElementIds": { - "id4": true, - "id5": true, + "id0": true, + "id1": true, }, "selectedGroupIds": { "A": true, @@ -13666,7 +14534,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "elements": { "added": {}, "removed": { - "id4": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13699,7 +14567,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "version": 1, }, }, - "id5": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13735,7 +14603,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "updated": {}, }, - "id": "id8", + "id": "id4", }, ] `; @@ -13865,7 +14733,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "frameId": null, "groupIds": [], "height": 100, - "id": "id10", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -13895,7 +14763,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "frameId": null, "groupIds": [], "height": 100, - "id": "id11", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -13933,7 +14801,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "elements": { "added": {}, "removed": { - "id10": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13964,7 +14832,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "version": 1, }, }, - "id11": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13998,7 +14866,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "updated": {}, }, - "id": "id13", + "id": "id3", }, ] `; @@ -14084,14 +14952,14 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id29": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id29": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -14129,7 +14997,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "frameId": null, "groupIds": [], "height": 10, - "id": "id29", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -14166,7 +15034,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "added": {}, "removed": {}, "updated": { - "id29": { + "id0": { "deleted": { "backgroundColor": "#ffc9c9", "version": 10, @@ -14178,7 +15046,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes }, }, }, - "id": "id43", + "id": "id14", }, { "appState": AppStateDelta { @@ -14191,7 +15059,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "added": {}, "removed": {}, "updated": { - "id29": { + "id0": { "deleted": { "backgroundColor": "transparent", "version": 11, @@ -14203,14 +15071,14 @@ exports[`history > singleplayer undo/redo > should not override appstate changes }, }, }, - "id": "id44", + "id": "id15", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id29": true, + "id0": true, }, }, "inserted": { @@ -14223,7 +15091,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "removed": {}, "updated": {}, }, - "id": "id45", + "id": "id16", }, ] `; @@ -14235,7 +15103,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "delta": Delta { "deleted": { "selectedElementIds": { - "id29": true, + "id0": true, }, }, "inserted": { @@ -14246,7 +15114,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "elements": { "added": {}, "removed": { - "id29": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14280,7 +15148,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes }, "updated": {}, }, - "id": "id31", + "id": "id2", }, ] `; @@ -14424,7 +15292,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "removed": {}, "updated": {}, }, - "id": "id88", + "id": "id7", }, { "appState": AppStateDelta { @@ -14442,7 +15310,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "removed": {}, "updated": {}, }, - "id": "id89", + "id": "id8", }, ] `; @@ -14528,14 +15396,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id230": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id243": true, + "id13": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -14569,11 +15437,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id231", + "id": "id1", "type": "text", }, { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -14582,7 +15450,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id230", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -14608,7 +15476,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id230", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -14616,7 +15484,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 25, - "id": "id231", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -14647,7 +15515,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -14656,7 +15524,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id232", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -14685,7 +15553,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id232", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -14693,7 +15561,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 0, - "id": "id243", + "id": "id13", "index": "a3", "isDeleted": false, "lastCommittedPoint": null, @@ -14716,7 +15584,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id230", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -14743,9 +15611,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "delta": Delta { "deleted": { "selectedElementIds": { - "id243": true, + "id13": true, }, - "selectedLinearElementId": "id243", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": {}, @@ -14756,7 +15624,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id243": { + "id13": { "deleted": { "isDeleted": false, "version": 10, @@ -14768,11 +15636,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id230": { + "id0": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -14783,7 +15651,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 5, }, }, - "id231": { + "id1": { "deleted": { "version": 6, }, @@ -14791,11 +15659,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 4, }, }, - "id232": { + "id2": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -14808,7 +15676,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id248", + "id": "id18", }, ] `; @@ -14825,7 +15693,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id230": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14856,7 +15724,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id231": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -14896,7 +15764,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id232": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14930,14 +15798,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "updated": {}, }, - "id": "id234", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id230": true, + "id0": true, }, }, "inserted": { @@ -14950,14 +15818,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id237", + "id": "id7", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id231": true, + "id1": true, }, }, "inserted": { @@ -14970,7 +15838,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id240", + "id": "id10", }, { "appState": AppStateDelta { @@ -14980,7 +15848,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id231": true, + "id1": true, }, }, }, @@ -14989,11 +15857,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id230": { + "id0": { "deleted": { "boundElements": [ { - "id": "id231", + "id": "id1", "type": "text", }, ], @@ -15004,9 +15872,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 2, }, }, - "id231": { + "id1": { "deleted": { - "containerId": "id230", + "containerId": "id0", "height": 25, "textAlign": "center", "version": 4, @@ -15028,20 +15896,20 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id242", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id243": true, + "id13": true, }, - "selectedLinearElementId": "id243", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": { - "id230": true, + "id0": true, }, "selectedLinearElementId": null, }, @@ -15050,7 +15918,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id243": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15059,7 +15927,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id232", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -15089,7 +15957,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id230", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -15109,11 +15977,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id230": { + "id0": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -15124,11 +15992,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 3, }, }, - "id232": { + "id2": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -15141,7 +16009,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id245", + "id": "id15", }, ] `; @@ -15227,14 +16095,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id212": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id225": true, + "id13": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -15268,11 +16136,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id213", + "id": "id1", "type": "text", }, { - "id": "id225", + "id": "id13", "type": "arrow", }, ], @@ -15281,7 +16149,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id212", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -15307,7 +16175,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id212", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -15315,7 +16183,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 25, - "id": "id213", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -15346,7 +16214,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id225", + "id": "id13", "type": "arrow", }, ], @@ -15355,7 +16223,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id214", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -15384,7 +16252,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id214", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -15392,7 +16260,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 0, - "id": "id225", + "id": "id13", "index": "a3", "isDeleted": false, "lastCommittedPoint": null, @@ -15415,7 +16283,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id212", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -15449,7 +16317,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id212": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15480,7 +16348,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id213": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -15520,7 +16388,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id214": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15554,14 +16422,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "updated": {}, }, - "id": "id216", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id212": true, + "id0": true, }, }, "inserted": { @@ -15574,14 +16442,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id219", + "id": "id7", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id213": true, + "id1": true, }, }, "inserted": { @@ -15594,7 +16462,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id222", + "id": "id10", }, { "appState": AppStateDelta { @@ -15604,7 +16472,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id213": true, + "id1": true, }, }, }, @@ -15613,11 +16481,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id212": { + "id0": { "deleted": { "boundElements": [ { - "id": "id213", + "id": "id1", "type": "text", }, ], @@ -15628,9 +16496,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 2, }, }, - "id213": { + "id1": { "deleted": { - "containerId": "id212", + "containerId": "id0", "height": 25, "textAlign": "center", "version": 4, @@ -15652,20 +16520,20 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id224", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id225": true, + "id13": true, }, - "selectedLinearElementId": "id225", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": { - "id212": true, + "id0": true, }, "selectedLinearElementId": null, }, @@ -15674,7 +16542,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id225": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15683,7 +16551,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id214", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -15713,7 +16581,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id212", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -15733,11 +16601,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id212": { + "id0": { "deleted": { "boundElements": [ { - "id": "id225", + "id": "id13", "type": "arrow", }, ], @@ -15748,7 +16616,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 5, }, }, - "id213": { + "id1": { "deleted": { "version": 8, }, @@ -15756,11 +16624,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 6, }, }, - "id214": { + "id2": { "deleted": { "boundElements": [ { - "id": "id225", + "id": "id13", "type": "arrow", }, ], @@ -15773,7 +16641,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id229", + "id": "id17", }, ] `; @@ -15859,14 +16727,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id249": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id262": true, + "id13": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -15900,11 +16768,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id250", + "id": "id1", "type": "text", }, { - "id": "id262", + "id": "id13", "type": "arrow", }, ], @@ -15913,7 +16781,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id249", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -15939,7 +16807,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id249", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -15947,7 +16815,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 25, - "id": "id250", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -15978,7 +16846,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id262", + "id": "id13", "type": "arrow", }, ], @@ -15987,7 +16855,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id251", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -16016,7 +16884,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id251", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16024,7 +16892,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 0, - "id": "id262", + "id": "id13", "index": "a3", "isDeleted": false, "lastCommittedPoint": null, @@ -16047,7 +16915,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id249", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16081,7 +16949,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id249": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16112,7 +16980,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 7, }, }, - "id250": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -16152,7 +17020,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 8, }, }, - "id251": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16186,14 +17054,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "updated": {}, }, - "id": "id270", + "id": "id21", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id249": true, + "id0": true, }, }, "inserted": { @@ -16206,14 +17074,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id271", + "id": "id22", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id250": true, + "id1": true, }, }, "inserted": { @@ -16226,7 +17094,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id272", + "id": "id23", }, { "appState": AppStateDelta { @@ -16236,7 +17104,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id250": true, + "id1": true, }, }, }, @@ -16245,11 +17113,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id249": { + "id0": { "deleted": { "boundElements": [ { - "id": "id250", + "id": "id1", "type": "text", }, ], @@ -16260,9 +17128,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 8, }, }, - "id250": { + "id1": { "deleted": { - "containerId": "id249", + "containerId": "id0", "height": 25, "textAlign": "center", "version": 10, @@ -16284,20 +17152,20 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id273", + "id": "id24", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id262": true, + "id13": true, }, - "selectedLinearElementId": "id262", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": { - "id249": true, + "id0": true, }, "selectedLinearElementId": null, }, @@ -16306,7 +17174,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id262": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16315,7 +17183,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id251", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16345,7 +17213,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id249", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16365,11 +17233,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id249": { + "id0": { "deleted": { "boundElements": [ { - "id": "id262", + "id": "id13", "type": "arrow", }, ], @@ -16380,7 +17248,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 9, }, }, - "id250": { + "id1": { "deleted": { "version": 12, }, @@ -16388,11 +17256,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 10, }, }, - "id251": { + "id2": { "deleted": { "boundElements": [ { - "id": "id262", + "id": "id13", "type": "arrow", }, ], @@ -16405,7 +17273,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id274", + "id": "id25", }, ] `; @@ -16496,7 +17364,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id275": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -16530,11 +17398,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, { - "id": "id276", + "id": "id1", "type": "text", }, ], @@ -16543,7 +17411,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id275", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -16569,7 +17437,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id275", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -16577,7 +17445,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 25, - "id": "id276", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -16608,7 +17476,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, ], @@ -16617,7 +17485,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id277", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -16646,7 +17514,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id277", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16654,7 +17522,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 0, - "id": "id288", + "id": "id13", "index": "a3", "isDeleted": false, "lastCommittedPoint": null, @@ -16677,7 +17545,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id275", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16704,7 +17572,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "delta": Delta { "deleted": { "selectedElementIds": { - "id275": true, + "id0": true, }, }, "inserted": { @@ -16715,7 +17583,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id275": { + "id0": { "deleted": { "isDeleted": false, "version": 8, @@ -16725,7 +17593,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 5, }, }, - "id276": { + "id1": { "deleted": { "isDeleted": false, "version": 8, @@ -16737,18 +17605,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id277": { - "deleted": { - "version": 5, - }, - "inserted": { - "version": 3, - }, - }, - "id288": { + "id13": { "deleted": { "startBinding": { - "elementId": "id275", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16759,9 +17619,17 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 7, }, }, + "id2": { + "deleted": { + "version": 5, + }, + "inserted": { + "version": 3, + }, + }, }, }, - "id": "id296", + "id": "id21", }, ] `; @@ -16778,7 +17646,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id275": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16809,7 +17677,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id276": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -16849,7 +17717,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id277": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16883,14 +17751,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "updated": {}, }, - "id": "id279", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id275": true, + "id0": true, }, }, "inserted": { @@ -16903,14 +17771,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id282", + "id": "id7", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id276": true, + "id1": true, }, }, "inserted": { @@ -16923,7 +17791,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id285", + "id": "id10", }, { "appState": AppStateDelta { @@ -16933,7 +17801,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id276": true, + "id1": true, }, }, }, @@ -16942,11 +17810,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id275": { + "id0": { "deleted": { "boundElements": [ { - "id": "id276", + "id": "id1", "type": "text", }, ], @@ -16957,9 +17825,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 2, }, }, - "id276": { + "id1": { "deleted": { - "containerId": "id275", + "containerId": "id0", "height": 25, "textAlign": "center", "version": 4, @@ -16981,20 +17849,20 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id287", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id288": true, + "id13": true, }, - "selectedLinearElementId": "id288", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": { - "id275": true, + "id0": true, }, "selectedLinearElementId": null, }, @@ -17003,7 +17871,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id288": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17012,7 +17880,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id277", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -17042,7 +17910,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id275", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -17062,11 +17930,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id275": { + "id0": { "deleted": { "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, ], @@ -17077,11 +17945,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 3, }, }, - "id277": { + "id2": { "deleted": { "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, ], @@ -17094,22 +17962,22 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id290", + "id": "id15", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id275": true, + "id0": true, }, "selectedLinearElementId": null, }, "inserted": { "selectedElementIds": { - "id288": true, + "id13": true, }, - "selectedLinearElementId": "id288", + "selectedLinearElementId": "id13", }, }, }, @@ -17118,7 +17986,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id293", + "id": "id18", }, ] `; @@ -17204,15 +18072,15 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id297": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id297": true, - "id299": true, + "id0": true, + "id2": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -17246,11 +18114,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, { - "id": "id298", + "id": "id1", "type": "text", }, ], @@ -17259,7 +18127,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id297", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -17285,7 +18153,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "autoResize": true, "backgroundColor": "transparent", "boundElements": null, - "containerId": "id297", + "containerId": "id0", "customData": undefined, "fillStyle": "solid", "fontFamily": 5, @@ -17293,7 +18161,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 25, - "id": "id298", + "id": "id1", "index": "a1", "isDeleted": false, "lineHeight": "1.25000", @@ -17324,7 +18192,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, ], @@ -17333,7 +18201,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id299", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, @@ -17362,7 +18230,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id299", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -17370,7 +18238,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 0, - "id": "id310", + "id": "id13", "index": "a3", "isDeleted": false, "lastCommittedPoint": null, @@ -17393,7 +18261,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id297", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -17420,8 +18288,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "delta": Delta { "deleted": { "selectedElementIds": { - "id297": true, - "id299": true, + "id0": true, + "id2": true, }, }, "inserted": { @@ -17432,7 +18300,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id297": { + "id0": { "deleted": { "isDeleted": false, "version": 8, @@ -17442,7 +18310,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 5, }, }, - "id298": { + "id1": { "deleted": { "isDeleted": false, "version": 8, @@ -17452,7 +18320,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 5, }, }, - "id299": { + "id2": { "deleted": { "isDeleted": false, "version": 5, @@ -17464,15 +18332,15 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id310": { + "id13": { "deleted": { "endBinding": { - "elementId": "id299", + "elementId": "id2", "focus": -0, "gap": 1, }, "startBinding": { - "elementId": "id297", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -17486,7 +18354,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id321", + "id": "id24", }, ] `; @@ -17503,7 +18371,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id297": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17534,7 +18402,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id298": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -17574,7 +18442,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 1, }, }, - "id299": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17608,14 +18476,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "updated": {}, }, - "id": "id301", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id297": true, + "id0": true, }, }, "inserted": { @@ -17628,14 +18496,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id304", + "id": "id7", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id298": true, + "id1": true, }, }, "inserted": { @@ -17648,7 +18516,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id307", + "id": "id10", }, { "appState": AppStateDelta { @@ -17658,7 +18526,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id298": true, + "id1": true, }, }, }, @@ -17667,11 +18535,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id297": { + "id0": { "deleted": { "boundElements": [ { - "id": "id298", + "id": "id1", "type": "text", }, ], @@ -17682,9 +18550,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 2, }, }, - "id298": { + "id1": { "deleted": { - "containerId": "id297", + "containerId": "id0", "height": 25, "textAlign": "center", "version": 4, @@ -17706,20 +18574,20 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id309", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id310": true, + "id13": true, }, - "selectedLinearElementId": "id310", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": { - "id297": true, + "id0": true, }, "selectedLinearElementId": null, }, @@ -17728,7 +18596,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id310": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17737,7 +18605,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id299", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -17767,7 +18635,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id297", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -17787,11 +18655,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, "updated": { - "id297": { + "id0": { "deleted": { "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, ], @@ -17802,11 +18670,11 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "version": 3, }, }, - "id299": { + "id2": { "deleted": { "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, ], @@ -17819,22 +18687,22 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, }, }, - "id": "id312", + "id": "id15", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id297": true, + "id0": true, }, "selectedLinearElementId": null, }, "inserted": { "selectedElementIds": { - "id310": true, + "id13": true, }, - "selectedLinearElementId": "id310", + "selectedLinearElementId": "id13", }, }, }, @@ -17843,14 +18711,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id315", + "id": "id18", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id299": true, + "id2": true, }, }, "inserted": { @@ -17863,7 +18731,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id318", + "id": "id21", }, ] `; @@ -17949,15 +18817,15 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id189": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id189": true, - "id195": true, + "id0": true, + "id6": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -17995,7 +18863,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "frameId": null, "groupIds": [], "height": 10, - "id": "id192", + "id": "id3", "index": "a1", "isDeleted": false, "link": null, @@ -18025,7 +18893,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "frameId": null, "groupIds": [], "height": 10, - "id": "id189", + "id": "id0", "index": "a2", "isDeleted": false, "link": null, @@ -18055,7 +18923,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "frameId": null, "groupIds": [], "height": 10, - "id": "id195", + "id": "id6", "index": "a3", "isDeleted": false, "link": null, @@ -18088,7 +18956,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "delta": Delta { "deleted": { "selectedElementIds": { - "id189": true, + "id0": true, }, }, "inserted": { @@ -18099,7 +18967,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "elements": { "added": {}, "removed": { - "id189": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18133,19 +19001,19 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, "updated": {}, }, - "id": "id191", + "id": "id2", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id192": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id189": true, + "id0": true, }, }, }, @@ -18153,7 +19021,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "elements": { "added": {}, "removed": { - "id192": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18187,19 +19055,19 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, "updated": {}, }, - "id": "id194", + "id": "id5", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id195": true, + "id6": true, }, }, "inserted": { "selectedElementIds": { - "id192": true, + "id3": true, }, }, }, @@ -18207,7 +19075,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "elements": { "added": {}, "removed": { - "id195": { + "id6": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18241,7 +19109,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, "updated": {}, }, - "id": "id197", + "id": "id8", }, { "appState": AppStateDelta { @@ -18254,7 +19122,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "added": {}, "removed": {}, "updated": { - "id195": { + "id6": { "deleted": { "index": "a0V", "version": 6, @@ -18266,19 +19134,19 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, }, }, - "id": "id201", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id189": true, + "id0": true, }, }, "inserted": { "selectedElementIds": { - "id195": true, + "id6": true, }, }, }, @@ -18288,14 +19156,14 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "removed": {}, "updated": {}, }, - "id": "id204", + "id": "id15", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id195": true, + "id6": true, }, }, "inserted": { @@ -18308,7 +19176,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "removed": {}, "updated": {}, }, - "id": "id207", + "id": "id18", }, { "appState": AppStateDelta { @@ -18321,7 +19189,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "added": {}, "removed": {}, "updated": { - "id189": { + "id0": { "deleted": { "index": "a2", "version": 7, @@ -18331,7 +19199,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "version": 6, }, }, - "id195": { + "id6": { "deleted": { "index": "a3", "version": 10, @@ -18343,7 +19211,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, }, }, - "id": "id211", + "id": "id22", }, ] `; @@ -18429,19 +19297,19 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id161": true, + "id1": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id184": true, - "id186": true, + "id24": true, + "id26": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": { - "id185": true, + "id25": true, }, "selectionElement": null, "shouldCacheIgnoreZoom": false, @@ -18479,7 +19347,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "A", ], "height": 100, - "id": "id160", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -18511,7 +19379,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "A", ], "height": 100, - "id": "id161", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, @@ -18540,10 +19408,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id185", + "id25", ], "height": 100, - "id": "id184", + "id": "id24", "index": "a1G", "isDeleted": false, "link": null, @@ -18572,10 +19440,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id185", + "id25", ], "height": 100, - "id": "id186", + "id": "id26", "index": "a1V", "isDeleted": false, "link": null, @@ -18604,10 +19472,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id177", + "id17", ], "height": 100, - "id": "id176", + "id": "id16", "index": "a2", "isDeleted": true, "link": null, @@ -18636,10 +19504,10 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id177", + "id17", ], "height": 100, - "id": "id178", + "id": "id18", "index": "a3", "isDeleted": true, "link": null, @@ -18672,8 +19540,8 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "delta": Delta { "deleted": { "selectedElementIds": { - "id160": true, - "id161": true, + "id0": true, + "id1": true, }, "selectedGroupIds": { "A": true, @@ -18688,7 +19556,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "elements": { "added": {}, "removed": { - "id160": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18721,7 +19589,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "version": 1, }, }, - "id161": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18757,24 +19625,24 @@ exports[`history > singleplayer undo/redo > should support duplication of groups }, "updated": {}, }, - "id": "id164", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id184": true, - "id186": true, + "id24": true, + "id26": true, }, "selectedGroupIds": { - "id185": true, + "id25": true, }, }, "inserted": { "selectedElementIds": { - "id160": true, - "id161": true, + "id0": true, + "id1": true, }, "selectedGroupIds": { "A": true, @@ -18785,7 +19653,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "elements": { "added": {}, "removed": { - "id184": { + "id24": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18794,7 +19662,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id185", + "id25", ], "height": 100, "index": "a1G", @@ -18818,7 +19686,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "version": 3, }, }, - "id186": { + "id26": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18827,7 +19695,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id185", + "id25", ], "height": 100, "index": "a1V", @@ -18854,7 +19722,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups }, "updated": {}, }, - "id": "id188", + "id": "id28", }, ] `; @@ -18940,7 +19808,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id93": true, + "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -18983,7 +19851,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "frameId": null, "groupIds": [], "height": 10, - "id": "id90", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, @@ -19013,7 +19881,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "frameId": null, "groupIds": [], "height": 10, - "id": "id93", + "id": "id3", "index": "a1", "isDeleted": true, "link": null, @@ -19043,7 +19911,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "frameId": null, "groupIds": [], "height": 10, - "id": "id96", + "id": "id6", "index": "a2", "isDeleted": true, "link": null, @@ -19076,7 +19944,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "delta": Delta { "deleted": { "selectedElementIds": { - "id90": true, + "id0": true, }, }, "inserted": { @@ -19087,7 +19955,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "elements": { "added": {}, "removed": { - "id90": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19121,19 +19989,19 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "updated": {}, }, - "id": "id113", + "id": "id23", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id93": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id90": true, + "id0": true, }, }, }, @@ -19141,7 +20009,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "elements": { "added": {}, "removed": { - "id93": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19175,19 +20043,19 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "updated": {}, }, - "id": "id114", + "id": "id24", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id96": true, + "id6": true, }, }, "inserted": { "selectedElementIds": { - "id93": true, + "id3": true, }, }, }, @@ -19195,7 +20063,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "elements": { "added": {}, "removed": { - "id96": { + "id6": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19229,19 +20097,19 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "updated": {}, }, - "id": "id115", + "id": "id25", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id93": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id96": true, + "id6": true, }, }, }, @@ -19251,14 +20119,14 @@ exports[`history > singleplayer undo/redo > should support element creation, del "removed": {}, "updated": {}, }, - "id": "id116", + "id": "id26", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id96": true, + "id6": true, }, }, "inserted": { @@ -19271,7 +20139,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "removed": {}, "updated": {}, }, - "id": "id117", + "id": "id27", }, { "appState": AppStateDelta { @@ -19281,15 +20149,15 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "inserted": { "selectedElementIds": { - "id93": true, - "id96": true, + "id3": true, + "id6": true, }, }, }, }, "elements": { "added": { - "id93": { + "id3": { "deleted": { "isDeleted": true, "version": 8, @@ -19299,7 +20167,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "version": 7, }, }, - "id96": { + "id6": { "deleted": { "isDeleted": true, "version": 8, @@ -19313,7 +20181,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "removed": {}, "updated": {}, }, - "id": "id118", + "id": "id28", }, ] `; @@ -19399,14 +20267,14 @@ exports[`history > singleplayer undo/redo > should support linear element creati "penMode": false, "pendingImageElementId": null, "previousSelectedElementIds": { - "id119": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id119": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -19447,7 +20315,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "frameId": null, "groupIds": [], "height": 20, - "id": "id119", + "id": "id0", "index": "a0", "isDeleted": false, "lastCommittedPoint": [ @@ -19502,7 +20370,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "delta": Delta { "deleted": { "selectedElementIds": { - "id119": true, + "id0": true, }, }, "inserted": { @@ -19513,7 +20381,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "elements": { "added": {}, "removed": { - "id119": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19568,7 +20436,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati }, "updated": {}, }, - "id": "id142", + "id": "id23", }, { "appState": AppStateDelta { @@ -19581,7 +20449,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "added": {}, "removed": {}, "updated": { - "id119": { + "id0": { "deleted": { "lastCommittedPoint": [ 20, @@ -19625,13 +20493,13 @@ exports[`history > singleplayer undo/redo > should support linear element creati }, }, }, - "id": "id143", + "id": "id24", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "selectedLinearElementId": "id119", + "selectedLinearElementId": "id0", }, "inserted": { "selectedLinearElementId": null, @@ -19643,13 +20511,13 @@ exports[`history > singleplayer undo/redo > should support linear element creati "removed": {}, "updated": {}, }, - "id": "id144", + "id": "id25", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "editingLinearElementId": "id119", + "editingLinearElementId": "id0", }, "inserted": { "editingLinearElementId": null, @@ -19661,7 +20529,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "removed": {}, "updated": {}, }, - "id": "id145", + "id": "id26", }, { "appState": AppStateDelta { @@ -19674,7 +20542,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "added": {}, "removed": {}, "updated": { - "id119": { + "id0": { "deleted": { "height": 20, "points": [ @@ -19714,7 +20582,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati }, }, }, - "id": "id146", + "id": "id27", }, { "appState": AppStateDelta { @@ -19723,7 +20591,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "editingLinearElementId": null, }, "inserted": { - "editingLinearElementId": "id119", + "editingLinearElementId": "id0", }, }, }, @@ -19732,7 +20600,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "removed": {}, "updated": {}, }, - "id": "id147", + "id": "id28", }, ] `; diff --git a/packages/excalidraw/tests/helpers/api.ts b/packages/excalidraw/tests/helpers/api.ts index 9c1ac99139..f378079534 100644 --- a/packages/excalidraw/tests/helpers/api.ts +++ b/packages/excalidraw/tests/helpers/api.ts @@ -499,13 +499,21 @@ export class API { value: { files, getData: (type: string) => { - if (type === blob.type) { + if (type === blob.type || type === "text") { return text; } return ""; }, + types: [blob.type], }, }); + Object.defineProperty(fileDropEvent, "clientX", { + value: 0, + }); + Object.defineProperty(fileDropEvent, "clientY", { + value: 0, + }); + await fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent); }; diff --git a/packages/excalidraw/tests/helpers/mocks.ts b/packages/excalidraw/tests/helpers/mocks.ts index 10e1dee2b4..e7fa629111 100644 --- a/packages/excalidraw/tests/helpers/mocks.ts +++ b/packages/excalidraw/tests/helpers/mocks.ts @@ -31,3 +31,30 @@ export const mockMermaidToExcalidraw = (opts: { }); } }; + +// Mock for HTMLImageElement (use with `vi.unstubAllGlobals()`) +// as jsdom.resources: "usable" throws an error on image load +export const mockHTMLImageElement = ( + naturalWidth: number, + naturalHeight: number, +) => { + vi.stubGlobal( + "Image", + class extends Image { + constructor() { + super(); + + Object.defineProperty(this, "naturalWidth", { + value: naturalWidth, + }); + Object.defineProperty(this, "naturalHeight", { + value: naturalHeight, + }); + + queueMicrotask(() => { + this.onload?.({} as Event); + }); + } + }, + ); +}; diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 328dab7c4f..c2da254543 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -19,6 +19,7 @@ import { COLOR_PALETTE, DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX, DEFAULT_ELEMENT_STROKE_COLOR_INDEX, + reseed, } from "@excalidraw/common"; import "@excalidraw/utils/test-utils"; @@ -35,6 +36,7 @@ import type { ExcalidrawGenericElement, ExcalidrawLinearElement, ExcalidrawTextElement, + FileId, FixedPointBinding, FractionalIndex, SceneElementsMap, @@ -49,12 +51,16 @@ import { } from "../actions"; import { createUndoAction, createRedoAction } from "../actions/actionHistory"; import { actionToggleViewMode } from "../actions/actionToggleViewMode"; +import * as StaticScene from "../renderer/staticScene"; import { getDefaultAppState } from "../appState"; import { Excalidraw } from "../index"; -import * as StaticScene from "../renderer/staticScene"; +import { createPasteEvent } from "../clipboard"; + +import * as blobModule from "../data/blob"; import { API } from "./helpers/api"; import { Keyboard, Pointer, UI } from "./helpers/ui"; +import { mockHTMLImageElement } from "./helpers/mocks"; import { GlobalTestState, act, @@ -63,6 +69,7 @@ import { togglePopover, getCloneByOrigId, checkpointHistory, + unmountComponent, } from "./test-utils"; import type { AppState } from "../types"; @@ -106,7 +113,22 @@ const violet = COLOR_PALETTE.violet[DEFAULT_ELEMENT_BACKGROUND_COLOR_INDEX]; describe("history", () => { beforeEach(() => { + unmountComponent(); renderStaticScene.mockClear(); + vi.clearAllMocks(); + vi.unstubAllGlobals(); + + reseed(7); + + const generateIdSpy = vi.spyOn(blobModule, "generateIdFromFile"); + const resizeFileSpy = vi.spyOn(blobModule, "resizeImageFile"); + + generateIdSpy.mockImplementation(() => Promise.resolve("fileId" as FileId)); + resizeFileSpy.mockImplementation((file: File) => Promise.resolve(file)); + + Object.assign(document, { + elementFromPoint: () => GlobalTestState.canvas, + }); }); afterEach(() => { @@ -559,6 +581,227 @@ describe("history", () => { ]); }); + it("should create new history entry on image drag&drop", async () => { + await render(); + + // it's necessary to specify the height in order to calculate natural dimensions of the image + h.state.height = 1000; + + const deerImageDimensions = { + width: 318, + height: 335, + }; + + mockHTMLImageElement( + deerImageDimensions.width, + deerImageDimensions.height, + ); + + await API.drop(await API.loadFile("./fixtures/deer.png")); + + await waitFor(() => { + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + ...deerImageDimensions, + }), + ]); + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + isDeleted: true, + ...deerImageDimensions, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + isDeleted: false, + ...deerImageDimensions, + }), + ]); + }); + + it("should create new history entry on embeddable link drag&drop", async () => { + await render(); + + const link = "https://www.youtube.com/watch?v=gkGMXY0wekg"; + await API.drop( + new Blob([link], { + type: MIME_TYPES.text, + }), + ); + + await waitFor(() => { + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "embeddable", + link, + }), + ]); + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "embeddable", + link, + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "embeddable", + link, + isDeleted: false, + }), + ]); + }); + + it("should create new history entry on image paste", async () => { + await render( + , + ); + + // it's necessary to specify the height in order to calculate natural dimensions of the image + h.state.height = 1000; + + const smileyImageDimensions = { + width: 56, + height: 77, + }; + + mockHTMLImageElement( + smileyImageDimensions.width, + smileyImageDimensions.height, + ); + + document.dispatchEvent( + createPasteEvent({ + files: [await API.loadFile("./fixtures/smiley_embedded_v2.png")], + }), + ); + + await waitFor(() => { + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + ...smileyImageDimensions, + }), + ]); + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + isDeleted: true, + ...smileyImageDimensions, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + isDeleted: false, + ...smileyImageDimensions, + }), + ]); + }); + + it("should create new history entry on embeddable link paste", async () => { + await render( + , + ); + + const link = "https://www.youtube.com/watch?v=gkGMXY0wekg"; + + document.dispatchEvent( + createPasteEvent({ + types: { + "text/plain": link, + }, + }), + ); + + await waitFor(() => { + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "embeddable", + link, + }), + ]); + }); + + Keyboard.undo(); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(1); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "embeddable", + link, + isDeleted: true, + }), + ]); + + Keyboard.redo(); + expect(API.getUndoStack().length).toBe(1); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ + type: "embeddable", + link, + isDeleted: false, + }), + ]); + }); + it("should support appstate name or viewBackgroundColor change", async () => { await render( Date: Tue, 10 Jun 2025 21:31:11 +0200 Subject: [PATCH 08/22] fix: remove image preview on image insertion (#9626) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- .../excalidraw/api/props/excalidraw-api.mdx | 10 +- packages/element/src/delta.ts | 14 +- .../excalidraw/actions/actionFinalize.tsx | 13 -- packages/excalidraw/appState.ts | 2 - packages/excalidraw/components/Actions.tsx | 1 - packages/excalidraw/components/App.tsx | 180 +++--------------- .../CommandPalette/CommandPalette.tsx | 1 - packages/excalidraw/components/HintViewer.tsx | 4 - .../components/canvases/InteractiveCanvas.tsx | 1 - .../components/canvases/StaticCanvas.tsx | 1 - packages/excalidraw/locales/en.json | 1 - packages/excalidraw/scene/Renderer.ts | 15 -- .../__snapshots__/contextmenu.test.tsx.snap | 17 -- .../tests/__snapshots__/history.test.tsx.snap | 62 ------ .../regressionTests.test.tsx.snap | 52 ----- packages/excalidraw/types.ts | 4 - .../tests/__snapshots__/export.test.ts.snap | 1 - 17 files changed, 35 insertions(+), 344 deletions(-) diff --git a/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx b/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx index b7a3bab5f4..c9580b66b6 100644 --- a/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx +++ b/dev-docs/docs/@excalidraw/excalidraw/api/props/excalidraw-api.mdx @@ -363,13 +363,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti ```ts ( tool: ( - | ( - | { type: Exclude } - | { - type: Extract; - insertOnCanvasDirectly?: boolean; - } - ) + | { type: ToolType } | { type: "custom"; customType: string } ) & { locked?: boolean }, ) => {}; @@ -377,7 +371,7 @@ This API has the below signature. It sets the `tool` passed in param as the acti | Name | Type | Default | Description | | --- | --- | --- | --- | -| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool. When setting `image` as active tool, the insertion onto canvas when using image tool is disabled by default, so you can enable it by setting `insertOnCanvasDirectly` to `true` | +| `type` | [ToolType](https://github.com/excalidraw/excalidraw/blob/master/packages/excalidraw/types.ts#L91) | `selection` | The tool type which should be set as active tool | | `locked` | `boolean` | `false` | Indicates whether the the active tool should be locked. It behaves the same way when using the `lock` tool in the editor interface | ## setCursor diff --git a/packages/element/src/delta.ts b/packages/element/src/delta.ts index 17d9f49ee5..9504237b51 100644 --- a/packages/element/src/delta.ts +++ b/packages/element/src/delta.ts @@ -694,14 +694,20 @@ export class AppStateDelta implements DeltaContainer { break; case "croppingElementId": { const croppingElementId = nextAppState[key]; - const element = - croppingElementId && nextElements.get(croppingElementId); - if (element && !element.isDeleted) { + if (!croppingElementId) { + // previously there was a croppingElementId (assuming visible), now there is none visibleDifferenceFlag.value = true; } else { - nextAppState[key] = null; + const element = nextElements.get(croppingElementId); + + if (element && !element.isDeleted) { + visibleDifferenceFlag.value = true; + } else { + nextAppState[key] = null; + } } + break; } case "editingGroupId": diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index cd3960a537..b6e2a9f075 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -122,18 +122,6 @@ export const actionFinalize = register({ let newElements = elements; - const pendingImageElement = - appState.pendingImageElementId && - scene.getElement(appState.pendingImageElementId); - - if (pendingImageElement) { - scene.mutateElement( - pendingImageElement, - { isDeleted: true }, - { informMutation: false, isDragging: false }, - ); - } - if (window.document.activeElement instanceof HTMLElement) { focusContainer(); } @@ -280,7 +268,6 @@ export const actionFinalize = register({ element && isLinearElement(element) ? new LinearElementEditor(element, arrayToMap(newElements)) : appState.selectedLinearElement, - pendingImageElementId: null, }, // TODO: #7348 we should not capture everything, but if we don't, it leads to incosistencies -> revisit captureUpdate: CaptureUpdateAction.IMMEDIATELY, diff --git a/packages/excalidraw/appState.ts b/packages/excalidraw/appState.ts index 75e99768c6..dcc3fba11b 100644 --- a/packages/excalidraw/appState.ts +++ b/packages/excalidraw/appState.ts @@ -109,7 +109,6 @@ export const getDefaultAppState = (): Omit< value: 1 as NormalizedZoomValue, }, viewModeEnabled: false, - pendingImageElementId: null, showHyperlinkPopup: false, selectedLinearElement: null, snapLines: [], @@ -238,7 +237,6 @@ const APP_STATE_STORAGE_CONF = (< zenModeEnabled: { browser: true, export: false, server: false }, zoom: { browser: true, export: false, server: false }, viewModeEnabled: { browser: false, export: false, server: false }, - pendingImageElementId: { browser: false, export: false, server: false }, showHyperlinkPopup: { browser: false, export: false, server: false }, selectedLinearElement: { browser: true, export: false, server: false }, snapLines: { browser: false, export: false, server: false }, diff --git a/packages/excalidraw/components/Actions.tsx b/packages/excalidraw/components/Actions.tsx index 60dab78f4f..919e9c688d 100644 --- a/packages/excalidraw/components/Actions.tsx +++ b/packages/excalidraw/components/Actions.tsx @@ -352,7 +352,6 @@ export const ShapesSwitcher = ({ if (value === "image") { app.setActiveTool({ type: value, - insertOnCanvasDirectly: pointerType !== "mouse", }); } else { app.setActiveTool({ type: value }); diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 8aa87b62bd..cdf5f1c906 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -161,7 +161,6 @@ import { maybeParseEmbedSrc, getEmbedLink, getInitializedImageElements, - loadHTMLImageElement, normalizeSVG, updateImageCache as _updateImageCache, getBoundTextElement, @@ -258,7 +257,6 @@ import type { ExcalidrawEmbeddableElement, Ordered, MagicGenerationData, - ExcalidrawNonSelectionElement, ExcalidrawArrowElement, ExcalidrawElbowArrowElement, } from "@excalidraw/element/types"; @@ -338,7 +336,6 @@ import { } from "../scene"; import { getStateForZoom } from "../scene/zoom"; import { - dataURLToFile, dataURLToString, generateIdFromFile, getDataURL, @@ -440,7 +437,6 @@ import type { AppProps, AppState, BinaryFileData, - DataURL, ExcalidrawImperativeAPI, BinaryFiles, Gesture, @@ -1520,7 +1516,6 @@ class App extends React.Component { width: this.state.width, editingTextElement: this.state.editingTextElement, newElementId: this.state.newElement?.id, - pendingImageElementId: this.state.pendingImageElementId, }); this.visibleElements = visibleElements; @@ -4711,16 +4706,10 @@ class App extends React.Component { }; setActiveTool = ( - tool: ( - | ( - | { type: Exclude } - | { - type: Extract; - insertOnCanvasDirectly?: boolean; - } - ) - | { type: "custom"; customType: string } - ) & { locked?: boolean; fromSelection?: boolean }, + tool: ({ type: ToolType } | { type: "custom"; customType: string }) & { + locked?: boolean; + fromSelection?: boolean; + }, keepSelection = false, ) => { if (!this.isToolSupported(tool.type)) { @@ -4746,10 +4735,7 @@ class App extends React.Component { this.setState({ suggestedBindings: [] }); } if (nextActiveTool.type === "image") { - this.onImageAction({ - insertOnCanvasDirectly: - (tool.type === "image" && tool.insertOnCanvasDirectly) ?? false, - }); + this.onImageAction(); } this.setState((prevState) => { @@ -6595,34 +6581,6 @@ class App extends React.Component { this.state.activeTool.type, pointerDownState, ); - } else if (this.state.activeTool.type === "image") { - // reset image preview on pointerdown - setCursor(this.interactiveCanvas, CURSOR_TYPE.CROSSHAIR); - - // retrieve the latest element as the state may be stale - const pendingImageElement = - this.state.pendingImageElementId && - this.scene.getElement(this.state.pendingImageElementId); - - if (!pendingImageElement) { - return; - } - - this.setState({ - newElement: pendingImageElement as ExcalidrawNonSelectionElement, - pendingImageElementId: null, - multiElement: null, - }); - - const { x, y } = viewportCoordsToSceneCoords(event, this.state); - - const frame = this.getTopLayerFrameAtSceneCoords({ x, y }); - - this.scene.mutateElement(pendingImageElement, { - x, - y, - frameId: frame ? frame.id : null, - }); } else if (this.state.activeTool.type === "freedraw") { this.handleFreeDrawElementOnPointerDown( event, @@ -6646,7 +6604,8 @@ class App extends React.Component { ); } else if ( this.state.activeTool.type !== "eraser" && - this.state.activeTool.type !== "hand" + this.state.activeTool.type !== "hand" && + this.state.activeTool.type !== "image" ) { this.createGenericElementOnPointerDown( this.state.activeTool.type, @@ -9092,10 +9051,6 @@ class App extends React.Component { pointerDownState.eventListeners.onKeyUp!, ); - if (this.state.pendingImageElementId) { - this.setState({ pendingImageElementId: null }); - } - this.props?.onPointerUp?.(activeTool, pointerDownState); this.onPointerUpEmitter.trigger( this.state.activeTool, @@ -9873,11 +9828,9 @@ class App extends React.Component { private initializeImage = async ({ imageFile, imageElement: _imageElement, - showCursorImagePreview = false, }: { imageFile: File; imageElement: ExcalidrawImageElement; - showCursorImagePreview?: boolean; }) => { // at this point this should be guaranteed image file, but we do this check // to satisfy TS down the line @@ -9935,16 +9888,6 @@ class App extends React.Component { } } - if (showCursorImagePreview) { - const dataURL = this.files[fileId]?.dataURL; - // optimization so that we don't unnecessarily resize the original - // full-size file for cursor preview - // (it's much faster to convert the resized dataURL to File) - const resizedFile = dataURL && dataURLToFile(dataURL); - - this.setImagePreviewCursor(resizedFile || imageFile); - } - const dataURL = this.files[fileId]?.dataURL || (await getDataURL(imageFile)); @@ -9983,11 +9926,7 @@ class App extends React.Component { const imageHTML = await cachedImageData?.image; - if ( - imageHTML && - this.state.pendingImageElementId !== imageElement.id && - this.state.newElement?.id !== imageElement.id - ) { + if (imageHTML && this.state.newElement?.id !== imageElement.id) { const naturalDimensions = this.getImageNaturalDimensions( imageElement, imageHTML, @@ -10000,10 +9939,6 @@ class App extends React.Component { } catch (error: any) { console.error(error); reject(new Error(t("errors.imageInsertError"))); - } finally { - if (!showCursorImagePreview) { - resetCursor(this.interactiveCanvas); - } } }, ); @@ -10015,7 +9950,6 @@ class App extends React.Component { insertImageElement = async ( imageElement: ExcalidrawImageElement, imageFile: File, - showCursorImagePreview?: boolean, ) => { // we should be handling all cases upstream, but in case we forget to handle // a future case, let's throw here @@ -10030,7 +9964,6 @@ class App extends React.Component { const image = await this.initializeImage({ imageFile, imageElement, - showCursorImagePreview, }); const nextElements = this.scene @@ -10063,58 +9996,7 @@ class App extends React.Component { } }; - private setImagePreviewCursor = async (imageFile: File) => { - // mustn't be larger than 128 px - // https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Basic_User_Interface/Using_URL_values_for_the_cursor_property - const cursorImageSizePx = 96; - let imagePreview; - - try { - imagePreview = await resizeImageFile(imageFile, { - maxWidthOrHeight: cursorImageSizePx, - }); - } catch (e: any) { - if (e.cause === "UNSUPPORTED") { - throw new Error(t("errors.unsupportedFileType")); - } - throw e; - } - - let previewDataURL = await getDataURL(imagePreview); - - // SVG cannot be resized via `resizeImageFile` so we resize by rendering to - // a small canvas - if (imageFile.type === MIME_TYPES.svg) { - const img = await loadHTMLImageElement(previewDataURL); - - let height = Math.min(img.height, cursorImageSizePx); - let width = height * (img.width / img.height); - - if (width > cursorImageSizePx) { - width = cursorImageSizePx; - height = width * (img.height / img.width); - } - - const canvas = document.createElement("canvas"); - canvas.height = height; - canvas.width = width; - const context = canvas.getContext("2d")!; - - context.drawImage(img, 0, 0, width, height); - - previewDataURL = canvas.toDataURL(MIME_TYPES.svg) as DataURL; - } - - if (this.state.pendingImageElementId) { - setCursor(this.interactiveCanvas, `url(${previewDataURL}) 4 4, auto`); - } - }; - - private onImageAction = async ({ - insertOnCanvasDirectly, - }: { - insertOnCanvasDirectly: boolean; - }) => { + private onImageAction = async () => { try { const clientX = this.state.width / 2 + this.state.offsetLeft; const clientY = this.state.height / 2 + this.state.offsetTop; @@ -10137,35 +10019,20 @@ class App extends React.Component { addToFrameUnderCursor: false, }); - if (insertOnCanvasDirectly) { - this.insertImageElement(imageElement, imageFile); - this.initializeImageDimensions(imageElement); - this.store.scheduleCapture(); - this.setState( - { - selectedElementIds: makeNextSelectedElementIds( - { [imageElement.id]: true }, - this.state, - ), - }, - () => { - this.actionManager.executeAction(actionFinalize); - }, - ); - } else { - this.setState( - { - pendingImageElementId: imageElement.id, - }, - () => { - this.insertImageElement( - imageElement, - imageFile, - /* showCursorImagePreview */ true, - ); - }, - ); - } + this.insertImageElement(imageElement, imageFile); + this.initializeImageDimensions(imageElement); + this.store.scheduleCapture(); + this.setState( + { + selectedElementIds: makeNextSelectedElementIds( + { [imageElement.id]: true }, + this.state, + ), + }, + () => { + this.actionManager.executeAction(actionFinalize); + }, + ); } catch (error: any) { if (error.name !== "AbortError") { console.error(error); @@ -10174,7 +10041,6 @@ class App extends React.Component { } this.setState( { - pendingImageElementId: null, newElement: null, activeTool: updateActiveTool(this.state, { type: "selection" }), }, diff --git a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx index b0fc5e7eef..740fa01620 100644 --- a/packages/excalidraw/components/CommandPalette/CommandPalette.tsx +++ b/packages/excalidraw/components/CommandPalette/CommandPalette.tsx @@ -503,7 +503,6 @@ function CommandPaletteInner({ if (value === "image") { app.setActiveTool({ type: value, - insertOnCanvasDirectly: event.type === EVENT.KEYDOWN, }); } else { app.setActiveTool({ type: value }); diff --git a/packages/excalidraw/components/HintViewer.tsx b/packages/excalidraw/components/HintViewer.tsx index f0cef544bb..5ab9d7bcea 100644 --- a/packages/excalidraw/components/HintViewer.tsx +++ b/packages/excalidraw/components/HintViewer.tsx @@ -74,10 +74,6 @@ const getHints = ({ return t("hints.embeddable"); } - if (appState.activeTool.type === "image" && appState.pendingImageElementId) { - return t("hints.placeImage"); - } - const selectedElements = app.scene.getSelectedElements(appState); if ( diff --git a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx index 37c754cb9d..4b1cd70605 100644 --- a/packages/excalidraw/components/canvases/InteractiveCanvas.tsx +++ b/packages/excalidraw/components/canvases/InteractiveCanvas.tsx @@ -198,7 +198,6 @@ const getRelevantAppStateProps = ( offsetLeft: appState.offsetLeft, offsetTop: appState.offsetTop, theme: appState.theme, - pendingImageElementId: appState.pendingImageElementId, selectionElement: appState.selectionElement, selectedGroupIds: appState.selectedGroupIds, selectedLinearElement: appState.selectedLinearElement, diff --git a/packages/excalidraw/components/canvases/StaticCanvas.tsx b/packages/excalidraw/components/canvases/StaticCanvas.tsx index 5a498ebacc..01ce94c431 100644 --- a/packages/excalidraw/components/canvases/StaticCanvas.tsx +++ b/packages/excalidraw/components/canvases/StaticCanvas.tsx @@ -100,7 +100,6 @@ const getRelevantAppStateProps = (appState: AppState): StaticCanvasAppState => { offsetLeft: appState.offsetLeft, offsetTop: appState.offsetTop, theme: appState.theme, - pendingImageElementId: appState.pendingImageElementId, shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom, viewBackgroundColor: appState.viewBackgroundColor, exportScale: appState.exportScale, diff --git a/packages/excalidraw/locales/en.json b/packages/excalidraw/locales/en.json index 5a887482c9..b89e8ae5b8 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -347,7 +347,6 @@ "lineEditor_line_info": "Double-click or press Enter to edit points", "lineEditor_pointSelected": "Press Delete to remove point(s),\nCtrlOrCmd+D to duplicate, or drag to move", "lineEditor_nothingSelected": "Select a point to edit (hold SHIFT to select multiple),\nor hold Alt and click to add new points", - "placeImage": "Click to place the image, or click and drag to set its size manually", "publishLibrary": "Publish your own library", "bindTextToElement": "Press enter to add text", "createFlowchart": "Hold CtrlOrCmd and Arrow key to create a flowchart", diff --git a/packages/excalidraw/scene/Renderer.ts b/packages/excalidraw/scene/Renderer.ts index cf112c2ced..6a016ccc1d 100644 --- a/packages/excalidraw/scene/Renderer.ts +++ b/packages/excalidraw/scene/Renderer.ts @@ -1,5 +1,4 @@ import { isElementInViewport } from "@excalidraw/element"; -import { isImageElement } from "@excalidraw/element"; import { memoize, toBrandedType } from "@excalidraw/common"; @@ -72,25 +71,14 @@ export class Renderer { elements, editingTextElement, newElementId, - pendingImageElementId, }: { elements: readonly NonDeletedExcalidrawElement[]; editingTextElement: AppState["editingTextElement"]; newElementId: ExcalidrawElement["id"] | undefined; - pendingImageElementId: AppState["pendingImageElementId"]; }) => { const elementsMap = toBrandedType(new Map()); for (const element of elements) { - if (isImageElement(element)) { - if ( - // => not placed on canvas yet (but in elements array) - pendingImageElementId === element.id - ) { - continue; - } - } - if (newElementId === element.id) { continue; } @@ -119,7 +107,6 @@ export class Renderer { width, editingTextElement, newElementId, - pendingImageElementId, // cache-invalidation nonce sceneNonce: _sceneNonce, }: { @@ -134,7 +121,6 @@ export class Renderer { /** note: first render of newElement will always bust the cache * (we'd have to prefilter elements outside of this function) */ newElementId: ExcalidrawElement["id"] | undefined; - pendingImageElementId: AppState["pendingImageElementId"]; sceneNonce: ReturnType["getSceneNonce"]>; }) => { const elements = this.scene.getNonDeletedElements(); @@ -143,7 +129,6 @@ export class Renderer { elements, editingTextElement, newElementId, - pendingImageElementId, }); const visibleElements = getVisibleCanvasElements({ diff --git a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 079e82da93..5f609f1e46 100644 --- a/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap @@ -957,7 +957,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1153,7 +1152,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1367,7 +1365,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1698,7 +1695,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2029,7 +2025,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2243,7 +2238,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2484,7 +2478,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2782,7 +2775,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -3154,7 +3146,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3647,7 +3638,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3970,7 +3960,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4293,7 +4282,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -5578,7 +5566,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -6795,7 +6782,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -7733,7 +7719,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8730,7 +8715,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9724,7 +9708,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index ed4ef54ce4..4d590b3b04 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -79,7 +79,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id4": true, }, @@ -692,7 +691,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id4": true, }, @@ -1178,7 +1176,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1542,7 +1539,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1909,7 +1905,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2169,7 +2164,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2611,7 +2605,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2873,7 +2866,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3139,7 +3131,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3433,7 +3424,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3719,7 +3709,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3954,7 +3943,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4211,7 +4199,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4482,7 +4469,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4711,7 +4697,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4940,7 +4925,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5167,7 +5151,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5394,7 +5377,6 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5647,7 +5629,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id1": true, }, @@ -5909,7 +5890,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id8": true, }, @@ -6272,7 +6252,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id1": true, }, @@ -6649,7 +6628,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -6958,7 +6936,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7261,7 +7238,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7459,7 +7435,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7811,7 +7786,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8163,7 +8137,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -8569,7 +8542,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8856,7 +8828,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9120,7 +9091,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9385,7 +9355,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9620,7 +9589,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9914,7 +9882,6 @@ exports[`history > multiplayer undo/redo > should override remotely added points }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10260,7 +10227,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10488,7 +10454,6 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10933,7 +10898,6 @@ exports[`history > multiplayer undo/redo > should update history entries after r }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11193,7 +11157,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11428,7 +11391,6 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11665,7 +11627,6 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12071,7 +12032,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12281,7 +12241,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12491,7 +12450,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12715,7 +12673,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -12939,7 +12896,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on s }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": -50, @@ -13181,7 +13137,6 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -13418,7 +13373,6 @@ exports[`history > singleplayer undo/redo > should end up with no history entry }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -13655,7 +13609,6 @@ exports[`history > singleplayer undo/redo > should iterate through the history w }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -13902,7 +13855,6 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14236,7 +14188,6 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14403,7 +14354,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14690,7 +14640,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14950,7 +14899,6 @@ exports[`history > singleplayer undo/redo > should not override appstate changes }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -15235,7 +15183,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15394,7 +15341,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -16093,7 +16039,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -16725,7 +16670,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -17357,7 +17301,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -18070,7 +18013,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -18815,7 +18757,6 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -19295,7 +19236,6 @@ exports[`history > singleplayer undo/redo > should support duplication of groups }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id1": true, }, @@ -19806,7 +19746,6 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -20265,7 +20204,6 @@ exports[`history > singleplayer undo/redo > should support linear element creati }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, diff --git a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap index d90fe92914..c5ac974eca 100644 --- a/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/regressionTests.test.tsx.snap @@ -80,7 +80,6 @@ exports[`given element A and group of elements B and given both are selected whe }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -506,7 +505,6 @@ exports[`given element A and group of elements B and given both are selected whe }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -922,7 +920,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1488,7 +1485,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1695,7 +1691,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -2079,7 +2074,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -2324,7 +2318,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2504,7 +2497,6 @@ exports[`regression tests > can drag element that covers another element, while }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id6": true, }, @@ -2829,7 +2821,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -3084,7 +3075,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -3325,7 +3315,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -3561,7 +3550,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -3819,7 +3807,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id6": true, }, @@ -4133,7 +4120,6 @@ exports[`regression tests > deleting last but one element in editing group shoul }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -4569,7 +4555,6 @@ exports[`regression tests > deselects group of selected elements on pointer down }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -4852,7 +4837,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -5128,7 +5112,6 @@ exports[`regression tests > deselects selected element on pointer down when poin }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -5336,7 +5319,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -5536,7 +5518,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -5929,7 +5910,6 @@ exports[`regression tests > drags selected elements from point inside common bou }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -6226,7 +6206,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -7058,7 +7037,6 @@ exports[`regression tests > given a group of selected elements with an element t }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id6": true, @@ -7392,7 +7370,6 @@ exports[`regression tests > given a selected element A and a not selected elemen }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -7671,7 +7648,6 @@ exports[`regression tests > given selected element A with lower z-index than uns }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -7906,7 +7882,6 @@ exports[`regression tests > given selected element A with lower z-index than uns }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -8146,7 +8121,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8326,7 +8300,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8506,7 +8479,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8686,7 +8658,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -8912,7 +8883,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9136,7 +9106,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9332,7 +9301,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9558,7 +9526,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9738,7 +9705,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9962,7 +9928,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10142,7 +10107,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10338,7 +10302,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10518,7 +10481,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -11049,7 +11011,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -11329,7 +11290,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": "-6.25000", @@ -11452,7 +11412,6 @@ exports[`regression tests > shift click on selected element should deselect it o }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -11652,7 +11611,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -11971,7 +11929,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -12400,7 +12357,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id15": true, @@ -13043,7 +12999,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 60, @@ -13166,7 +13121,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -13797,7 +13751,6 @@ exports[`regression tests > switches from group of selected elements to another }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, "id6": true, @@ -14136,7 +14089,6 @@ exports[`regression tests > switches selected element on pointer down > [end of }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -14400,7 +14352,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 20, @@ -14523,7 +14474,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14912,7 +14862,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -15038,7 +14987,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, diff --git a/packages/excalidraw/types.ts b/packages/excalidraw/types.ts index e0f908177b..7981e7b7f4 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -24,7 +24,6 @@ import type { ChartType, FontFamilyValues, FileId, - ExcalidrawImageElement, Theme, StrokeRoundness, ExcalidrawEmbeddableElement, @@ -191,7 +190,6 @@ type _CommonCanvasAppState = { offsetLeft: AppState["offsetLeft"]; offsetTop: AppState["offsetTop"]; theme: AppState["theme"]; - pendingImageElementId: AppState["pendingImageElementId"]; }; export type StaticCanvasAppState = Readonly< @@ -416,8 +414,6 @@ export interface AppState { shown: true; data: Spreadsheet; }; - /** imageElement waiting to be placed on canvas */ - pendingImageElementId: ExcalidrawImageElement["id"] | null; showHyperlinkPopup: false | "info" | "editor"; selectedLinearElement: LinearElementEditor | null; snapLines: readonly SnapLine[]; diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 8307e5a543..209ef87579 100644 --- a/packages/utils/tests/__snapshots__/export.test.ts.snap +++ b/packages/utils/tests/__snapshots__/export.test.ts.snap @@ -81,7 +81,6 @@ exports[`exportToSvg > with default arguments 1`] = ` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, From 18808481fdbe665fe5e37cb07b80148efcc03dc2 Mon Sep 17 00:00:00 2001 From: Ashwin Temkar <83207402+ashwintemkar@users.noreply.github.com> Date: Wed, 11 Jun 2025 20:22:02 +0530 Subject: [PATCH 09/22] fix: set cursor to auto when not hovering a point on linear element (#9642) * fix: set cursor to auto when not hovering a point on linear element #9628 * Simplify hover test for cursor * Add back comment * Fix test for hit testing --------- Co-authored-by: Mark Tolmacs --- packages/element/tests/collision.test.tsx | 2 +- packages/excalidraw/components/App.tsx | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/element/tests/collision.test.tsx b/packages/element/tests/collision.test.tsx index bcbf114f5e..72996bdb1f 100644 --- a/packages/element/tests/collision.test.tsx +++ b/packages/element/tests/collision.test.tsx @@ -28,7 +28,7 @@ describe("check rotated elements can be hit:", () => { //const p = [120, -211]; //const p = [0, 13]; const hit = hitElementItself({ - point: pointFrom(87, -68), + point: pointFrom(88, -68), element: window.h.elements[0], threshold: 10, elementsMap: window.h.scene.getNonDeletedElementsMap(), diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index cdf5f1c906..beca285e1c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -6147,7 +6147,10 @@ class App extends React.Component { setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } else if (isOverScrollBar) { setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); - } else if (this.state.selectedLinearElement) { + } else if ( + this.state.selectedLinearElement && + hitElement?.id === this.state.selectedLinearElement.elementId + ) { this.handleHoverSelectedLinearElement( this.state.selectedLinearElement, scenePointerX, From f42e1ab64efc79ad23babb1184f2750394d4045a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Wed, 11 Jun 2025 19:15:48 +0200 Subject: [PATCH 10/22] perf: Improve elbow arrow indirect binding logic (#9624) --- packages/element/src/elbowArrow.ts | 15 ++++----------- packages/element/src/resizeElements.ts | 6 +----- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index fb60c10db7..2425c350cf 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -20,6 +20,7 @@ import { tupleToCoors, getSizeFromPoints, isDevEnv, + arrayToMap, } from "@excalidraw/common"; import type { AppState } from "@excalidraw/excalidraw/types"; @@ -2208,20 +2209,12 @@ const getGlobalPoint = ( return initialPoint; } - if (element && elementsMap) { - const fixedGlobalPoint = getGlobalFixedPointForBindableElement( + if (element) { + return getGlobalFixedPointForBindableElement( fixedPointRatio || [0, 0], element, - elementsMap, + elementsMap ?? arrayToMap([element]), ); - - // NOTE: Resize scales the binding position point too, so we need to update it - return Math.abs( - distanceToElement(element, elementsMap, fixedGlobalPoint) - - FIXED_BINDING_DISTANCE, - ) > 0.01 - ? bindPointToSnapToElementOutline(arrow, element, startOrEnd, elementsMap) - : fixedGlobalPoint; } return initialPoint; diff --git a/packages/element/src/resizeElements.ts b/packages/element/src/resizeElements.ts index dea6e3d75d..96a053226a 100644 --- a/packages/element/src/resizeElements.ts +++ b/packages/element/src/resizeElements.ts @@ -1518,11 +1518,7 @@ export const resizeMultipleElements = ( } of elementsAndUpdates) { const { width, height, angle } = update; - scene.mutateElement(element, update, { - informMutation: true, - // needed for the fixed binding point udpate to take effect - isDragging: true, - }); + scene.mutateElement(element, update); updateBoundElements(element, scene, { simultaneouslyUpdated: elementsToUpdate, From 9f3fdf55054827507ca324129436d23490789144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Thu, 12 Jun 2025 10:39:50 +0200 Subject: [PATCH 11/22] fix: Test hook usage in production code (#9645) --- packages/excalidraw/renderer/helpers.ts | 13 +++++-------- packages/excalidraw/renderer/interactiveScene.ts | 9 +++++++-- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/excalidraw/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts index 9dd26df646..c94bcd8eb6 100644 --- a/packages/excalidraw/renderer/helpers.ts +++ b/packages/excalidraw/renderer/helpers.ts @@ -16,6 +16,7 @@ import { } from "@excalidraw/math"; import type { + ElementsMap, ExcalidrawDiamondElement, ExcalidrawRectanguloidElement, } from "@excalidraw/element/types"; @@ -128,14 +129,12 @@ function drawCatmullRomCubicApprox( export const drawHighlightForRectWithRotation = ( context: CanvasRenderingContext2D, element: ExcalidrawRectanguloidElement, + elementsMap: ElementsMap, padding: number, ) => { const [x, y] = pointRotateRads( pointFrom(element.x, element.y), - elementCenterPoint( - element, - window.h.app.scene.getElementsMapIncludingDeleted(), - ), + elementCenterPoint(element, elementsMap), element.angle, ); @@ -289,13 +288,11 @@ export const drawHighlightForDiamondWithRotation = ( context: CanvasRenderingContext2D, padding: number, element: ExcalidrawDiamondElement, + elementsMap: ElementsMap, ) => { const [x, y] = pointRotateRads( pointFrom(element.x, element.y), - elementCenterPoint( - element, - window.h.app.scene.getElementsMapIncludingDeleted(), - ), + elementCenterPoint(element, elementsMap), element.angle, ); context.save(); diff --git a/packages/excalidraw/renderer/interactiveScene.ts b/packages/excalidraw/renderer/interactiveScene.ts index 40bce1c7d0..1f3e0ff21d 100644 --- a/packages/excalidraw/renderer/interactiveScene.ts +++ b/packages/excalidraw/renderer/interactiveScene.ts @@ -205,10 +205,15 @@ const renderBindingHighlightForBindableElement = ( case "embeddable": case "frame": case "magicframe": - drawHighlightForRectWithRotation(context, element, padding); + drawHighlightForRectWithRotation(context, element, elementsMap, padding); break; case "diamond": - drawHighlightForDiamondWithRotation(context, padding, element); + drawHighlightForDiamondWithRotation( + context, + padding, + element, + elementsMap, + ); break; case "ellipse": { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap); From f0458cc216ceabb6f0f202c78aec5729d01883da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Thu, 12 Jun 2025 21:08:37 +0200 Subject: [PATCH 12/22] fix: Mid-point for rounded linears are not precisely centered (#9544) --- packages/element/src/ShapeCache.ts | 95 ---- packages/element/src/binding.ts | 2 +- packages/element/src/bounds.ts | 72 ++- packages/element/src/collision.ts | 15 +- packages/element/src/distance.ts | 8 +- packages/element/src/elbowArrow.ts | 2 +- packages/element/src/flowchart.ts | 2 +- packages/element/src/index.ts | 4 +- packages/element/src/linearElementEditor.ts | 97 ++-- packages/element/src/mutateElement.ts | 2 +- packages/element/src/renderElement.ts | 4 +- packages/element/src/{Shape.ts => shape.ts} | 227 ++++++++- packages/element/src/shapes.ts | 447 ------------------ packages/element/src/textElement.ts | 3 - packages/element/src/utils.ts | 57 ++- .../tests/linearElementEditor.test.tsx | 36 +- .../excalidraw/actions/actionLinearEditor.tsx | 7 +- .../excalidraw/actions/actionProperties.tsx | 10 +- .../data/__snapshots__/transform.test.ts.snap | 2 +- packages/math/src/constants.ts | 57 +++ packages/math/src/curve.ts | 121 +++++ 21 files changed, 609 insertions(+), 661 deletions(-) delete mode 100644 packages/element/src/ShapeCache.ts rename packages/element/src/{Shape.ts => shape.ts} (79%) delete mode 100644 packages/element/src/shapes.ts create mode 100644 packages/math/src/constants.ts diff --git a/packages/element/src/ShapeCache.ts b/packages/element/src/ShapeCache.ts deleted file mode 100644 index 8f0c94324c..0000000000 --- a/packages/element/src/ShapeCache.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { RoughGenerator } from "roughjs/bin/generator"; - -import { COLOR_PALETTE } from "@excalidraw/common"; - -import type { - AppState, - EmbedsValidationStatus, -} from "@excalidraw/excalidraw/types"; -import type { - ElementShape, - ElementShapes, -} from "@excalidraw/excalidraw/scene/types"; - -import { _generateElementShape } from "./Shape"; - -import { elementWithCanvasCache } from "./renderElement"; - -import type { ExcalidrawElement, ExcalidrawSelectionElement } from "./types"; - -import type { Drawable } from "roughjs/bin/core"; - -export class ShapeCache { - private static rg = new RoughGenerator(); - private static cache = new WeakMap(); - - /** - * Retrieves shape from cache if available. Use this only if shape - * is optional and you have a fallback in case it's not cached. - */ - public static get = (element: T) => { - return ShapeCache.cache.get( - element, - ) as T["type"] extends keyof ElementShapes - ? ElementShapes[T["type"]] | undefined - : ElementShape | undefined; - }; - - public static set = ( - element: T, - shape: T["type"] extends keyof ElementShapes - ? ElementShapes[T["type"]] - : Drawable, - ) => ShapeCache.cache.set(element, shape); - - public static delete = (element: ExcalidrawElement) => - ShapeCache.cache.delete(element); - - public static destroy = () => { - ShapeCache.cache = new WeakMap(); - }; - - /** - * Generates & caches shape for element if not already cached, otherwise - * returns cached shape. - */ - public static generateElementShape = < - T extends Exclude, - >( - element: T, - renderConfig: { - isExporting: boolean; - canvasBackgroundColor: AppState["viewBackgroundColor"]; - embedsValidationStatus: EmbedsValidationStatus; - } | null, - ) => { - // when exporting, always regenerated to guarantee the latest shape - const cachedShape = renderConfig?.isExporting - ? undefined - : ShapeCache.get(element); - - // `null` indicates no rc shape applicable for this element type, - // but it's considered a valid cache value (= do not regenerate) - if (cachedShape !== undefined) { - return cachedShape; - } - - elementWithCanvasCache.delete(element); - - const shape = _generateElementShape( - element, - ShapeCache.rg, - renderConfig || { - isExporting: false, - canvasBackgroundColor: COLOR_PALETTE.white, - embedsValidationStatus: null, - }, - ) as T["type"] extends keyof ElementShapes - ? ElementShapes[T["type"]] - : Drawable | null; - - ShapeCache.cache.set(element, shape); - - return shape; - }; -} diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 0a7a4a68ae..16f3216616 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -61,7 +61,7 @@ import { isTextElement, } from "./typeChecks"; -import { aabbForElement } from "./shapes"; +import { aabbForElement } from "./bounds"; import { updateElbowArrowPoints } from "./elbowArrow"; import type { Scene } from "./Scene"; diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index 1bfb441585..2a7b3fb25c 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -2,6 +2,7 @@ import rough from "roughjs/bin/rough"; import { arrayToMap, + elementCenterPoint, invariant, rescalePoints, sizeOf, @@ -33,8 +34,8 @@ import type { AppState } from "@excalidraw/excalidraw/types"; import type { Mutable } from "@excalidraw/common/utility-types"; -import { generateRoughOptions } from "./Shape"; -import { ShapeCache } from "./ShapeCache"; +import { generateRoughOptions } from "./shape"; +import { ShapeCache } from "./shape"; import { LinearElementEditor } from "./linearElementEditor"; import { getBoundTextElement, getContainerElement } from "./textElement"; import { @@ -45,7 +46,7 @@ import { isTextElement, } from "./typeChecks"; -import { getElementShape } from "./shapes"; +import { getElementShape } from "./shape"; import { deconstructDiamondElement, @@ -1178,3 +1179,68 @@ export const doBoundsIntersect = ( return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; }; + +/** + * Get the axis-aligned bounding box for a given element + */ +export const aabbForElement = ( + element: Readonly, + elementsMap: ElementsMap, + offset?: [number, number, number, number], +) => { + const bbox = { + minX: element.x, + minY: element.y, + maxX: element.x + element.width, + maxY: element.y + element.height, + midX: element.x + element.width / 2, + midY: element.y + element.height / 2, + }; + + const center = elementCenterPoint(element, elementsMap); + const [topLeftX, topLeftY] = pointRotateRads( + pointFrom(bbox.minX, bbox.minY), + center, + element.angle, + ); + const [topRightX, topRightY] = pointRotateRads( + pointFrom(bbox.maxX, bbox.minY), + center, + element.angle, + ); + const [bottomRightX, bottomRightY] = pointRotateRads( + pointFrom(bbox.maxX, bbox.maxY), + center, + element.angle, + ); + const [bottomLeftX, bottomLeftY] = pointRotateRads( + pointFrom(bbox.minX, bbox.maxY), + center, + element.angle, + ); + + const bounds = [ + Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX), + Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY), + Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX), + Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY), + ] as Bounds; + + if (offset) { + const [topOffset, rightOffset, downOffset, leftOffset] = offset; + return [ + bounds[0] - leftOffset, + bounds[1] - topOffset, + bounds[2] + rightOffset, + bounds[3] + downOffset, + ] as Bounds; + } + + return bounds; +}; + +export const pointInsideBounds =

( + p: P, + bounds: Bounds, +): boolean => + p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index af81ff99ce..88e96a15ad 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -22,7 +22,7 @@ import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; -import { isPathALoop } from "./shapes"; +import { isPathALoop } from "./utils"; import { type Bounds, doBoundsIntersect, @@ -250,25 +250,16 @@ export const intersectElementWithLineSegment = ( case "line": case "freedraw": case "arrow": - return intersectLinearOrFreeDrawWithLineSegment( - element, - elementsMap, - line, - onlyFirst, - ); + return intersectLinearOrFreeDrawWithLineSegment(element, line, onlyFirst); } }; const intersectLinearOrFreeDrawWithLineSegment = ( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, - elementsMap: ElementsMap, segment: LineSegment, onlyFirst = false, ): GlobalPoint[] => { - const [lines, curves] = deconstructLinearOrFreeDrawElement( - element, - elementsMap, - ); + const [lines, curves] = deconstructLinearOrFreeDrawElement(element); const intersections = []; for (const l of lines) { diff --git a/packages/element/src/distance.ts b/packages/element/src/distance.ts index 55e9ed2bdb..ed900ecc69 100644 --- a/packages/element/src/distance.ts +++ b/packages/element/src/distance.ts @@ -48,7 +48,7 @@ export const distanceToElement = ( case "line": case "arrow": case "freedraw": - return distanceToLinearOrFreeDraElement(element, elementsMap, p); + return distanceToLinearOrFreeDraElement(element, p); } }; @@ -133,13 +133,9 @@ const distanceToEllipseElement = ( const distanceToLinearOrFreeDraElement = ( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, - elementsMap: ElementsMap, p: GlobalPoint, ) => { - const [lines, curves] = deconstructLinearOrFreeDrawElement( - element, - elementsMap, - ); + const [lines, curves] = deconstructLinearOrFreeDrawElement(element); return Math.min( ...lines.map((s) => distanceToLineSegment(p, s)), ...curves.map((a) => curvePointDistance(a, p)), diff --git a/packages/element/src/elbowArrow.ts b/packages/element/src/elbowArrow.ts index 2425c350cf..0021851645 100644 --- a/packages/element/src/elbowArrow.ts +++ b/packages/element/src/elbowArrow.ts @@ -52,7 +52,7 @@ import { type NonDeletedSceneElementsMap, } from "./types"; -import { aabbForElement, pointInsideBounds } from "./shapes"; +import { aabbForElement, pointInsideBounds } from "./bounds"; import type { Bounds } from "./bounds"; import type { Heading } from "./heading"; diff --git a/packages/element/src/flowchart.ts b/packages/element/src/flowchart.ts index 9e5af4216e..6cffb56a83 100644 --- a/packages/element/src/flowchart.ts +++ b/packages/element/src/flowchart.ts @@ -21,7 +21,7 @@ import { import { LinearElementEditor } from "./linearElementEditor"; import { mutateElement } from "./mutateElement"; import { newArrowElement, newElement } from "./newElement"; -import { aabbForElement } from "./shapes"; +import { aabbForElement } from "./bounds"; import { elementsAreInFrameBounds, elementOverlapsWithFrame } from "./frame"; import { isBindableElement, diff --git a/packages/element/src/index.ts b/packages/element/src/index.ts index 93024f9940..9bf5214d0f 100644 --- a/packages/element/src/index.ts +++ b/packages/element/src/index.ts @@ -102,9 +102,7 @@ export * from "./resizeElements"; export * from "./resizeTest"; export * from "./Scene"; export * from "./selection"; -export * from "./Shape"; -export * from "./ShapeCache"; -export * from "./shapes"; +export * from "./shape"; export * from "./showSelectedShapeActions"; export * from "./sizeHelpers"; export * from "./sortElements"; diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 447d1e3682..44d365e2e5 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -7,6 +7,8 @@ import { type LocalPoint, pointDistance, vectorFromPoint, + curveLength, + curvePointAtLength, } from "@excalidraw/math"; import { getCurvePathOps } from "@excalidraw/utils/shape"; @@ -20,7 +22,11 @@ import { tupleToCoors, } from "@excalidraw/common"; -import type { Store } from "@excalidraw/element"; +import { + deconstructLinearOrFreeDrawElement, + isPathALoop, + type Store, +} from "@excalidraw/element"; import type { Radians } from "@excalidraw/math"; @@ -55,16 +61,7 @@ import { isFixedPointBinding, } from "./typeChecks"; -import { ShapeCache } from "./ShapeCache"; - -import { - isPathALoop, - getBezierCurveLength, - getControlPointsForBezierCurve, - mapIntervalToBezierT, - getBezierXY, - toggleLinePolygonState, -} from "./shapes"; +import { ShapeCache, toggleLinePolygonState } from "./shape"; import { getLockedLinearCursorAlignSize } from "./sizeHelpers"; @@ -629,10 +626,7 @@ export class LinearElementEditor { } const segmentMidPoint = LinearElementEditor.getSegmentMidPoint( element, - points[index], - points[index + 1], index + 1, - elementsMap, ); midpoints.push(segmentMidPoint); index++; @@ -734,7 +728,18 @@ export class LinearElementEditor { let distance = pointDistance(startPoint, endPoint); if (element.points.length > 2 && element.roundness) { - distance = getBezierCurveLength(element, endPoint); + const [lines, curves] = deconstructLinearOrFreeDrawElement(element); + + invariant( + lines.length === 0 && curves.length > 0, + "Only linears built out of curves are supported", + ); + invariant( + lines.length + curves.length >= index, + "Invalid segment index while calculating mid point", + ); + + distance = curveLength(curves[index]); } return distance * zoom.value < LinearElementEditor.POINT_HANDLE_SIZE * 4; @@ -742,39 +747,42 @@ export class LinearElementEditor { static getSegmentMidPoint( element: NonDeleted, - startPoint: GlobalPoint, - endPoint: GlobalPoint, - endPointIndex: number, - elementsMap: ElementsMap, + index: number, ): GlobalPoint { - let segmentMidPoint = pointCenter(startPoint, endPoint); - if (element.points.length > 2 && element.roundness) { - const controlPoints = getControlPointsForBezierCurve( - element, - element.points[endPointIndex], + if (isElbowArrow(element)) { + invariant( + element.points.length >= index, + "Invalid segment index while calculating elbow arrow mid point", ); - if (controlPoints) { - const t = mapIntervalToBezierT( - element, - element.points[endPointIndex], - 0.5, - ); - segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates( - element, - getBezierXY( - controlPoints[0], - controlPoints[1], - controlPoints[2], - controlPoints[3], - t, - ), - elementsMap, - ); - } + const p = pointCenter(element.points[index - 1], element.points[index]); + + return pointFrom(element.x + p[0], element.y + p[1]); } - return segmentMidPoint; + const [lines, curves] = deconstructLinearOrFreeDrawElement(element); + + invariant( + (lines.length === 0 && curves.length > 0) || + (lines.length > 0 && curves.length === 0), + "Only linears built out of either segments or curves are supported", + ); + invariant( + lines.length + curves.length >= index, + "Invalid segment index while calculating mid point", + ); + + if (lines.length) { + const segment = lines[index - 1]; + return pointCenter(segment[0], segment[1]); + } + + if (curves.length) { + const segment = curves[index - 1]; + return curvePointAtLength(segment, 0.5); + } + + invariant(false, "Invalid segment type while calculating mid point"); } static getSegmentMidPointIndex( @@ -1670,10 +1678,7 @@ export class LinearElementEditor { const index = element.points.length / 2 - 1; const midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint( element, - points[index], - points[index + 1], index + 1, - elementsMap, ); x = midSegmentMidpoint[0] - boundTextElement.width / 2; diff --git a/packages/element/src/mutateElement.ts b/packages/element/src/mutateElement.ts index 4b5526917f..0fc3e0bb8f 100644 --- a/packages/element/src/mutateElement.ts +++ b/packages/element/src/mutateElement.ts @@ -8,7 +8,7 @@ import type { Radians } from "@excalidraw/math"; import type { Mutable } from "@excalidraw/common/utility-types"; -import { ShapeCache } from "./ShapeCache"; +import { ShapeCache } from "./shape"; import { updateElbowArrowPoints } from "./elbowArrow"; diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index 2786f3f84a..e870d977fb 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -54,9 +54,9 @@ import { isImageElement, } from "./typeChecks"; import { getContainingFrame } from "./frame"; -import { getCornerRadius } from "./shapes"; +import { getCornerRadius } from "./utils"; -import { ShapeCache } from "./ShapeCache"; +import { ShapeCache } from "./shape"; import type { ExcalidrawElement, diff --git a/packages/element/src/Shape.ts b/packages/element/src/shape.ts similarity index 79% rename from packages/element/src/Shape.ts rename to packages/element/src/shape.ts index 317dfbacb8..7a8cd351a1 100644 --- a/packages/element/src/Shape.ts +++ b/packages/element/src/shape.ts @@ -1,12 +1,27 @@ import { simplify } from "points-on-curve"; +import { + type GeometricShape, + getClosedCurveShape, + getCurveShape, + getEllipseShape, + getFreedrawShape, + getPolygonShape, +} from "@excalidraw/utils/shape"; + import { pointFrom, pointDistance, type LocalPoint, pointRotateRads, } from "@excalidraw/math"; -import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common"; +import { + ROUGHNESS, + isTransparent, + assertNever, + COLOR_PALETTE, + LINE_POLYGON_POINT_MERGE_DISTANCE, +} from "@excalidraw/common"; import { RoughGenerator } from "roughjs/bin/generator"; @@ -14,17 +29,26 @@ import type { GlobalPoint } from "@excalidraw/math"; import type { Mutable } from "@excalidraw/common/utility-types"; -import type { EmbedsValidationStatus } from "@excalidraw/excalidraw/types"; -import type { ElementShapes } from "@excalidraw/excalidraw/scene/types"; +import type { + AppState, + EmbedsValidationStatus, +} from "@excalidraw/excalidraw/types"; +import type { + ElementShape, + ElementShapes, +} from "@excalidraw/excalidraw/scene/types"; + +import { elementWithCanvasCache } from "./renderElement"; import { + canBecomePolygon, isElbowArrow, isEmbeddableElement, isIframeElement, isIframeLikeElement, isLinearElement, } from "./typeChecks"; -import { getCornerRadius, isPathALoop } from "./shapes"; +import { getCornerRadius, isPathALoop } from "./utils"; import { headingForPointIsHorizontal } from "./heading"; import { canChangeRoundness } from "./comparisons"; @@ -33,8 +57,9 @@ import { getArrowheadPoints, getCenterForBounds, getDiamondPoints, - getElementBounds, + getElementAbsoluteCoords, } from "./bounds"; +import { shouldTestInside } from "./collision"; import type { ExcalidrawElement, @@ -44,11 +69,87 @@ import type { Arrowhead, ExcalidrawFreeDrawElement, ElementsMap, + ExcalidrawLineElement, } from "./types"; import type { Drawable, Options } from "roughjs/bin/core"; import type { Point as RoughPoint } from "roughjs/bin/geometry"; +export class ShapeCache { + private static rg = new RoughGenerator(); + private static cache = new WeakMap(); + + /** + * Retrieves shape from cache if available. Use this only if shape + * is optional and you have a fallback in case it's not cached. + */ + public static get = (element: T) => { + return ShapeCache.cache.get( + element, + ) as T["type"] extends keyof ElementShapes + ? ElementShapes[T["type"]] | undefined + : ElementShape | undefined; + }; + + public static set = ( + element: T, + shape: T["type"] extends keyof ElementShapes + ? ElementShapes[T["type"]] + : Drawable, + ) => ShapeCache.cache.set(element, shape); + + public static delete = (element: ExcalidrawElement) => + ShapeCache.cache.delete(element); + + public static destroy = () => { + ShapeCache.cache = new WeakMap(); + }; + + /** + * Generates & caches shape for element if not already cached, otherwise + * returns cached shape. + */ + public static generateElementShape = < + T extends Exclude, + >( + element: T, + renderConfig: { + isExporting: boolean; + canvasBackgroundColor: AppState["viewBackgroundColor"]; + embedsValidationStatus: EmbedsValidationStatus; + } | null, + ) => { + // when exporting, always regenerated to guarantee the latest shape + const cachedShape = renderConfig?.isExporting + ? undefined + : ShapeCache.get(element); + + // `null` indicates no rc shape applicable for this element type, + // but it's considered a valid cache value (= do not regenerate) + if (cachedShape !== undefined) { + return cachedShape; + } + + elementWithCanvasCache.delete(element); + + const shape = generateElementShape( + element, + ShapeCache.rg, + renderConfig || { + isExporting: false, + canvasBackgroundColor: COLOR_PALETTE.white, + embedsValidationStatus: null, + }, + ) as T["type"] extends keyof ElementShapes + ? ElementShapes[T["type"]] + : Drawable | null; + + ShapeCache.cache.set(element, shape); + + return shape; + }; +} + const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth]; @@ -320,7 +421,6 @@ const getArrowheadShapes = ( export const generateLinearCollisionShape = ( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, - elementsMap: ElementsMap, ) => { const generator = new RoughGenerator(); const options: Options = { @@ -331,7 +431,18 @@ export const generateLinearCollisionShape = ( preserveVertices: true, }; const center = getCenterForBounds( - getElementBounds(element, elementsMap, true), + // Need a non-rotated center point + element.points.reduce( + (acc, point) => { + return [ + Math.min(element.x + point[0], acc[0]), + Math.min(element.y + point[1], acc[1]), + Math.max(element.x + point[0], acc[2]), + Math.max(element.y + point[1], acc[3]), + ]; + }, + [Infinity, Infinity, -Infinity, -Infinity], + ), ); switch (element.type) { @@ -491,7 +602,7 @@ export const generateLinearCollisionShape = ( * * @private */ -export const _generateElementShape = ( +const generateElementShape = ( element: Exclude, generator: RoughGenerator, { @@ -792,3 +903,103 @@ const generateElbowArrowShape = ( return d.join(" "); }; + +/** + * get the pure geometric shape of an excalidraw elementw + * which is then used for hit detection + */ +export const getElementShape = ( + element: ExcalidrawElement, + elementsMap: ElementsMap, +): GeometricShape => { + switch (element.type) { + case "rectangle": + case "diamond": + case "frame": + case "magicframe": + case "embeddable": + case "image": + case "iframe": + case "text": + case "selection": + return getPolygonShape(element); + case "arrow": + case "line": { + const roughShape = + ShapeCache.get(element)?.[0] ?? + ShapeCache.generateElementShape(element, null)[0]; + const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); + + return shouldTestInside(element) + ? getClosedCurveShape( + element, + roughShape, + pointFrom(element.x, element.y), + element.angle, + pointFrom(cx, cy), + ) + : getCurveShape( + roughShape, + pointFrom(element.x, element.y), + element.angle, + pointFrom(cx, cy), + ); + } + + case "ellipse": + return getEllipseShape(element); + + case "freedraw": { + const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); + return getFreedrawShape( + element, + pointFrom(cx, cy), + shouldTestInside(element), + ); + } + } +}; + +export const toggleLinePolygonState = ( + element: ExcalidrawLineElement, + nextPolygonState: boolean, +): { + polygon: ExcalidrawLineElement["polygon"]; + points: ExcalidrawLineElement["points"]; +} | null => { + const updatedPoints = [...element.points]; + + if (nextPolygonState) { + if (!canBecomePolygon(element.points)) { + return null; + } + + const firstPoint = updatedPoints[0]; + const lastPoint = updatedPoints[updatedPoints.length - 1]; + + const distance = Math.hypot( + firstPoint[0] - lastPoint[0], + firstPoint[1] - lastPoint[1], + ); + + if ( + distance > LINE_POLYGON_POINT_MERGE_DISTANCE || + updatedPoints.length < 4 + ) { + updatedPoints.push(pointFrom(firstPoint[0], firstPoint[1])); + } else { + updatedPoints[updatedPoints.length - 1] = pointFrom( + firstPoint[0], + firstPoint[1], + ); + } + } + + // TODO: satisfies ElementUpdate + const ret = { + polygon: nextPolygonState, + points: updatedPoints, + }; + + return ret; +}; diff --git a/packages/element/src/shapes.ts b/packages/element/src/shapes.ts deleted file mode 100644 index 3abf7f5905..0000000000 --- a/packages/element/src/shapes.ts +++ /dev/null @@ -1,447 +0,0 @@ -import { - DEFAULT_ADAPTIVE_RADIUS, - DEFAULT_PROPORTIONAL_RADIUS, - LINE_CONFIRM_THRESHOLD, - ROUNDNESS, - invariant, - elementCenterPoint, - LINE_POLYGON_POINT_MERGE_DISTANCE, -} from "@excalidraw/common"; -import { - isPoint, - pointFrom, - pointDistance, - pointFromPair, - pointRotateRads, - pointsEqual, - type GlobalPoint, - type LocalPoint, -} from "@excalidraw/math"; -import { - getClosedCurveShape, - getCurvePathOps, - getCurveShape, - getEllipseShape, - getFreedrawShape, - getPolygonShape, - type GeometricShape, -} from "@excalidraw/utils/shape"; - -import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types"; - -import { shouldTestInside } from "./collision"; -import { LinearElementEditor } from "./linearElementEditor"; -import { getBoundTextElement } from "./textElement"; -import { ShapeCache } from "./ShapeCache"; - -import { getElementAbsoluteCoords, type Bounds } from "./bounds"; - -import { canBecomePolygon } from "./typeChecks"; - -import type { - ElementsMap, - ExcalidrawElement, - ExcalidrawLinearElement, - ExcalidrawLineElement, - NonDeleted, -} from "./types"; - -/** - * get the pure geometric shape of an excalidraw elementw - * which is then used for hit detection - */ -export const getElementShape = ( - element: ExcalidrawElement, - elementsMap: ElementsMap, -): GeometricShape => { - switch (element.type) { - case "rectangle": - case "diamond": - case "frame": - case "magicframe": - case "embeddable": - case "image": - case "iframe": - case "text": - case "selection": - return getPolygonShape(element); - case "arrow": - case "line": { - const roughShape = - ShapeCache.get(element)?.[0] ?? - ShapeCache.generateElementShape(element, null)[0]; - const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); - - return shouldTestInside(element) - ? getClosedCurveShape( - element, - roughShape, - pointFrom(element.x, element.y), - element.angle, - pointFrom(cx, cy), - ) - : getCurveShape( - roughShape, - pointFrom(element.x, element.y), - element.angle, - pointFrom(cx, cy), - ); - } - - case "ellipse": - return getEllipseShape(element); - - case "freedraw": { - const [, , , , cx, cy] = getElementAbsoluteCoords(element, elementsMap); - return getFreedrawShape( - element, - pointFrom(cx, cy), - shouldTestInside(element), - ); - } - } -}; - -export const getBoundTextShape = ( - element: ExcalidrawElement, - elementsMap: ElementsMap, -): GeometricShape | null => { - const boundTextElement = getBoundTextElement(element, elementsMap); - - if (boundTextElement) { - if (element.type === "arrow") { - return getElementShape( - { - ...boundTextElement, - // arrow's bound text accurate position is not stored in the element's property - // but rather calculated and returned from the following static method - ...LinearElementEditor.getBoundTextElementPosition( - element, - boundTextElement, - elementsMap, - ), - }, - elementsMap, - ); - } - return getElementShape(boundTextElement, elementsMap); - } - - return null; -}; - -export const getControlPointsForBezierCurve = < - P extends GlobalPoint | LocalPoint, ->( - element: NonDeleted, - endPoint: P, -) => { - const shape = ShapeCache.generateElementShape(element, null); - if (!shape) { - return null; - } - - const ops = getCurvePathOps(shape[0]); - let currentP = pointFrom

(0, 0); - let index = 0; - let minDistance = Infinity; - let controlPoints: P[] | null = null; - - while (index < ops.length) { - const { op, data } = ops[index]; - if (op === "move") { - invariant( - isPoint(data), - "The returned ops is not compatible with a point", - ); - currentP = pointFromPair(data); - } - if (op === "bcurveTo") { - const p0 = currentP; - const p1 = pointFrom

(data[0], data[1]); - const p2 = pointFrom

(data[2], data[3]); - const p3 = pointFrom

(data[4], data[5]); - const distance = pointDistance(p3, endPoint); - if (distance < minDistance) { - minDistance = distance; - controlPoints = [p0, p1, p2, p3]; - } - currentP = p3; - } - index++; - } - - return controlPoints; -}; - -export const getBezierXY =

( - p0: P, - p1: P, - p2: P, - p3: P, - t: number, -): P => { - const equation = (t: number, idx: number) => - Math.pow(1 - t, 3) * p3[idx] + - 3 * t * Math.pow(1 - t, 2) * p2[idx] + - 3 * Math.pow(t, 2) * (1 - t) * p1[idx] + - p0[idx] * Math.pow(t, 3); - const tx = equation(t, 0); - const ty = equation(t, 1); - return pointFrom(tx, ty); -}; - -const getPointsInBezierCurve =

( - element: NonDeleted, - endPoint: P, -) => { - const controlPoints: P[] = getControlPointsForBezierCurve(element, endPoint)!; - if (!controlPoints) { - return []; - } - const pointsOnCurve: P[] = []; - let t = 1; - // Take 20 points on curve for better accuracy - while (t > 0) { - const p = getBezierXY( - controlPoints[0], - controlPoints[1], - controlPoints[2], - controlPoints[3], - t, - ); - pointsOnCurve.push(pointFrom(p[0], p[1])); - t -= 0.05; - } - if (pointsOnCurve.length) { - if (pointsEqual(pointsOnCurve.at(-1)!, endPoint)) { - pointsOnCurve.push(pointFrom(endPoint[0], endPoint[1])); - } - } - return pointsOnCurve; -}; - -const getBezierCurveArcLengths =

( - element: NonDeleted, - endPoint: P, -) => { - const arcLengths: number[] = []; - arcLengths[0] = 0; - const points = getPointsInBezierCurve(element, endPoint); - let index = 0; - let distance = 0; - while (index < points.length - 1) { - const segmentDistance = pointDistance(points[index], points[index + 1]); - distance += segmentDistance; - arcLengths.push(distance); - index++; - } - - return arcLengths; -}; - -export const getBezierCurveLength =

( - element: NonDeleted, - endPoint: P, -) => { - const arcLengths = getBezierCurveArcLengths(element, endPoint); - return arcLengths.at(-1) as number; -}; - -// This maps interval to actual interval t on the curve so that when t = 0.5, its actually the point at 50% of the length -export const mapIntervalToBezierT =

( - element: NonDeleted, - endPoint: P, - interval: number, // The interval between 0 to 1 for which you want to find the point on the curve, -) => { - const arcLengths = getBezierCurveArcLengths(element, endPoint); - const pointsCount = arcLengths.length - 1; - const curveLength = arcLengths.at(-1) as number; - const targetLength = interval * curveLength; - let low = 0; - let high = pointsCount; - let index = 0; - // Doing a binary search to find the largest length that is less than the target length - while (low < high) { - index = Math.floor(low + (high - low) / 2); - if (arcLengths[index] < targetLength) { - low = index + 1; - } else { - high = index; - } - } - if (arcLengths[index] > targetLength) { - index--; - } - if (arcLengths[index] === targetLength) { - return index / pointsCount; - } - - return ( - 1 - - (index + - (targetLength - arcLengths[index]) / - (arcLengths[index + 1] - arcLengths[index])) / - pointsCount - ); -}; - -/** - * Get the axis-aligned bounding box for a given element - */ -export const aabbForElement = ( - element: Readonly, - elementsMap: ElementsMap, - offset?: [number, number, number, number], -) => { - const bbox = { - minX: element.x, - minY: element.y, - maxX: element.x + element.width, - maxY: element.y + element.height, - midX: element.x + element.width / 2, - midY: element.y + element.height / 2, - }; - - const center = elementCenterPoint(element, elementsMap); - const [topLeftX, topLeftY] = pointRotateRads( - pointFrom(bbox.minX, bbox.minY), - center, - element.angle, - ); - const [topRightX, topRightY] = pointRotateRads( - pointFrom(bbox.maxX, bbox.minY), - center, - element.angle, - ); - const [bottomRightX, bottomRightY] = pointRotateRads( - pointFrom(bbox.maxX, bbox.maxY), - center, - element.angle, - ); - const [bottomLeftX, bottomLeftY] = pointRotateRads( - pointFrom(bbox.minX, bbox.maxY), - center, - element.angle, - ); - - const bounds = [ - Math.min(topLeftX, topRightX, bottomRightX, bottomLeftX), - Math.min(topLeftY, topRightY, bottomRightY, bottomLeftY), - Math.max(topLeftX, topRightX, bottomRightX, bottomLeftX), - Math.max(topLeftY, topRightY, bottomRightY, bottomLeftY), - ] as Bounds; - - if (offset) { - const [topOffset, rightOffset, downOffset, leftOffset] = offset; - return [ - bounds[0] - leftOffset, - bounds[1] - topOffset, - bounds[2] + rightOffset, - bounds[3] + downOffset, - ] as Bounds; - } - - return bounds; -}; - -export const pointInsideBounds =

( - p: P, - bounds: Bounds, -): boolean => - p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; - -export const aabbsOverlapping = (a: Bounds, b: Bounds) => - pointInsideBounds(pointFrom(a[0], a[1]), b) || - pointInsideBounds(pointFrom(a[2], a[1]), b) || - pointInsideBounds(pointFrom(a[2], a[3]), b) || - pointInsideBounds(pointFrom(a[0], a[3]), b) || - pointInsideBounds(pointFrom(b[0], b[1]), a) || - pointInsideBounds(pointFrom(b[2], b[1]), a) || - pointInsideBounds(pointFrom(b[2], b[3]), a) || - pointInsideBounds(pointFrom(b[0], b[3]), a); - -export const getCornerRadius = (x: number, element: ExcalidrawElement) => { - if ( - element.roundness?.type === ROUNDNESS.PROPORTIONAL_RADIUS || - element.roundness?.type === ROUNDNESS.LEGACY - ) { - return x * DEFAULT_PROPORTIONAL_RADIUS; - } - - if (element.roundness?.type === ROUNDNESS.ADAPTIVE_RADIUS) { - const fixedRadiusSize = element.roundness?.value ?? DEFAULT_ADAPTIVE_RADIUS; - - const CUTOFF_SIZE = fixedRadiusSize / DEFAULT_PROPORTIONAL_RADIUS; - - if (x <= CUTOFF_SIZE) { - return x * DEFAULT_PROPORTIONAL_RADIUS; - } - - return fixedRadiusSize; - } - - return 0; -}; - -// Checks if the first and last point are close enough -// to be considered a loop -export const isPathALoop = ( - points: ExcalidrawLinearElement["points"], - /** supply if you want the loop detection to account for current zoom */ - zoomValue: Zoom["value"] = 1 as NormalizedZoomValue, -): boolean => { - if (points.length >= 3) { - const [first, last] = [points[0], points[points.length - 1]]; - const distance = pointDistance(first, last); - - // Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in - // really close we make the threshold smaller, and vice versa. - return distance <= LINE_CONFIRM_THRESHOLD / zoomValue; - } - return false; -}; - -export const toggleLinePolygonState = ( - element: ExcalidrawLineElement, - nextPolygonState: boolean, -): { - polygon: ExcalidrawLineElement["polygon"]; - points: ExcalidrawLineElement["points"]; -} | null => { - const updatedPoints = [...element.points]; - - if (nextPolygonState) { - if (!canBecomePolygon(element.points)) { - return null; - } - - const firstPoint = updatedPoints[0]; - const lastPoint = updatedPoints[updatedPoints.length - 1]; - - const distance = Math.hypot( - firstPoint[0] - lastPoint[0], - firstPoint[1] - lastPoint[1], - ); - - if ( - distance > LINE_POLYGON_POINT_MERGE_DISTANCE || - updatedPoints.length < 4 - ) { - updatedPoints.push(pointFrom(firstPoint[0], firstPoint[1])); - } else { - updatedPoints[updatedPoints.length - 1] = pointFrom( - firstPoint[0], - firstPoint[1], - ); - } - } - - // TODO: satisfies ElementUpdate - const ret = { - polygon: nextPolygonState, - points: updatedPoints, - }; - - return ret; -}; diff --git a/packages/element/src/textElement.ts b/packages/element/src/textElement.ts index 46b728158d..31347db240 100644 --- a/packages/element/src/textElement.ts +++ b/packages/element/src/textElement.ts @@ -326,10 +326,7 @@ export const getContainerCenter = ( if (!midSegmentMidpoint) { midSegmentMidpoint = LinearElementEditor.getSegmentMidPoint( container, - points[index], - points[index + 1], index + 1, - elementsMap, ); } return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index 673b9ef1b0..ca31639d5c 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -1,8 +1,16 @@ +import { + DEFAULT_ADAPTIVE_RADIUS, + DEFAULT_PROPORTIONAL_RADIUS, + LINE_CONFIRM_THRESHOLD, + ROUNDNESS, +} from "@excalidraw/common"; + import { curve, curveCatmullRomCubicApproxPoints, curveOffsetPoints, lineSegment, + pointDistance, pointFrom, pointFromArray, rectangle, @@ -11,14 +19,13 @@ import { import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math"; -import { getCornerRadius } from "./shapes"; +import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types"; import { getDiamondPoints } from "./bounds"; -import { generateLinearCollisionShape } from "./Shape"; +import { generateLinearCollisionShape } from "./shape"; import type { - ElementsMap, ExcalidrawDiamondElement, ExcalidrawElement, ExcalidrawFreeDrawElement, @@ -85,7 +92,6 @@ const setElementShapesCacheEntry = ( export function deconstructLinearOrFreeDrawElement( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, - elementsMap: ElementsMap, ): [LineSegment[], Curve[]] { const cachedShape = getElementShapesCacheEntry(element, 0); @@ -93,7 +99,7 @@ export function deconstructLinearOrFreeDrawElement( return cachedShape; } - const ops = generateLinearCollisionShape(element, elementsMap) as { + const ops = generateLinearCollisionShape(element) as { op: string; data: number[]; }[]; @@ -428,3 +434,44 @@ export function deconstructDiamondElement( return shape; } + +// Checks if the first and last point are close enough +// to be considered a loop +export const isPathALoop = ( + points: ExcalidrawLinearElement["points"], + /** supply if you want the loop detection to account for current zoom */ + zoomValue: Zoom["value"] = 1 as NormalizedZoomValue, +): boolean => { + if (points.length >= 3) { + const [first, last] = [points[0], points[points.length - 1]]; + const distance = pointDistance(first, last); + + // Adjusting LINE_CONFIRM_THRESHOLD to current zoom so that when zoomed in + // really close we make the threshold smaller, and vice versa. + return distance <= LINE_CONFIRM_THRESHOLD / zoomValue; + } + return false; +}; + +export const getCornerRadius = (x: number, element: ExcalidrawElement) => { + if ( + element.roundness?.type === ROUNDNESS.PROPORTIONAL_RADIUS || + element.roundness?.type === ROUNDNESS.LEGACY + ) { + return x * DEFAULT_PROPORTIONAL_RADIUS; + } + + if (element.roundness?.type === ROUNDNESS.ADAPTIVE_RADIUS) { + const fixedRadiusSize = element.roundness?.value ?? DEFAULT_ADAPTIVE_RADIUS; + + const CUTOFF_SIZE = fixedRadiusSize / DEFAULT_PROPORTIONAL_RADIUS; + + if (x <= CUTOFF_SIZE) { + return x * DEFAULT_PROPORTIONAL_RADIUS; + } + + return fixedRadiusSize; + } + + return 0; +}; diff --git a/packages/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index 8618154aba..4b957022c6 100644 --- a/packages/element/tests/linearElementEditor.test.tsx +++ b/packages/element/tests/linearElementEditor.test.tsx @@ -423,12 +423,12 @@ describe("Test Linear Elements", () => { expect(midPointsWithRoundEdge).toMatchInlineSnapshot(` [ [ - "55.96978", - "47.44233", + "54.27552", + "46.16120", ], [ - "76.08587", - "43.29417", + "76.95494", + "44.56052", ], ] `); @@ -488,12 +488,12 @@ describe("Test Linear Elements", () => { expect(newMidPoints).toMatchInlineSnapshot(` [ [ - "105.96978", - "67.44233", + "104.27552", + "66.16120", ], [ - "126.08587", - "63.29417", + "126.95494", + "64.56052", ], ] `); @@ -804,12 +804,12 @@ describe("Test Linear Elements", () => { expect(newMidPoints).toMatchInlineSnapshot(` [ [ - "31.88408", - "23.13276", + "29.28349", + "20.91105", ], [ - "77.74793", - "44.57841", + "78.86048", + "46.12277", ], ] `); @@ -893,12 +893,12 @@ describe("Test Linear Elements", () => { expect(newMidPoints).toMatchInlineSnapshot(` [ [ - "55.96978", - "47.44233", + "54.27552", + "46.16120", ], [ - "76.08587", - "43.29417", + "76.95494", + "44.56052", ], ] `); @@ -1060,8 +1060,8 @@ describe("Test Linear Elements", () => { ); expect(position).toMatchInlineSnapshot(` { - "x": "85.82202", - "y": "75.63461", + "x": "86.17305", + "y": "76.11251", } `); }); diff --git a/packages/excalidraw/actions/actionLinearEditor.tsx b/packages/excalidraw/actions/actionLinearEditor.tsx index 32eba33394..28295d9395 100644 --- a/packages/excalidraw/actions/actionLinearEditor.tsx +++ b/packages/excalidraw/actions/actionLinearEditor.tsx @@ -6,7 +6,10 @@ import { } from "@excalidraw/element"; import { arrayToMap } from "@excalidraw/common"; -import { CaptureUpdateAction } from "@excalidraw/element"; +import { + toggleLinePolygonState, + CaptureUpdateAction, +} from "@excalidraw/element"; import type { ExcalidrawLinearElement, @@ -22,8 +25,6 @@ import { ButtonIcon } from "../components/ButtonIcon"; import { newElementWith } from "../../element/src/mutateElement"; -import { toggleLinePolygonState } from "../../element/src/shapes"; - import { register } from "./register"; export const actionToggleLinearEditor = register({ diff --git a/packages/excalidraw/actions/actionProperties.tsx b/packages/excalidraw/actions/actionProperties.tsx index 62c6bae91b..63cfe76727 100644 --- a/packages/excalidraw/actions/actionProperties.tsx +++ b/packages/excalidraw/actions/actionProperties.tsx @@ -52,9 +52,11 @@ import { import { hasStrokeColor } from "@excalidraw/element"; -import { updateElbowArrowPoints } from "@excalidraw/element"; - -import { CaptureUpdateAction } from "@excalidraw/element"; +import { + updateElbowArrowPoints, + CaptureUpdateAction, + toggleLinePolygonState, +} from "@excalidraw/element"; import type { LocalPoint } from "@excalidraw/math"; @@ -135,8 +137,6 @@ import { isSomeElementSelected, } from "../scene"; -import { toggleLinePolygonState } from "../../element/src/shapes"; - import { register } from "./register"; import type { AppClassProperties, AppState, Primitive } from "../types"; diff --git a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap index 1bc3ce9253..f00a51817d 100644 --- a/packages/excalidraw/data/__snapshots__/transform.test.ts.snap +++ b/packages/excalidraw/data/__snapshots__/transform.test.ts.snap @@ -1791,7 +1791,7 @@ exports[`Test Transform > should transform the elements correctly when linear el "versionNonce": Any, "verticalAlign": "middle", "width": 120, - "x": 187.7545, + "x": 187.75450000000004, "y": 44.5, } `; diff --git a/packages/math/src/constants.ts b/packages/math/src/constants.ts new file mode 100644 index 0000000000..ce39e42682 --- /dev/null +++ b/packages/math/src/constants.ts @@ -0,0 +1,57 @@ +export const PRECISION = 10e-5; + +// Legendre-Gauss abscissae (x values) and weights for n=24 +// Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html +export const LegendreGaussN24TValues = [ + -0.0640568928626056260850430826247450385909, + 0.0640568928626056260850430826247450385909, + -0.1911188674736163091586398207570696318404, + 0.1911188674736163091586398207570696318404, + -0.3150426796961633743867932913198102407864, + 0.3150426796961633743867932913198102407864, + -0.4337935076260451384870842319133497124524, + 0.4337935076260451384870842319133497124524, + -0.5454214713888395356583756172183723700107, + 0.5454214713888395356583756172183723700107, + -0.6480936519369755692524957869107476266696, + 0.6480936519369755692524957869107476266696, + -0.7401241915785543642438281030999784255232, + 0.7401241915785543642438281030999784255232, + -0.8200019859739029219539498726697452080761, + 0.8200019859739029219539498726697452080761, + -0.8864155270044010342131543419821967550873, + 0.8864155270044010342131543419821967550873, + -0.9382745520027327585236490017087214496548, + 0.9382745520027327585236490017087214496548, + -0.9747285559713094981983919930081690617411, + 0.9747285559713094981983919930081690617411, + -0.9951872199970213601799974097007368118745, + 0.9951872199970213601799974097007368118745, +]; + +export const LegendreGaussN24CValues = [ + 0.1279381953467521569740561652246953718517, + 0.1279381953467521569740561652246953718517, + 0.1258374563468282961213753825111836887264, + 0.1258374563468282961213753825111836887264, + 0.121670472927803391204463153476262425607, + 0.121670472927803391204463153476262425607, + 0.1155056680537256013533444839067835598622, + 0.1155056680537256013533444839067835598622, + 0.1074442701159656347825773424466062227946, + 0.1074442701159656347825773424466062227946, + 0.0976186521041138882698806644642471544279, + 0.0976186521041138882698806644642471544279, + 0.086190161531953275917185202983742667185, + 0.086190161531953275917185202983742667185, + 0.0733464814110803057340336152531165181193, + 0.0733464814110803057340336152531165181193, + 0.0592985849154367807463677585001085845412, + 0.0592985849154367807463677585001085845412, + 0.0442774388174198061686027482113382288593, + 0.0442774388174198061686027482113382288593, + 0.0285313886289336631813078159518782864491, + 0.0285313886289336631813078159518782864491, + 0.0123412297999871995468056670700372915759, + 0.0123412297999871995468056670700372915759, +]; diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 26ab690e9c..614f072504 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -2,6 +2,7 @@ import { doBoundsIntersect, type Bounds } from "@excalidraw/element"; import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; +import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -406,3 +407,123 @@ export function offsetPointsForQuadraticBezier( return offsetPoints; } + +/** + * Implementation based on Legendre-Gauss quadrature for more accurate arc + * length calculation. + * + * Reference: https://pomax.github.io/bezierinfo/#arclength + * + * @param c The curve to calculate the length of + * @returns The approximated length of the curve + */ +export function curveLength

( + c: Curve

, +): number { + const z2 = 0.5; + let sum = 0; + + for (let i = 0; i < 24; i++) { + const t = z2 * LegendreGaussN24TValues[i] + z2; + const derivativeVector = curveTangent(c, t); + const magnitude = Math.sqrt( + derivativeVector[0] * derivativeVector[0] + + derivativeVector[1] * derivativeVector[1], + ); + sum += LegendreGaussN24CValues[i] * magnitude; + } + + return z2 * sum; +} + +/** + * Calculates the curve length from t=0 to t=parameter using the same + * Legendre-Gauss quadrature method used in curveLength + * + * @param c The curve to calculate the partial length for + * @param t The parameter value (0 to 1) to calculate length up to + * @returns The length of the curve from beginning to parameter t + */ +export function curveLengthAtParameter

( + c: Curve

, + t: number, +): number { + if (t <= 0) { + return 0; + } + if (t >= 1) { + return curveLength(c); + } + + // Scale and shift the integration interval from [0,t] to [-1,1] + // which is what the Legendre-Gauss quadrature expects + const z1 = t / 2; + const z2 = t / 2; + + let sum = 0; + + for (let i = 0; i < 24; i++) { + const parameter = z1 * LegendreGaussN24TValues[i] + z2; + const derivativeVector = curveTangent(c, parameter); + const magnitude = Math.sqrt( + derivativeVector[0] * derivativeVector[0] + + derivativeVector[1] * derivativeVector[1], + ); + sum += LegendreGaussN24CValues[i] * magnitude; + } + + return z1 * sum; // Scale the result back to the original interval +} + +/** + * Calculates the point at a specific percentage of a curve's total length + * using binary search for improved efficiency and accuracy. + * + * @param c The curve to calculate point on + * @param percent A value between 0 and 1 representing the percentage of the curve's length + * @returns The point at the specified percentage of curve length + */ +export function curvePointAtLength

( + c: Curve

, + percent: number, +): P { + if (percent <= 0) { + return bezierEquation(c, 0); + } + + if (percent >= 1) { + return bezierEquation(c, 1); + } + + const totalLength = curveLength(c); + const targetLength = totalLength * percent; + + // Binary search to find parameter t where length at t equals target length + let tMin = 0; + let tMax = 1; + let t = percent; // Start with a reasonable guess (t = percent) + let currentLength = 0; + + // Tolerance for length comparison and iteration limit to avoid infinite loops + const tolerance = totalLength * 0.0001; + const maxIterations = 20; + + for (let iteration = 0; iteration < maxIterations; iteration++) { + currentLength = curveLengthAtParameter(c, t); + const error = Math.abs(currentLength - targetLength); + + if (error < tolerance) { + break; + } + + if (currentLength < targetLength) { + tMin = t; + } else { + tMax = t; + } + + t = (tMin + tMax) / 2; + } + + return bezierEquation(c, t); +} From 60512f13d51a6440809cf6f9d2ce84a76f74038c Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Sat, 14 Jun 2025 12:29:58 +0200 Subject: [PATCH 13/22] Fix broken history when eleemnt in update scene are optional --- packages/excalidraw/components/App.tsx | 3 +- .../tests/__snapshots__/history.test.tsx.snap | 152 ++++++++++++++++++ packages/excalidraw/tests/history.test.tsx | 31 ++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index beca285e1c..9b3fe0d8bd 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -3907,6 +3907,7 @@ class App extends React.Component { const { elements, appState, collaborators, captureUpdate } = sceneData; if (captureUpdate) { + const nextElements = elements ? elements : undefined; const observedAppState = appState ? getObservedAppState({ ...this.store.snapshot.appState, @@ -3916,7 +3917,7 @@ class App extends React.Component { this.store.scheduleMicroAction({ action: captureUpdate, - elements: elements ?? [], + elements: nextElements, appState: observedAppState, }); } diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 4d590b3b04..22b8519dc0 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -14820,6 +14820,158 @@ exports[`history > singleplayer undo/redo > should not end up with history entry ] `; +exports[`history > singleplayer undo/redo > should not modify anything on unrelated appstate change > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeLockedId": null, + "activeTool": { + "customType": null, + "fromSelection": false, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "croppingElementId": null, + "currentChartType": "bar", + "currentHoveredFontFamily": null, + "currentItemArrowType": "round", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "solid", + "currentItemFontFamily": 5, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "sharp", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 2, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "editingTextElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "followedBy": Set {}, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridModeEnabled": false, + "gridSize": 20, + "gridStep": 5, + "height": 0, + "hoveredElementIds": {}, + "isBindingEnabled": true, + "isCropping": false, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "lockedMultiSelections": {}, + "multiElement": null, + "newElement": null, + "objectsSnapModeEnabled": false, + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "originSnapOffset": { + "x": 0, + "y": 0, + }, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "searchMatches": null, + "selectedElementIds": {}, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": {}, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showWelcomeScreen": false, + "snapLines": [], + "startBoundElement": null, + "stats": { + "open": false, + "panels": 3, + }, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "userToFollow": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": true, + "width": 0, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`history > singleplayer undo/redo > should not modify anything on unrelated appstate change > [end of test] element 0 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [], + "customData": undefined, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": "id0", + "index": "a0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 2, + "width": 100, + "x": 0, + "y": 0, +} +`; + +exports[`history > singleplayer undo/redo > should not modify anything on unrelated appstate change > [end of test] number of elements 1`] = `1`; + +exports[`history > singleplayer undo/redo > should not modify anything on unrelated appstate change > [end of test] number of renders 1`] = `4`; + +exports[`history > singleplayer undo/redo > should not modify anything on unrelated appstate change > [end of test] redo stack 1`] = `[]`; + +exports[`history > singleplayer undo/redo > should not modify anything on unrelated appstate change > [end of test] undo stack 1`] = `[]`; + exports[`history > singleplayer undo/redo > should not override appstate changes when redo stack is not cleared > [end of test] appState 1`] = ` { "activeEmbeddable": null, diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index c2da254543..be7c5821f4 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -243,6 +243,37 @@ describe("history", () => { ]); }); + it("should not modify anything on unrelated appstate change", async () => { + const rect = API.createElement({ type: "rectangle" }); + await render( + , + ); + + API.updateScene({ + appState: { + viewModeEnabled: true, + }, + captureUpdate: CaptureUpdateAction.NEVER, + }); + + await waitFor(() => { + expect(h.state.viewModeEnabled).toBe(true); + expect(API.getUndoStack().length).toBe(0); + expect(API.getRedoStack().length).toBe(0); + expect(h.elements).toEqual([ + expect.objectContaining({ id: rect.id, isDeleted: false }), + ]); + expect(h.store.snapshot.elements.get(rect.id)).toEqual( + expect.objectContaining({ id: rect.id, isDeleted: false }), + ); + }); + }); + it("should not clear the redo stack on standalone appstate change", async () => { await render(); From 320af405e9578d67443c6261d42fecb8d46e38ae Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 14 Jun 2025 12:49:22 +0200 Subject: [PATCH 14/22] fix: move elementCenterPoint from common/src/utils.ts to element/src/bounds.ts (#9647) move elementCenterPoint from utils to bounds.ts --- packages/common/src/utils.ts | 16 +--------------- packages/element/src/binding.ts | 3 +-- packages/element/src/bounds.ts | 12 +++++++++++- packages/element/src/collision.ts | 3 ++- packages/element/src/cropElement.ts | 3 +-- packages/element/src/distance.ts | 4 ++-- packages/excalidraw/renderer/helpers.ts | 4 ++-- packages/excalidraw/tests/helpers/ui.ts | 8 ++++++-- 8 files changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/common/src/utils.ts b/packages/common/src/utils.ts index 2baa434946..1054960650 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -1,12 +1,9 @@ -import { average, pointFrom, type GlobalPoint } from "@excalidraw/math"; -import { getCenterForBounds, getElementBounds } from "@excalidraw/element"; +import { average } from "@excalidraw/math"; import type { ExcalidrawBindableElement, FontFamilyValues, FontString, - ExcalidrawElement, - ElementsMap, } from "@excalidraw/element/types"; import type { @@ -1239,17 +1236,6 @@ export const escapeDoubleQuotes = (str: string) => { export const castArray = (value: T | T[]): T[] => Array.isArray(value) ? value : [value]; -export const elementCenterPoint = ( - element: ExcalidrawElement, - elementsMap: ElementsMap, - xOffset: number = 0, - yOffset: number = 0, -) => { - const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap)); - - return pointFrom(x + xOffset, y + yOffset); -}; - /** hack for Array.isArray type guard not working with readonly value[] */ export const isReadonlyArray = (value?: any): value is readonly any[] => { return Array.isArray(value); diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 16f3216616..82bb97e79d 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -6,7 +6,6 @@ import { invariant, isDevEnv, isTestEnv, - elementCenterPoint, } from "@excalidraw/common"; import { @@ -61,7 +60,7 @@ import { isTextElement, } from "./typeChecks"; -import { aabbForElement } from "./bounds"; +import { aabbForElement, elementCenterPoint } from "./bounds"; import { updateElbowArrowPoints } from "./elbowArrow"; import type { Scene } from "./Scene"; diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index 2a7b3fb25c..d5a6e300d5 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -2,7 +2,6 @@ import rough from "roughjs/bin/rough"; import { arrayToMap, - elementCenterPoint, invariant, rescalePoints, sizeOf, @@ -1244,3 +1243,14 @@ export const pointInsideBounds =

( bounds: Bounds, ): boolean => p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; + +export const elementCenterPoint = ( + element: ExcalidrawElement, + elementsMap: ElementsMap, + xOffset: number = 0, + yOffset: number = 0, +) => { + const [x, y] = getCenterForBounds(getElementBounds(element, elementsMap)); + + return pointFrom(x + xOffset, y + yOffset); +}; diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 88e96a15ad..008b50cc9c 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -1,4 +1,4 @@ -import { isTransparent, elementCenterPoint } from "@excalidraw/common"; +import { isTransparent } from "@excalidraw/common"; import { curveIntersectLineSegment, isPointWithinBounds, @@ -26,6 +26,7 @@ import { isPathALoop } from "./utils"; import { type Bounds, doBoundsIntersect, + elementCenterPoint, getCenterForBounds, getElementBounds, } from "./bounds"; diff --git a/packages/element/src/cropElement.ts b/packages/element/src/cropElement.ts index c2a9f91fdb..3803944caf 100644 --- a/packages/element/src/cropElement.ts +++ b/packages/element/src/cropElement.ts @@ -14,9 +14,8 @@ import { } from "@excalidraw/math"; import { type Point } from "points-on-curve"; -import { elementCenterPoint } from "@excalidraw/common"; - import { + elementCenterPoint, getElementAbsoluteCoords, getResizedElementAbsoluteCoords, } from "./bounds"; diff --git a/packages/element/src/distance.ts b/packages/element/src/distance.ts index ed900ecc69..4766ac9eef 100644 --- a/packages/element/src/distance.ts +++ b/packages/element/src/distance.ts @@ -6,8 +6,6 @@ import { import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse"; -import { elementCenterPoint } from "@excalidraw/common"; - import type { GlobalPoint, Radians } from "@excalidraw/math"; import { @@ -16,6 +14,8 @@ import { deconstructRectanguloidElement, } from "./utils"; +import { elementCenterPoint } from "./bounds"; + import type { ElementsMap, ExcalidrawDiamondElement, diff --git a/packages/excalidraw/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts index c94bcd8eb6..d357822ec6 100644 --- a/packages/excalidraw/renderer/helpers.ts +++ b/packages/excalidraw/renderer/helpers.ts @@ -1,8 +1,8 @@ -import { elementCenterPoint, THEME, THEME_FILTER } from "@excalidraw/common"; +import { THEME, THEME_FILTER } from "@excalidraw/common"; import { FIXED_BINDING_DISTANCE } from "@excalidraw/element"; import { getDiamondPoints } from "@excalidraw/element"; -import { getCornerRadius } from "@excalidraw/element"; +import { elementCenterPoint, getCornerRadius } from "@excalidraw/element"; import { curve, diff --git a/packages/excalidraw/tests/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index 3188c5ada6..80a7e4c561 100644 --- a/packages/excalidraw/tests/helpers/ui.ts +++ b/packages/excalidraw/tests/helpers/ui.ts @@ -1,6 +1,10 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math"; -import { getCommonBounds, getElementPointsCoords } from "@excalidraw/element"; +import { + elementCenterPoint, + getCommonBounds, + getElementPointsCoords, +} from "@excalidraw/element"; import { cropElement } from "@excalidraw/element"; import { getTransformHandles, @@ -16,7 +20,7 @@ import { isTextElement, isFrameLikeElement, } from "@excalidraw/element"; -import { KEYS, arrayToMap, elementCenterPoint } from "@excalidraw/common"; +import { KEYS, arrayToMap } from "@excalidraw/common"; import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; From 84e96e939358a559bcef3863670bcf55874cd14b Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 14 Jun 2025 13:01:30 +0200 Subject: [PATCH 15/22] fix: move doBoundsIntersect from element/src/bounds.ts to common/math/src/utils.ts (#9650) move doBoundsIntersect to math/utils Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/binding.ts | 7 ++----- packages/element/src/bounds.ts | 14 -------------- packages/element/src/collision.ts | 2 +- packages/excalidraw/lasso/utils.ts | 2 +- packages/math/src/curve.ts | 3 ++- packages/math/src/utils.ts | 16 ++++++++++++++++ 6 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 82bb97e79d..af66ebd9fe 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -24,6 +24,7 @@ import { pointsEqual, lineSegmentIntersectionPoints, PRECISION, + doBoundsIntersect, } from "@excalidraw/math"; import type { LocalPoint, Radians } from "@excalidraw/math"; @@ -32,11 +33,7 @@ import type { AppState } from "@excalidraw/excalidraw/types"; import type { MapEntry, Mutable } from "@excalidraw/common/utility-types"; -import { - getCenterForBounds, - getElementBounds, - doBoundsIntersect, -} from "./bounds"; +import { getCenterForBounds, getElementBounds } from "./bounds"; import { intersectElementWithLineSegment } from "./collision"; import { distanceToElement } from "./distance"; import { diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index d5a6e300d5..260a9bd4ed 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -1165,20 +1165,6 @@ export const getCenterForBounds = (bounds: Bounds): GlobalPoint => bounds[1] + (bounds[3] - bounds[1]) / 2, ); -export const doBoundsIntersect = ( - bounds1: Bounds | null, - bounds2: Bounds | null, -): boolean => { - if (bounds1 == null || bounds2 == null) { - return false; - } - - const [minX1, minY1, maxX1, maxY1] = bounds1; - const [minX2, minY2, maxX2, maxY2] = bounds2; - - return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; -}; - /** * Get the axis-aligned bounding box for a given element */ diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 008b50cc9c..5ae7753e63 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -11,6 +11,7 @@ import { vectorFromPoint, vectorNormalize, vectorScale, + doBoundsIntersect, } from "@excalidraw/math"; import { @@ -25,7 +26,6 @@ import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; import { isPathALoop } from "./utils"; import { type Bounds, - doBoundsIntersect, elementCenterPoint, getCenterForBounds, getElementBounds, diff --git a/packages/excalidraw/lasso/utils.ts b/packages/excalidraw/lasso/utils.ts index 2cab64662b..8256f0069a 100644 --- a/packages/excalidraw/lasso/utils.ts +++ b/packages/excalidraw/lasso/utils.ts @@ -4,12 +4,12 @@ import { polygonFromPoints, lineSegment, polygonIncludesPointNonZero, + doBoundsIntersect, } from "@excalidraw/math"; import { type Bounds, computeBoundTextPosition, - doBoundsIntersect, getBoundTextElement, getElementBounds, intersectElementWithLineSegment, diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 614f072504..3323456b51 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,8 +1,9 @@ -import { doBoundsIntersect, type Bounds } from "@excalidraw/element"; +import { type Bounds } from "@excalidraw/element"; import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants"; +import { doBoundsIntersect } from "./utils"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; diff --git a/packages/math/src/utils.ts b/packages/math/src/utils.ts index 8807c275e4..912ddd1358 100644 --- a/packages/math/src/utils.ts +++ b/packages/math/src/utils.ts @@ -1,3 +1,5 @@ +import { type Bounds } from "@excalidraw/element"; + export const PRECISION = 10e-5; export const clamp = (value: number, min: number, max: number) => { @@ -31,3 +33,17 @@ export const isFiniteNumber = (value: any): value is number => { export const isCloseTo = (a: number, b: number, precision = PRECISION) => Math.abs(a - b) < precision; + +export const doBoundsIntersect = ( + bounds1: Bounds | null, + bounds2: Bounds | null, +): boolean => { + if (bounds1 == null || bounds2 == null) { + return false; + } + + const [minX1, minY1, maxX1, maxY1] = bounds1; + const [minX2, minY2, maxX2, maxY2] = bounds2; + + return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; +}; From 93c92d13e9ba6467a63292dabd335df7691cf45c Mon Sep 17 00:00:00 2001 From: Ryan Di Date: Sat, 14 Jun 2025 21:05:24 +1000 Subject: [PATCH 16/22] feat: wrap texts from stats panel (#9552) --- packages/element/src/resizeElements.ts | 241 ++++-------------- .../components/Stats/stats.test.tsx | 25 +- packages/excalidraw/components/Stats/utils.ts | 8 +- 3 files changed, 74 insertions(+), 200 deletions(-) diff --git a/packages/element/src/resizeElements.ts b/packages/element/src/resizeElements.ts index 96a053226a..acb72b299b 100644 --- a/packages/element/src/resizeElements.ts +++ b/packages/element/src/resizeElements.ts @@ -2,7 +2,6 @@ import { pointCenter, normalizeRadians, pointFrom, - pointFromPair, pointRotateRads, type Radians, type LocalPoint, @@ -104,18 +103,6 @@ export const transformElements = ( ); updateBoundElements(element, scene); } - } else if (isTextElement(element) && transformHandleType) { - resizeSingleTextElement( - originalElements, - element, - scene, - transformHandleType, - shouldResizeFromCenter, - pointerX, - pointerY, - ); - updateBoundElements(element, scene); - return true; } else if (transformHandleType) { const elementId = selectedElements[0].id; const latestElement = elementsMap.get(elementId); @@ -150,6 +137,9 @@ export const transformElements = ( ); } } + if (isTextElement(element)) { + updateBoundElements(element, scene); + } return true; } else if (selectedElements.length > 1) { if (transformHandleType === "rotation") { @@ -282,151 +272,50 @@ export const measureFontSizeFromWidth = ( }; }; -const resizeSingleTextElement = ( - originalElements: PointerDownState["originalElements"], +export const resizeSingleTextElement = ( + origElement: NonDeleted, element: NonDeleted, scene: Scene, transformHandleType: TransformHandleDirection, shouldResizeFromCenter: boolean, - pointerX: number, - pointerY: number, + nextWidth: number, + nextHeight: number, ) => { const elementsMap = scene.getNonDeletedElementsMap(); - const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords( - element, - elementsMap, - ); - // rotation pointer with reverse angle - const [rotatedX, rotatedY] = pointRotateRads( - pointFrom(pointerX, pointerY), - pointFrom(cx, cy), - -element.angle as Radians, - ); - let scaleX = 0; - let scaleY = 0; - if (transformHandleType !== "e" && transformHandleType !== "w") { - if (transformHandleType.includes("e")) { - scaleX = (rotatedX - x1) / (x2 - x1); - } - if (transformHandleType.includes("w")) { - scaleX = (x2 - rotatedX) / (x2 - x1); - } - if (transformHandleType.includes("n")) { - scaleY = (y2 - rotatedY) / (y2 - y1); - } - if (transformHandleType.includes("s")) { - scaleY = (rotatedY - y1) / (y2 - y1); - } + const metricsWidth = element.width * (nextHeight / element.height); + + const metrics = measureFontSizeFromWidth(element, elementsMap, metricsWidth); + if (metrics === null) { + return; } - const scale = Math.max(scaleX, scaleY); + if (transformHandleType.includes("n") || transformHandleType.includes("s")) { + const previousOrigin = pointFrom(origElement.x, origElement.y); - if (scale > 0) { - const nextWidth = element.width * scale; - const nextHeight = element.height * scale; - const metrics = measureFontSizeFromWidth(element, elementsMap, nextWidth); - if (metrics === null) { - return; - } - - const startTopLeft = [x1, y1]; - const startBottomRight = [x2, y2]; - const startCenter = [cx, cy]; - - let newTopLeft = pointFrom(x1, y1); - if (["n", "w", "nw"].includes(transformHandleType)) { - newTopLeft = pointFrom( - startBottomRight[0] - Math.abs(nextWidth), - startBottomRight[1] - Math.abs(nextHeight), - ); - } - if (transformHandleType === "ne") { - const bottomLeft = [startTopLeft[0], startBottomRight[1]]; - newTopLeft = pointFrom( - bottomLeft[0], - bottomLeft[1] - Math.abs(nextHeight), - ); - } - if (transformHandleType === "sw") { - const topRight = [startBottomRight[0], startTopLeft[1]]; - newTopLeft = pointFrom( - topRight[0] - Math.abs(nextWidth), - topRight[1], - ); - } - - if (["s", "n"].includes(transformHandleType)) { - newTopLeft[0] = startCenter[0] - nextWidth / 2; - } - if (["e", "w"].includes(transformHandleType)) { - newTopLeft[1] = startCenter[1] - nextHeight / 2; - } - - if (shouldResizeFromCenter) { - newTopLeft[0] = startCenter[0] - Math.abs(nextWidth) / 2; - newTopLeft[1] = startCenter[1] - Math.abs(nextHeight) / 2; - } - - const angle = element.angle; - const rotatedTopLeft = pointRotateRads( - newTopLeft, - pointFrom(cx, cy), - angle, + const newOrigin = getResizedOrigin( + previousOrigin, + origElement.width, + origElement.height, + metricsWidth, + nextHeight, + origElement.angle, + transformHandleType, + false, + shouldResizeFromCenter, ); - const newCenter = pointFrom( - newTopLeft[0] + Math.abs(nextWidth) / 2, - newTopLeft[1] + Math.abs(nextHeight) / 2, - ); - const rotatedNewCenter = pointRotateRads( - newCenter, - pointFrom(cx, cy), - angle, - ); - newTopLeft = pointRotateRads( - rotatedTopLeft, - rotatedNewCenter, - -angle as Radians, - ); - const [nextX, nextY] = newTopLeft; scene.mutateElement(element, { fontSize: metrics.size, - width: nextWidth, + width: metricsWidth, height: nextHeight, - x: nextX, - y: nextY, + x: newOrigin.x, + y: newOrigin.y, }); + return; } if (transformHandleType === "e" || transformHandleType === "w") { - const stateAtResizeStart = originalElements.get(element.id)!; - const [x1, y1, x2, y2] = getResizedElementAbsoluteCoords( - stateAtResizeStart, - stateAtResizeStart.width, - stateAtResizeStart.height, - true, - ); - const startTopLeft = pointFrom(x1, y1); - const startBottomRight = pointFrom(x2, y2); - const startCenter = pointCenter(startTopLeft, startBottomRight); - - const rotatedPointer = pointRotateRads( - pointFrom(pointerX, pointerY), - startCenter, - -stateAtResizeStart.angle as Radians, - ); - - const [esx1, , esx2] = getResizedElementAbsoluteCoords( - element, - element.width, - element.height, - true, - ); - - const boundsCurrentWidth = esx2 - esx1; - - const atStartBoundsWidth = startBottomRight[0] - startTopLeft[0]; const minWidth = getMinTextElementWidth( getFontString({ fontSize: element.fontSize, @@ -435,17 +324,7 @@ const resizeSingleTextElement = ( element.lineHeight, ); - let scaleX = atStartBoundsWidth / boundsCurrentWidth; - - if (transformHandleType.includes("e")) { - scaleX = (rotatedPointer[0] - startTopLeft[0]) / boundsCurrentWidth; - } - if (transformHandleType.includes("w")) { - scaleX = (startBottomRight[0] - rotatedPointer[0]) / boundsCurrentWidth; - } - - const newWidth = - element.width * scaleX < minWidth ? minWidth : element.width * scaleX; + const newWidth = Math.max(minWidth, nextWidth); const text = wrapText( element.originalText, @@ -458,49 +337,27 @@ const resizeSingleTextElement = ( element.lineHeight, ); - const eleNewHeight = metrics.height; + const newHeight = metrics.height; - const [newBoundsX1, newBoundsY1, newBoundsX2, newBoundsY2] = - getResizedElementAbsoluteCoords( - stateAtResizeStart, - newWidth, - eleNewHeight, - true, - ); - const newBoundsWidth = newBoundsX2 - newBoundsX1; - const newBoundsHeight = newBoundsY2 - newBoundsY1; + const previousOrigin = pointFrom(origElement.x, origElement.y); - let newTopLeft = [...startTopLeft] as [number, number]; - if (["n", "w", "nw"].includes(transformHandleType)) { - newTopLeft = [ - startBottomRight[0] - Math.abs(newBoundsWidth), - startTopLeft[1], - ]; - } - - // adjust topLeft to new rotation point - const angle = stateAtResizeStart.angle; - const rotatedTopLeft = pointRotateRads( - pointFromPair(newTopLeft), - startCenter, - angle, - ); - const newCenter = pointFrom( - newTopLeft[0] + Math.abs(newBoundsWidth) / 2, - newTopLeft[1] + Math.abs(newBoundsHeight) / 2, - ); - const rotatedNewCenter = pointRotateRads(newCenter, startCenter, angle); - newTopLeft = pointRotateRads( - rotatedTopLeft, - rotatedNewCenter, - -angle as Radians, + const newOrigin = getResizedOrigin( + previousOrigin, + origElement.width, + origElement.height, + newWidth, + newHeight, + element.angle, + transformHandleType, + false, + shouldResizeFromCenter, ); const resizedElement: Partial = { width: Math.abs(newWidth), height: Math.abs(metrics.height), - x: newTopLeft[0], - y: newTopLeft[1], + x: newOrigin.x, + y: newOrigin.y, text, autoResize: false, }; @@ -821,6 +678,18 @@ export const resizeSingleElement = ( shouldInformMutation?: boolean; } = {}, ) => { + if (isTextElement(latestElement) && isTextElement(origElement)) { + return resizeSingleTextElement( + origElement, + latestElement, + scene, + handleDirection, + shouldResizeFromCenter, + nextWidth, + nextHeight, + ); + } + let boundTextFont: { fontSize?: number } = {}; const elementsMap = scene.getNonDeletedElementsMap(); const boundTextElement = getBoundTextElement(latestElement, elementsMap); diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index 902848f97e..40cb598a0e 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -401,11 +401,23 @@ describe("stats for a non-generic element", () => { UI.updateInput(input, "36"); expect(text.fontSize).toBe(36); - // cannot change width or height - const width = UI.queryStatsProperty("W")?.querySelector(".drag-input"); - expect(width).toBeUndefined(); - const height = UI.queryStatsProperty("H")?.querySelector(".drag-input"); - expect(height).toBeUndefined(); + // can change width or height + const width = UI.queryStatsProperty("W")?.querySelector( + ".drag-input", + ) as HTMLInputElement; + expect(width).toBeDefined(); + const height = UI.queryStatsProperty("H")?.querySelector( + ".drag-input", + ) as HTMLInputElement; + expect(height).toBeDefined(); + + const textHeightBeforeWrapping = text.height; + const textBeforeWrapping = text.text; + const originalTextBeforeWrapping = textBeforeWrapping; + UI.updateInput(width, "30"); + expect(text.height).toBeGreaterThan(textHeightBeforeWrapping); + expect(text.text).not.toBe(textBeforeWrapping); + expect(text.originalText).toBe(originalTextBeforeWrapping); // min font size is 4 UI.updateInput(input, "0"); @@ -627,12 +639,11 @@ describe("stats for multiple elements", () => { ) as HTMLInputElement; expect(fontSize).toBeDefined(); - // changing width does not affect text UI.updateInput(width, "200"); expect(rectangle?.width).toBe(200); expect(frame.width).toBe(200); - expect(text?.width).not.toBe(200); + expect(text?.width).toBe(200); UI.updateInput(angle, "40"); diff --git a/packages/excalidraw/components/Stats/utils.ts b/packages/excalidraw/components/Stats/utils.ts index f07a53dfe1..68d2020987 100644 --- a/packages/excalidraw/components/Stats/utils.ts +++ b/packages/excalidraw/components/Stats/utils.ts @@ -1,7 +1,7 @@ import { pointFrom, pointRotateRads } from "@excalidraw/math"; import { getBoundTextElement } from "@excalidraw/element"; -import { isFrameLikeElement, isTextElement } from "@excalidraw/element"; +import { isFrameLikeElement } from "@excalidraw/element"; import { getSelectedGroupIds, @@ -41,12 +41,6 @@ export const isPropertyEditable = ( element: ExcalidrawElement, property: keyof ExcalidrawElement, ) => { - if (property === "height" && isTextElement(element)) { - return false; - } - if (property === "width" && isTextElement(element)) { - return false; - } if (property === "angle" && isFrameLikeElement(element)) { return false; } From 3f194918e6ce1dd20de206a285b2770fbcde28df Mon Sep 17 00:00:00 2001 From: Spawn Date: Mon, 16 Jun 2025 02:11:37 +0800 Subject: [PATCH 17/22] feat: add mulitplatform Docker image support (#9594) --- .github/workflows/publish-docker.yml | 7 ++++++- Dockerfile | 9 +++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/publish-docker.yml b/.github/workflows/publish-docker.yml index a4a8a4c5ff..68eee27755 100644 --- a/.github/workflows/publish-docker.yml +++ b/.github/workflows/publish-docker.yml @@ -17,9 +17,14 @@ jobs: with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - name: Build and push - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: true tags: excalidraw/excalidraw:latest + platforms: linux/amd64, linux/arm64, linux/arm/v7 diff --git a/Dockerfile b/Dockerfile index 2716803fa6..c08385d654 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:18 AS build +FROM --platform=${BUILDPLATFORM} node:18 AS build WORKDIR /opt/node_app @@ -6,13 +6,14 @@ COPY . . # do not ignore optional dependencies: # Error: Cannot find module @rollup/rollup-linux-x64-gnu -RUN yarn --network-timeout 600000 +RUN --mount=type=cache,target=/root/.cache/yarn \ + npm_config_target_arch=${TARGETARCH} yarn --network-timeout 600000 ARG NODE_ENV=production -RUN yarn build:app:docker +RUN npm_config_target_arch=${TARGETARCH} yarn build:app:docker -FROM nginx:1.27-alpine +FROM --platform=${TARGETPLATFORM} nginx:1.27-alpine COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html From 058918f8e5406d036fff76bab9bbb2b5281547c9 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Sun, 15 Jun 2025 23:43:14 +0200 Subject: [PATCH 18/22] feat: capture images after they initialize (#9643) Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/store.ts | 14 +- packages/excalidraw/components/App.tsx | 250 +++++++----------- .../tests/__snapshots__/history.test.tsx.snap | 12 +- packages/excalidraw/tests/flip.test.tsx | 24 ++ packages/excalidraw/tests/history.test.tsx | 25 ++ 5 files changed, 164 insertions(+), 161 deletions(-) diff --git a/packages/element/src/store.ts b/packages/element/src/store.ts index da7352e3ee..0f5933422c 100644 --- a/packages/element/src/store.ts +++ b/packages/element/src/store.ts @@ -23,6 +23,8 @@ import { syncInvalidIndicesImmutable, hashElementsVersion, hashString, + isInitializedImageElement, + isImageElement, } from "./index"; import type { @@ -137,6 +139,8 @@ export class Store { } else { // immediately create an immutable change of the scheduled updates, // compared to the current state, so that they won't mutate later on during batching + // also, we have to compare against the current state, + // as comparing against the snapshot might include yet uncomitted changes (i.e. async freedraw / text / image, etc.) const currentSnapshot = StoreSnapshot.create( this.app.scene.getElementsMapIncludingDeleted(), this.app.state, @@ -869,7 +873,7 @@ export class StoreSnapshot { } /** - * Detect if there any changed elements. + * Detect if there are any changed elements. */ private detectChangedElements( nextElements: SceneElementsMap, @@ -904,6 +908,14 @@ export class StoreSnapshot { !prevElement || // element was added prevElement.version < nextElement.version // element was updated ) { + if ( + isImageElement(nextElement) && + !isInitializedImageElement(nextElement) + ) { + // ignore any updates on uninitialized image elements + continue; + } + changedElements.set(nextElement.id, nextElement); } } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index 9b3fe0d8bd..dd362ecc21 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -3063,18 +3063,7 @@ class App extends React.Component { return; } - const imageElement = this.createImageElement({ sceneX, sceneY }); - this.insertImageElement(imageElement, file); - this.initializeImageDimensions(imageElement); - this.store.scheduleCapture(); - this.setState({ - selectedElementIds: makeNextSelectedElementIds( - { - [imageElement.id]: true, - }, - this.state, - ), - }); + this.createImageElement({ sceneX, sceneY, imageFile: file }); return; } @@ -3380,15 +3369,12 @@ class App extends React.Component { const nextSelectedIds: Record = {}; for (const response of responses) { if (response.file) { - const imageElement = this.createImageElement({ + const initializedImageElement = await this.createImageElement({ sceneX, sceneY: y, + imageFile: response.file, }); - const initializedImageElement = await this.insertImageElement( - imageElement, - response.file, - ); if (initializedImageElement) { // vertically center first image in the batch if (!firstImageYOffsetDone) { @@ -3403,9 +3389,9 @@ class App extends React.Component { { informMutation: false, isDragging: false }, ); - y = imageElement.y + imageElement.height + 25; + y = initializedImageElement.y + initializedImageElement.height + 25; - nextSelectedIds[imageElement.id] = true; + nextSelectedIds[initializedImageElement.id] = true; } } } @@ -7628,14 +7614,16 @@ class App extends React.Component { return element; }; - private createImageElement = ({ + private createImageElement = async ({ sceneX, sceneY, addToFrameUnderCursor = true, + imageFile, }: { sceneX: number; sceneY: number; addToFrameUnderCursor?: boolean; + imageFile: File; }) => { const [gridX, gridY] = getGridPoint( sceneX, @@ -7652,10 +7640,10 @@ class App extends React.Component { }) : null; - const element = newImageElement({ + const placeholderSize = 100 / this.state.zoom.value; + + const placeholderImageElement = newImageElement({ type: "image", - x: gridX, - y: gridY, strokeColor: this.state.currentItemStrokeColor, backgroundColor: this.state.currentItemBackgroundColor, fillStyle: this.state.currentItemFillStyle, @@ -7666,9 +7654,18 @@ class App extends React.Component { opacity: this.state.currentItemOpacity, locked: false, frameId: topLayerFrame ? topLayerFrame.id : null, + x: gridX - placeholderSize / 2, + y: gridY - placeholderSize / 2, + width: placeholderSize, + height: placeholderSize, }); - return element; + const initializedImageElement = await this.insertImageElement( + placeholderImageElement, + imageFile, + ); + + return initializedImageElement; }; private handleLinearElementOnPointerDown = ( @@ -9092,32 +9089,6 @@ class App extends React.Component { return; } - if (isImageElement(newElement)) { - const imageElement = newElement; - try { - this.initializeImageDimensions(imageElement); - this.setState( - { - selectedElementIds: makeNextSelectedElementIds( - { [imageElement.id]: true }, - this.state, - ), - }, - () => { - this.actionManager.executeAction(actionFinalize); - }, - ); - } catch (error: any) { - console.error(error); - this.scene.replaceAllElements( - this.scene - .getElementsIncludingDeleted() - .filter((el) => el.id !== imageElement.id), - ); - this.actionManager.executeAction(actionFinalize); - } - return; - } if (isLinearElement(newElement)) { if (newElement!.points.length > 1) { @@ -9829,13 +9800,10 @@ class App extends React.Component { } }; - private initializeImage = async ({ - imageFile, - imageElement: _imageElement, - }: { - imageFile: File; - imageElement: ExcalidrawImageElement; - }) => { + private initializeImage = async ( + placeholderImageElement: ExcalidrawImageElement, + imageFile: File, + ) => { // at this point this should be guaranteed image file, but we do this check // to satisfy TS down the line if (!isSupportedImageFile(imageFile)) { @@ -9895,13 +9863,14 @@ class App extends React.Component { const dataURL = this.files[fileId]?.dataURL || (await getDataURL(imageFile)); - let imageElement = newElementWith(_imageElement, { - fileId, - }) as NonDeleted; - return new Promise>( async (resolve, reject) => { try { + let initializedImageElement = this.getLatestInitializedImageElement( + placeholderImageElement, + fileId, + ); + this.addMissingFiles([ { mimeType, @@ -9912,34 +9881,39 @@ class App extends React.Component { }, ]); - let cachedImageData = this.imageCache.get(fileId); - - if (!cachedImageData) { + if (!this.imageCache.get(fileId)) { this.addNewImagesToImageCache(); - const { updatedFiles } = await this.updateImageCache([ - imageElement, + const { erroredFiles } = await this.updateImageCache([ + initializedImageElement, ]); - if (updatedFiles.size) { - ShapeCache.delete(_imageElement); + if (erroredFiles.size) { + throw new Error("Image cache update resulted with an error."); } - - cachedImageData = this.imageCache.get(fileId); } - const imageHTML = await cachedImageData?.image; + const imageHTML = await this.imageCache.get(fileId)?.image; + + if ( + imageHTML && + this.state.newElement?.id !== initializedImageElement.id + ) { + initializedImageElement = this.getLatestInitializedImageElement( + placeholderImageElement, + fileId, + ); - if (imageHTML && this.state.newElement?.id !== imageElement.id) { const naturalDimensions = this.getImageNaturalDimensions( - imageElement, + initializedImageElement, imageHTML, ); - imageElement = newElementWith(imageElement, naturalDimensions); + // no need to create a new instance anymore, just assign the natural dimensions + Object.assign(initializedImageElement, naturalDimensions); } - resolve(imageElement); + resolve(initializedImageElement); } catch (error: any) { console.error(error); reject(new Error(t("errors.imageInsertError"))); @@ -9948,11 +9922,31 @@ class App extends React.Component { ); }; + /** + * use during async image initialization, + * when the placeholder image could have been modified in the meantime, + * and when you don't want to loose those modifications + */ + private getLatestInitializedImageElement = ( + imagePlaceholder: ExcalidrawImageElement, + fileId: FileId, + ) => { + const latestImageElement = + this.scene.getElement(imagePlaceholder.id) ?? imagePlaceholder; + + return newElementWith( + latestImageElement as InitializedExcalidrawImageElement, + { + fileId, + }, + ); + }; + /** * inserts image into elements array and rerenders */ - insertImageElement = async ( - imageElement: ExcalidrawImageElement, + private insertImageElement = async ( + placeholderImageElement: ExcalidrawImageElement, imageFile: File, ) => { // we should be handling all cases upstream, but in case we forget to handle @@ -9962,34 +9956,39 @@ class App extends React.Component { return; } - this.scene.insertElement(imageElement); + this.scene.insertElement(placeholderImageElement); try { - const image = await this.initializeImage({ + const initializedImageElement = await this.initializeImage( + placeholderImageElement, imageFile, - imageElement, - }); + ); const nextElements = this.scene .getElementsIncludingDeleted() .map((element) => { - if (element.id === image.id) { - return image; + if (element.id === initializedImageElement.id) { + return initializedImageElement; } return element; }); - // schedules an immediate micro action, which will update snapshot, - // but won't be undoable, which is what we want! this.updateScene({ - captureUpdate: CaptureUpdateAction.NEVER, + captureUpdate: CaptureUpdateAction.IMMEDIATELY, elements: nextElements, + appState: { + selectedElementIds: makeNextSelectedElementIds( + { [initializedImageElement.id]: true }, + this.state, + ), + }, }); - return image; + return initializedImageElement; } catch (error: any) { - this.scene.mutateElement(imageElement, { + this.store.scheduleAction(CaptureUpdateAction.NEVER); + this.scene.mutateElement(placeholderImageElement, { isDeleted: true, }); this.actionManager.executeAction(actionFinalize); @@ -10017,26 +10016,17 @@ class App extends React.Component { ) as (keyof typeof IMAGE_MIME_TYPES)[], }); - const imageElement = this.createImageElement({ + await this.createImageElement({ sceneX: x, sceneY: y, addToFrameUnderCursor: false, + imageFile, }); - this.insertImageElement(imageElement, imageFile); - this.initializeImageDimensions(imageElement); - this.store.scheduleCapture(); - this.setState( - { - selectedElementIds: makeNextSelectedElementIds( - { [imageElement.id]: true }, - this.state, - ), - }, - () => { - this.actionManager.executeAction(actionFinalize); - }, - ); + // avoid being batched (just in case) + this.setState({}, () => { + this.actionManager.executeAction(actionFinalize); + }); } catch (error: any) { if (error.name !== "AbortError") { console.error(error); @@ -10055,45 +10045,6 @@ class App extends React.Component { } }; - initializeImageDimensions = (imageElement: ExcalidrawImageElement) => { - const imageHTML = - isInitializedImageElement(imageElement) && - this.imageCache.get(imageElement.fileId)?.image; - - if (!imageHTML || imageHTML instanceof Promise) { - if ( - imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && - imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value - ) { - const placeholderSize = 100 / this.state.zoom.value; - - this.scene.mutateElement(imageElement, { - x: imageElement.x - placeholderSize / 2, - y: imageElement.y - placeholderSize / 2, - width: placeholderSize, - height: placeholderSize, - }); - } - - return; - } - - // if user-created bounding box is below threshold, assume the - // intention was to click instead of drag, and use the image's - // intrinsic size - if ( - imageElement.width < DRAGGING_THRESHOLD / this.state.zoom.value && - imageElement.height < DRAGGING_THRESHOLD / this.state.zoom.value - ) { - const naturalDimensions = this.getImageNaturalDimensions( - imageElement, - imageHTML, - ); - - this.scene.mutateElement(imageElement, naturalDimensions); - } - }; - private getImageNaturalDimensions = ( imageElement: ExcalidrawImageElement, imageHTML: HTMLImageElement, @@ -10135,8 +10086,9 @@ class App extends React.Component { }); if (erroredFiles.size) { + this.store.scheduleAction(CaptureUpdateAction.NEVER); this.scene.replaceAllElements( - this.scene.getElementsIncludingDeleted().map((element) => { + elements.map((element) => { if ( isInitializedImageElement(element) && erroredFiles.has(element.fileId) @@ -10357,17 +10309,7 @@ class App extends React.Component { // if no scene is embedded or we fail for whatever reason, fall back // to importing as regular image // --------------------------------------------------------------------- - - const imageElement = this.createImageElement({ sceneX, sceneY }); - this.insertImageElement(imageElement, file); - this.initializeImageDimensions(imageElement); - this.store.scheduleCapture(); - this.setState({ - selectedElementIds: makeNextSelectedElementIds( - { [imageElement.id]: true }, - this.state, - ), - }); + this.createImageElement({ sceneX, sceneY, imageFile: file }); return; } diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 22b8519dc0..9a2c621311 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -12514,7 +12514,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on i "strokeWidth": 2, "type": "image", "updated": 1, - "version": 7, + "version": 5, "width": 318, "x": -159, "y": "-167.50000", @@ -12573,14 +12573,14 @@ exports[`history > singleplayer undo/redo > should create new history entry on i "strokeStyle": "solid", "strokeWidth": 2, "type": "image", - "version": 7, + "version": 5, "width": 318, "x": -159, "y": "-167.50000", }, "inserted": { "isDeleted": true, - "version": 6, + "version": 4, }, }, }, @@ -12737,7 +12737,7 @@ exports[`history > singleplayer undo/redo > should create new history entry on i "strokeWidth": 2, "type": "image", "updated": 1, - "version": 7, + "version": 5, "width": 56, "x": -28, "y": "-38.50000", @@ -12796,14 +12796,14 @@ exports[`history > singleplayer undo/redo > should create new history entry on i "strokeStyle": "solid", "strokeWidth": 2, "type": "image", - "version": 7, + "version": 5, "width": 56, "x": -28, "y": "-38.50000", }, "inserted": { "isDeleted": true, - "version": 6, + "version": 4, }, }, }, diff --git a/packages/excalidraw/tests/flip.test.tsx b/packages/excalidraw/tests/flip.test.tsx index 79a935068c..e965a00686 100644 --- a/packages/excalidraw/tests/flip.test.tsx +++ b/packages/excalidraw/tests/flip.test.tsx @@ -38,6 +38,8 @@ import { import { getTextEditor } from "./queries/dom"; +import { mockHTMLImageElement } from "./helpers/mocks"; + import type { NormalizedZoomValue } from "../types"; const { h } = window; @@ -742,6 +744,28 @@ describe("freedraw", () => { //image //TODO: currently there is no test for pixel colors at flipped positions. describe("image", () => { + const smileyImageDimensions = { + width: 56, + height: 77, + }; + + beforeEach(() => { + // it's necessary to specify the height in order to calculate natural dimensions of the image + h.state.height = 1000; + }); + + beforeAll(() => { + mockHTMLImageElement( + smileyImageDimensions.width, + smileyImageDimensions.height, + ); + }); + + afterAll(() => { + vi.unstubAllGlobals(); + h.state.height = 0; + }); + const createImage = async () => { const sendPasteEvent = (file?: File) => { const clipboardEvent = createPasteEvent({ files: file ? [file] : [] }); diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index be7c5821f4..ba013e29d8 100644 --- a/packages/excalidraw/tests/history.test.tsx +++ b/packages/excalidraw/tests/history.test.tsx @@ -642,6 +642,19 @@ describe("history", () => { ...deerImageDimensions, }), ]); + + // need to check that delta actually contains initialized image element (with fileId & natural dimensions) + expect( + Object.values(h.history.undoStack[0].elements.removed)[0].deleted, + ).toEqual( + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + ...deerImageDimensions, + }), + ); }); Keyboard.undo(); @@ -753,6 +766,18 @@ describe("history", () => { ...smileyImageDimensions, }), ]); + // need to check that delta actually contains initialized image element (with fileId & natural dimensions) + expect( + Object.values(h.history.undoStack[0].elements.removed)[0].deleted, + ).toEqual( + expect.objectContaining({ + type: "image", + fileId: expect.any(String), + x: expect.toBeNonNaNNumber(), + y: expect.toBeNonNaNNumber(), + ...smileyImageDimensions, + }), + ); }); Keyboard.undo(); From 958597dfaa8604f5a1c27601e48f2f736788ffcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 16 Jun 2025 12:30:42 +0200 Subject: [PATCH 19/22] chore: Refactor doBoundsIntersect (#9657) --- packages/element/src/binding.ts | 7 +- packages/element/src/bounds.ts | 16 ++- packages/element/src/collision.ts | 180 ++++++++++++++++++++--------- packages/element/src/utils.ts | 16 ++- packages/excalidraw/lasso/utils.ts | 2 +- packages/math/src/curve.ts | 25 ---- packages/math/src/rectangle.ts | 15 +++ packages/math/src/utils.ts | 16 --- 8 files changed, 172 insertions(+), 105 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index af66ebd9fe..25c09732dc 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -24,7 +24,6 @@ import { pointsEqual, lineSegmentIntersectionPoints, PRECISION, - doBoundsIntersect, } from "@excalidraw/math"; import type { LocalPoint, Radians } from "@excalidraw/math"; @@ -33,7 +32,11 @@ import type { AppState } from "@excalidraw/excalidraw/types"; import type { MapEntry, Mutable } from "@excalidraw/common/utility-types"; -import { getCenterForBounds, getElementBounds } from "./bounds"; +import { + doBoundsIntersect, + getCenterForBounds, + getElementBounds, +} from "./bounds"; import { intersectElementWithLineSegment } from "./collision"; import { distanceToElement } from "./distance"; import { diff --git a/packages/element/src/bounds.ts b/packages/element/src/bounds.ts index 260a9bd4ed..2c07631a7a 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -584,7 +584,7 @@ const solveQuadratic = ( return [s1, s2]; }; -const getCubicBezierCurveBound = ( +export const getCubicBezierCurveBound = ( p0: GlobalPoint, p1: GlobalPoint, p2: GlobalPoint, @@ -1230,6 +1230,20 @@ export const pointInsideBounds =

( ): boolean => p[0] > bounds[0] && p[0] < bounds[2] && p[1] > bounds[1] && p[1] < bounds[3]; +export const doBoundsIntersect = ( + bounds1: Bounds | null, + bounds2: Bounds | null, +): boolean => { + if (bounds1 == null || bounds2 == null) { + return false; + } + + const [minX1, minY1, maxX1, maxY1] = bounds1; + const [minX2, minY2, maxX2, maxY2] = bounds2; + + return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; +}; + export const elementCenterPoint = ( element: ExcalidrawElement, elementsMap: ElementsMap, diff --git a/packages/element/src/collision.ts b/packages/element/src/collision.ts index 5ae7753e63..cc15947edb 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -11,7 +11,6 @@ import { vectorFromPoint, vectorNormalize, vectorScale, - doBoundsIntersect, } from "@excalidraw/math"; import { @@ -19,15 +18,22 @@ import { ellipseSegmentInterceptPoints, } from "@excalidraw/math/ellipse"; -import type { GlobalPoint, LineSegment, Radians } from "@excalidraw/math"; +import type { + Curve, + GlobalPoint, + LineSegment, + Radians, +} from "@excalidraw/math"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; import { isPathALoop } from "./utils"; import { type Bounds, + doBoundsIntersect, elementCenterPoint, getCenterForBounds, + getCubicBezierCurveBound, getElementBounds, } from "./bounds"; import { @@ -255,13 +261,75 @@ export const intersectElementWithLineSegment = ( } }; +const curveIntersections = ( + curves: Curve[], + segment: LineSegment, + intersections: GlobalPoint[], + center: GlobalPoint, + angle: Radians, + onlyFirst = false, +) => { + for (const c of curves) { + // Optimize by doing a cheap bounding box check first + const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]); + const b2 = [ + Math.min(segment[0][0], segment[1][0]), + Math.min(segment[0][1], segment[1][1]), + Math.max(segment[0][0], segment[1][0]), + Math.max(segment[0][1], segment[1][1]), + ] as Bounds; + + if (!doBoundsIntersect(b1, b2)) { + continue; + } + + const hits = curveIntersectLineSegment(c, segment); + + if (hits.length > 0) { + for (const j of hits) { + intersections.push(pointRotateRads(j, center, angle)); + } + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; +}; + +const lineIntersections = ( + lines: LineSegment[], + segment: LineSegment, + intersections: GlobalPoint[], + center: GlobalPoint, + angle: Radians, + onlyFirst = false, +) => { + for (const l of lines) { + const intersection = lineSegmentIntersectionPoints(l, segment); + if (intersection) { + intersections.push(pointRotateRads(intersection, center, angle)); + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; +}; + const intersectLinearOrFreeDrawWithLineSegment = ( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, segment: LineSegment, onlyFirst = false, ): GlobalPoint[] => { + // NOTE: This is the only one which return the decomposed elements + // rotated! This is due to taking advantage of roughjs definitions. const [lines, curves] = deconstructLinearOrFreeDrawElement(element); - const intersections = []; + const intersections: GlobalPoint[] = []; for (const l of lines) { const intersection = lineSegmentIntersectionPoints(l, segment); @@ -275,6 +343,19 @@ const intersectLinearOrFreeDrawWithLineSegment = ( } for (const c of curves) { + // Optimize by doing a cheap bounding box check first + const b1 = getCubicBezierCurveBound(c[0], c[1], c[2], c[3]); + const b2 = [ + Math.min(segment[0][0], segment[1][0]), + Math.min(segment[0][1], segment[1][1]), + Math.max(segment[0][0], segment[1][0]), + Math.max(segment[0][1], segment[1][1]), + ] as Bounds; + + if (!doBoundsIntersect(b1, b2)) { + continue; + } + const hits = curveIntersectLineSegment(c, segment); if (hits.length > 0) { @@ -292,7 +373,7 @@ const intersectLinearOrFreeDrawWithLineSegment = ( const intersectRectanguloidWithLineSegment = ( element: ExcalidrawRectanguloidElement, elementsMap: ElementsMap, - l: LineSegment, + segment: LineSegment, offset: number = 0, onlyFirst = false, ): GlobalPoint[] => { @@ -300,48 +381,43 @@ const intersectRectanguloidWithLineSegment = ( // To emulate a rotated rectangle we rotate the point in the inverse angle // instead. It's all the same distance-wise. const rotatedA = pointRotateRads( - l[0], + segment[0], center, -element.angle as Radians, ); const rotatedB = pointRotateRads( - l[1], + segment[1], center, -element.angle as Radians, ); + const rotatedIntersector = lineSegment(rotatedA, rotatedB); // Get the element's building components we can test against const [sides, corners] = deconstructRectanguloidElement(element, offset); const intersections: GlobalPoint[] = []; - for (const s of sides) { - const intersection = lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ); - if (intersection) { - intersections.push(pointRotateRads(intersection, center, element.angle)); + lineIntersections( + sides, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); - if (onlyFirst) { - return intersections; - } - } + if (onlyFirst && intersections.length > 0) { + return intersections; } - for (const t of corners) { - const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)); - - if (hits.length > 0) { - for (const j of hits) { - intersections.push(pointRotateRads(j, center, element.angle)); - } - - if (onlyFirst) { - return intersections; - } - } - } + curveIntersections( + corners, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); return intersections; }; @@ -366,38 +442,32 @@ const intersectDiamondWithLineSegment = ( // points. It's all the same distance-wise. const rotatedA = pointRotateRads(l[0], center, -element.angle as Radians); const rotatedB = pointRotateRads(l[1], center, -element.angle as Radians); + const rotatedIntersector = lineSegment(rotatedA, rotatedB); const [sides, corners] = deconstructDiamondElement(element, offset); - const intersections: GlobalPoint[] = []; - for (const s of sides) { - const intersection = lineSegmentIntersectionPoints( - lineSegment(rotatedA, rotatedB), - s, - ); - if (intersection) { - intersections.push(pointRotateRads(intersection, center, element.angle)); + lineIntersections( + sides, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); - if (onlyFirst) { - return intersections; - } - } + if (onlyFirst && intersections.length > 0) { + return intersections; } - for (const t of corners) { - const hits = curveIntersectLineSegment(t, lineSegment(rotatedA, rotatedB)); - - if (hits.length > 0) { - for (const j of hits) { - intersections.push(pointRotateRads(j, center, element.angle)); - } - - if (onlyFirst) { - return intersections; - } - } - } + curveIntersections( + corners, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); return intersections; }; diff --git a/packages/element/src/utils.ts b/packages/element/src/utils.ts index ca31639d5c..44b0fe79c6 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -90,6 +90,12 @@ const setElementShapesCacheEntry = ( shapes.set(offset, shape); }; +/** + * Returns the **rotated** components of freedraw, line or arrow elements. + * + * @param element The linear element to deconstruct + * @returns The rotated in components. + */ export function deconstructLinearOrFreeDrawElement( element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, ): [LineSegment[], Curve[]] { @@ -171,11 +177,11 @@ export function deconstructLinearOrFreeDrawElement( /** * Get the building components of a rectanguloid element in the form of - * line segments and curves. + * line segments and curves **unrotated**. * * @param element Target rectanguloid element * @param offset Optional offset to expand the rectanguloid shape - * @returns Tuple of line segments (0) and curves (1) + * @returns Tuple of **unrotated** line segments (0) and curves (1) */ export function deconstructRectanguloidElement( element: ExcalidrawRectanguloidElement, @@ -310,12 +316,12 @@ export function deconstructRectanguloidElement( } /** - * Get the building components of a diamond element in the form of - * line segments and curves as a tuple, in this order. + * Get the **unrotated** building components of a diamond element + * in the form of line segments and curves as a tuple, in this order. * * @param element The element to deconstruct * @param offset An optional offset - * @returns Tuple of line segments (0) and curves (1) + * @returns Tuple of line **unrotated** segments (0) and curves (1) */ export function deconstructDiamondElement( element: ExcalidrawDiamondElement, diff --git a/packages/excalidraw/lasso/utils.ts b/packages/excalidraw/lasso/utils.ts index 8256f0069a..2cab64662b 100644 --- a/packages/excalidraw/lasso/utils.ts +++ b/packages/excalidraw/lasso/utils.ts @@ -4,12 +4,12 @@ import { polygonFromPoints, lineSegment, polygonIncludesPointNonZero, - doBoundsIntersect, } from "@excalidraw/math"; import { type Bounds, computeBoundTextPosition, + doBoundsIntersect, getBoundTextElement, getElementBounds, intersectElementWithLineSegment, diff --git a/packages/math/src/curve.ts b/packages/math/src/curve.ts index 3323456b51..fa11abd460 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,9 +1,6 @@ -import { type Bounds } from "@excalidraw/element"; - import { isPoint, pointDistance, pointFrom, pointFromVector } from "./point"; import { vector, vectorNormal, vectorNormalize, vectorScale } from "./vector"; import { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants"; -import { doBoundsIntersect } from "./utils"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -105,19 +102,6 @@ export const bezierEquation = ( export function curveIntersectLineSegment< Point extends GlobalPoint | LocalPoint, >(c: Curve, l: LineSegment): Point[] { - // Optimize by doing a cheap bounding box check first - const b1 = curveBounds(c); - const b2 = [ - Math.min(l[0][0], l[1][0]), - Math.min(l[0][1], l[1][1]), - Math.max(l[0][0], l[1][0]), - Math.max(l[0][1], l[1][1]), - ] as Bounds; - - if (!doBoundsIntersect(b1, b2)) { - return []; - } - const line = (s: number) => pointFrom( l[0][0] + s * (l[1][0] - l[0][0]), @@ -295,15 +279,6 @@ export function curveTangent( ); } -function curveBounds( - c: Curve, -): Bounds { - const [P0, P1, P2, P3] = c; - const x = [P0[0], P1[0], P2[0], P3[0]]; - const y = [P0[1], P1[1], P2[1], P3[1]]; - return [Math.min(...x), Math.min(...y), Math.max(...x), Math.max(...y)]; -} - export function curveCatmullRomQuadraticApproxPoints( points: GlobalPoint[], tension = 0.5, diff --git a/packages/math/src/rectangle.ts b/packages/math/src/rectangle.ts index 394b5c2f83..865e4afe53 100644 --- a/packages/math/src/rectangle.ts +++ b/packages/math/src/rectangle.ts @@ -10,6 +10,12 @@ export function rectangle

( return [topLeft, bottomRight] as Rectangle

; } +export function rectangleFromNumberSequence< + Point extends LocalPoint | GlobalPoint, +>(minX: number, minY: number, maxX: number, maxY: number) { + return rectangle(pointFrom(minX, minY), pointFrom(maxX, maxY)); +} + export function rectangleIntersectLineSegment< Point extends LocalPoint | GlobalPoint, >(r: Rectangle, l: LineSegment): Point[] { @@ -22,3 +28,12 @@ export function rectangleIntersectLineSegment< .map((s) => lineSegmentIntersectionPoints(l, s)) .filter((i): i is Point => !!i); } + +export function rectangleIntersectRectangle< + Point extends LocalPoint | GlobalPoint, +>(rectangle1: Rectangle, rectangle2: Rectangle): boolean { + const [[minX1, minY1], [maxX1, maxY1]] = rectangle1; + const [[minX2, minY2], [maxX2, maxY2]] = rectangle2; + + return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; +} diff --git a/packages/math/src/utils.ts b/packages/math/src/utils.ts index 912ddd1358..8807c275e4 100644 --- a/packages/math/src/utils.ts +++ b/packages/math/src/utils.ts @@ -1,5 +1,3 @@ -import { type Bounds } from "@excalidraw/element"; - export const PRECISION = 10e-5; export const clamp = (value: number, min: number, max: number) => { @@ -33,17 +31,3 @@ export const isFiniteNumber = (value: any): value is number => { export const isCloseTo = (a: number, b: number, precision = PRECISION) => Math.abs(a - b) < precision; - -export const doBoundsIntersect = ( - bounds1: Bounds | null, - bounds2: Bounds | null, -): boolean => { - if (bounds1 == null || bounds2 == null) { - return false; - } - - const [minX1, minY1, maxX1, maxY1] = bounds1; - const [minX2, minY2, maxX2, maxY2] = bounds2; - - return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; -}; From 0a19c93509866c581b7cb842a03ad0690d17b82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 16 Jun 2025 12:30:59 +0200 Subject: [PATCH 20/22] fix: Bindings at partially overlapping binding areas (#9536) --- packages/element/src/binding.ts | 44 ++++++- packages/element/src/linearElementEditor.ts | 104 ++++++++++------- .../excalidraw/actions/actionFinalize.tsx | 34 ++++-- packages/excalidraw/components/App.tsx | 110 ++++++------------ .../tests/__snapshots__/history.test.tsx.snap | 20 ++-- 5 files changed, 174 insertions(+), 138 deletions(-) diff --git a/packages/element/src/binding.ts b/packages/element/src/binding.ts index 25c09732dc..9d97801f2e 100644 --- a/packages/element/src/binding.ts +++ b/packages/element/src/binding.ts @@ -384,6 +384,48 @@ export const getSuggestedBindingsForArrows = ( ); }; +export const maybeSuggestBindingsForLinearElementAtCoords = ( + linearElement: NonDeleted, + /** scene coords */ + pointerCoords: { + x: number; + y: number; + }[], + scene: Scene, + zoom: AppState["zoom"], + // During line creation the start binding hasn't been written yet + // into `linearElement` + oppositeBindingBoundElement?: ExcalidrawBindableElement | null, +): ExcalidrawBindableElement[] => + Array.from( + pointerCoords.reduce( + (acc: Set>, coords) => { + const hoveredBindableElement = getHoveredElementForBinding( + coords, + scene.getNonDeletedElements(), + scene.getNonDeletedElementsMap(), + zoom, + isElbowArrow(linearElement), + isElbowArrow(linearElement), + ); + + if ( + hoveredBindableElement != null && + !isLinearElementSimpleAndAlreadyBound( + linearElement, + oppositeBindingBoundElement?.id, + hoveredBindableElement, + ) + ) { + acc.add(hoveredBindableElement); + } + + return acc; + }, + new Set() as Set>, + ), + ); + export const maybeBindLinearElement = ( linearElement: NonDeleted, appState: AppState, @@ -513,7 +555,7 @@ export const isLinearElementSimpleAndAlreadyBound = ( const isLinearElementSimple = ( linearElement: NonDeleted, -): boolean => linearElement.points.length < 3; +): boolean => linearElement.points.length < 3 && !isElbowArrow(linearElement); const unbindLinearElement = ( linearElement: NonDeleted, diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index 44d365e2e5..3f666c412c 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -20,6 +20,7 @@ import { getGridPoint, invariant, tupleToCoors, + viewportCoordsToSceneCoords, } from "@excalidraw/common"; import { @@ -45,6 +46,7 @@ import { bindOrUnbindLinearElement, getHoveredElementForBinding, isBindingEnabled, + maybeSuggestBindingsForLinearElementAtCoords, } from "./binding"; import { getElementAbsoluteCoords, @@ -275,18 +277,13 @@ export class LinearElementEditor { app: AppClassProperties, scenePointerX: number, scenePointerY: number, - maybeSuggestBinding: ( - element: NonDeleted, - pointSceneCoords: { x: number; y: number }[], - ) => void, linearElementEditor: LinearElementEditor, - scene: Scene, - ): LinearElementEditor | null { + ): Pick | null { if (!linearElementEditor) { return null; } const { elementId } = linearElementEditor; - const elementsMap = scene.getNonDeletedElementsMap(); + const elementsMap = app.scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement(elementId, elementsMap); let customLineAngle = linearElementEditor.customLineAngle; if (!element) { @@ -347,7 +344,7 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, - scene, + app.scene, new Map([ [ selectedIndex, @@ -375,7 +372,7 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, - scene, + app.scene, new Map( selectedPointsIndices.map((pointIndex) => { const newPointPosition: LocalPoint = @@ -407,46 +404,59 @@ export class LinearElementEditor { const boundTextElement = getBoundTextElement(element, elementsMap); if (boundTextElement) { - handleBindTextResize(element, scene, false); + handleBindTextResize(element, app.scene, false); } // suggest bindings for first and last point if selected + let suggestedBindings: ExcalidrawBindableElement[] = []; if (isBindingElement(element, false)) { + const firstSelectedIndex = selectedPointsIndices[0] === 0; + const lastSelectedIndex = + selectedPointsIndices[selectedPointsIndices.length - 1] === + element.points.length - 1; const coords: { x: number; y: number }[] = []; - const firstSelectedIndex = selectedPointsIndices[0]; - if (firstSelectedIndex === 0) { - coords.push( - tupleToCoors( - LinearElementEditor.getPointGlobalCoordinates( - element, - element.points[0], - elementsMap, + if (!firstSelectedIndex !== !lastSelectedIndex) { + coords.push({ x: scenePointerX, y: scenePointerY }); + } else { + if (firstSelectedIndex) { + coords.push( + tupleToCoors( + LinearElementEditor.getPointGlobalCoordinates( + element, + element.points[0], + elementsMap, + ), ), - ), - ); - } + ); + } - const lastSelectedIndex = - selectedPointsIndices[selectedPointsIndices.length - 1]; - if (lastSelectedIndex === element.points.length - 1) { - coords.push( - tupleToCoors( - LinearElementEditor.getPointGlobalCoordinates( - element, - element.points[lastSelectedIndex], - elementsMap, + if (lastSelectedIndex) { + coords.push( + tupleToCoors( + LinearElementEditor.getPointGlobalCoordinates( + element, + element.points[ + selectedPointsIndices[selectedPointsIndices.length - 1] + ], + elementsMap, + ), ), - ), - ); + ); + } } if (coords.length) { - maybeSuggestBinding(element, coords); + suggestedBindings = maybeSuggestBindingsForLinearElementAtCoords( + element, + coords, + app.scene, + app.state.zoom, + ); } } - return { + const newLinearElementEditor = { ...linearElementEditor, selectedPointsIndices, segmentMidPointHoveredCoords: @@ -466,6 +476,15 @@ export class LinearElementEditor { isDragging: true, customLineAngle, }; + + return { + ...app.state, + editingLinearElement: app.state.editingLinearElement + ? newLinearElementEditor + : null, + selectedLinearElement: newLinearElementEditor, + suggestedBindings, + }; } return null; @@ -479,6 +498,7 @@ export class LinearElementEditor { ): LinearElementEditor { const elementsMap = scene.getNonDeletedElementsMap(); const elements = scene.getNonDeletedElements(); + const pointerCoords = viewportCoordsToSceneCoords(event, appState); const { elementId, selectedPointsIndices, isDragging, pointerDownState } = editingLinearElement; @@ -534,13 +554,15 @@ export class LinearElementEditor { const bindingElement = isBindingEnabled(appState) ? getHoveredElementForBinding( - tupleToCoors( - LinearElementEditor.getPointAtIndexGlobalCoordinates( - element, - selectedPoint!, - elementsMap, - ), - ), + (selectedPointsIndices?.length ?? 0) > 1 + ? tupleToCoors( + LinearElementEditor.getPointAtIndexGlobalCoordinates( + element, + selectedPoint!, + elementsMap, + ), + ) + : pointerCoords, elements, elementsMap, appState.zoom, diff --git a/packages/excalidraw/actions/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index b6e2a9f075..7a4511e051 100644 --- a/packages/excalidraw/actions/actionFinalize.tsx +++ b/packages/excalidraw/actions/actionFinalize.tsx @@ -14,7 +14,12 @@ import { isLineElement, } from "@excalidraw/element"; -import { KEYS, arrayToMap, updateActiveTool } from "@excalidraw/common"; +import { + KEYS, + arrayToMap, + tupleToCoors, + updateActiveTool, +} from "@excalidraw/common"; import { isPathALoop } from "@excalidraw/element"; import { isInvisiblySmallElement } from "@excalidraw/element"; @@ -43,12 +48,16 @@ export const actionFinalize = register({ trackEvent: false, perform: (elements, appState, data, app) => { const { interactiveCanvas, focusContainer, scene } = app; - + const { event, sceneCoords } = + (data as { + event?: PointerEvent; + sceneCoords?: { x: number; y: number }; + }) ?? {}; const elementsMap = scene.getNonDeletedElementsMap(); - if (data?.event && appState.selectedLinearElement) { + if (event && appState.selectedLinearElement) { const linearElementEditor = LinearElementEditor.handlePointerUp( - data.event, + event, appState.selectedLinearElement, appState, app.scene, @@ -204,12 +213,17 @@ export const actionFinalize = register({ element.points.length > 1 && isBindingEnabled(appState) ) { - const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( - element, - -1, - arrayToMap(elements), - ); - maybeBindLinearElement(element, appState, { x, y }, scene); + const coords = + sceneCoords ?? + tupleToCoors( + LinearElementEditor.getPointAtIndexGlobalCoordinates( + element, + -1, + arrayToMap(elements), + ), + ); + + maybeBindLinearElement(element, appState, coords, scene); } } } diff --git a/packages/excalidraw/components/App.tsx b/packages/excalidraw/components/App.tsx index dd362ecc21..54305e4e97 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -105,12 +105,12 @@ import { import { getObservedAppState, getCommonBounds, + maybeSuggestBindingsForLinearElementAtCoords, getElementAbsoluteCoords, bindOrUnbindLinearElements, fixBindingsAfterDeletion, getHoveredElementForBinding, isBindingEnabled, - isLinearElementSimpleAndAlreadyBound, shouldEnableBindingForPointerEvent, updateBoundElements, getSuggestedBindingsForArrows, @@ -237,7 +237,6 @@ import { import type { LocalPoint, Radians } from "@excalidraw/math"; import type { - ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawGenericElement, @@ -5883,11 +5882,15 @@ class App extends React.Component { // and point const { newElement } = this.state; if (isBindingElement(newElement, false)) { - this.maybeSuggestBindingsForLinearElementAtCoords( - newElement, - [scenePointer], - this.state.startBoundElement, - ); + this.setState({ + suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords( + newElement, + [scenePointer], + this.scene, + this.state.zoom, + this.state.startBoundElement, + ), + }); } else { this.maybeSuggestBindingAtCursor(scenePointer, false); } @@ -8217,31 +8220,19 @@ class App extends React.Component { return; } - const newLinearElementEditor = LinearElementEditor.handlePointDragging( + const newState = LinearElementEditor.handlePointDragging( event, this, pointerCoords.x, pointerCoords.y, - (element, pointsSceneCoords) => { - this.maybeSuggestBindingsForLinearElementAtCoords( - element, - pointsSceneCoords, - ); - }, linearElementEditor, - this.scene, ); - if (newLinearElementEditor) { + if (newState) { pointerDownState.lastCoords.x = pointerCoords.x; pointerDownState.lastCoords.y = pointerCoords.y; pointerDownState.drag.hasOccurred = true; - this.setState({ - editingLinearElement: this.state.editingLinearElement - ? newLinearElementEditor - : null, - selectedLinearElement: newLinearElementEditor, - }); + this.setState(newState); return; } @@ -8720,11 +8711,15 @@ class App extends React.Component { if (isBindingElement(newElement, false)) { // When creating a linear element by dragging - this.maybeSuggestBindingsForLinearElementAtCoords( - newElement, - [pointerCoords], - this.state.startBoundElement, - ); + this.setState({ + suggestedBindings: maybeSuggestBindingsForLinearElementAtCoords( + newElement, + [pointerCoords], + this.scene, + this.state.zoom, + this.state.startBoundElement, + ), + }); } } else { pointerDownState.lastCoords.x = pointerCoords.x; @@ -8919,16 +8914,17 @@ class App extends React.Component { const hitElements = pointerDownState.hit.allHitElements; + const sceneCoords = viewportCoordsToSceneCoords( + { clientX: childEvent.clientX, clientY: childEvent.clientY }, + this.state, + ); + if ( this.state.activeTool.type === "selection" && !pointerDownState.boxSelection.hasOccurred && !pointerDownState.resize.isResizing && !hitElements.some((el) => this.state.selectedElementIds[el.id]) ) { - const sceneCoords = viewportCoordsToSceneCoords( - { clientX: childEvent.clientX, clientY: childEvent.clientY }, - this.state, - ); const hitLockedElement = this.getElementAtPosition( sceneCoords.x, sceneCoords.y, @@ -9029,6 +9025,7 @@ class App extends React.Component { } else if (this.state.selectedLinearElement.isDragging) { this.actionManager.executeAction(actionFinalize, "ui", { event: childEvent, + sceneCoords, }); } } @@ -9123,7 +9120,10 @@ class App extends React.Component { isBindingEnabled(this.state) && isBindingElement(newElement, false) ) { - this.actionManager.executeAction(actionFinalize); + this.actionManager.executeAction(actionFinalize, "ui", { + event: childEvent, + sceneCoords, + }); } this.setState({ suggestedBindings: [], startBoundElement: null }); if (!activeTool.locked) { @@ -9706,7 +9706,8 @@ class App extends React.Component { } if ( - pointerDownState.drag.hasOccurred || + (pointerDownState.drag.hasOccurred && + !this.state.selectedLinearElement) || isResizing || isRotating || isCropping @@ -10172,49 +10173,6 @@ class App extends React.Component { }); }; - private maybeSuggestBindingsForLinearElementAtCoords = ( - linearElement: NonDeleted, - /** scene coords */ - pointerCoords: { - x: number; - y: number; - }[], - // During line creation the start binding hasn't been written yet - // into `linearElement` - oppositeBindingBoundElement?: ExcalidrawBindableElement | null, - ): void => { - if (!pointerCoords.length) { - return; - } - - const suggestedBindings = pointerCoords.reduce( - (acc: NonDeleted[], coords) => { - const hoveredBindableElement = getHoveredElementForBinding( - coords, - this.scene.getNonDeletedElements(), - this.scene.getNonDeletedElementsMap(), - this.state.zoom, - isElbowArrow(linearElement), - isElbowArrow(linearElement), - ); - if ( - hoveredBindableElement != null && - !isLinearElementSimpleAndAlreadyBound( - linearElement, - oppositeBindingBoundElement?.id, - hoveredBindableElement, - ) - ) { - acc.push(hoveredBindableElement); - } - return acc; - }, - [], - ); - - this.setState({ suggestedBindings }); - }; - private clearSelection(hitElement: ExcalidrawElement | null): void { this.setState((prevState) => ({ selectedElementIds: makeNextSelectedElementIds({}, prevState), diff --git a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 9a2c621311..077897575f 100644 --- a/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap +++ b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap @@ -224,7 +224,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 37, + "version": 35, "width": "98.40611", "x": 1, "y": 0, @@ -348,7 +348,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02970", "gap": 1, }, - "version": 35, + "version": 33, }, "inserted": { "endBinding": { @@ -372,7 +372,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02000", "gap": 1, }, - "version": 32, + "version": 30, }, }, }, @@ -427,7 +427,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl ], ], "startBinding": null, - "version": 37, + "version": 35, "y": 0, }, "inserted": { @@ -447,7 +447,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": "0.02970", "gap": 1, }, - "version": 35, + "version": 33, "y": "35.82151", }, }, @@ -828,7 +828,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 33, + "version": 31, "width": 0, "x": 149, "y": 0, @@ -878,7 +878,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id4": { "deleted": { "endBinding": null, - "version": 32, + "version": 30, }, "inserted": { "endBinding": { @@ -886,7 +886,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": -0, "gap": 1, }, - "version": 30, + "version": 28, }, }, }, @@ -922,7 +922,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "id4": { "deleted": { "startBinding": null, - "version": 33, + "version": 31, }, "inserted": { "startBinding": { @@ -930,7 +930,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": 0, "gap": 1, }, - "version": 32, + "version": 30, }, }, }, From 8e27de2cdc05ed969f2f547233eeb3ab2ffd2679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Mon, 16 Jun 2025 14:07:03 +0200 Subject: [PATCH 21/22] fix: Frame dimensions change by stats don't include new elements (#9568) --- .../excalidraw/components/Stats/Dimension.tsx | 153 ++++++++++---- .../excalidraw/components/Stats/DragInput.tsx | 26 ++- .../components/Stats/MultiDimension.tsx | 85 +++++++- .../components/Stats/stats.test.tsx | 193 ++++++++++++++++++ 4 files changed, 418 insertions(+), 39 deletions(-) diff --git a/packages/excalidraw/components/Stats/Dimension.tsx b/packages/excalidraw/components/Stats/Dimension.tsx index a8868721bf..cfc0f2dda1 100644 --- a/packages/excalidraw/components/Stats/Dimension.tsx +++ b/packages/excalidraw/components/Stats/Dimension.tsx @@ -7,6 +7,9 @@ import { } from "@excalidraw/element"; import { resizeSingleElement } from "@excalidraw/element"; import { isImageElement } from "@excalidraw/element"; +import { isFrameLikeElement } from "@excalidraw/element"; +import { getElementsInResizingFrame } from "@excalidraw/element"; +import { replaceAllElementsInFrame } from "@excalidraw/element"; import type { ExcalidrawElement } from "@excalidraw/element/types"; @@ -15,7 +18,10 @@ import type { Scene } from "@excalidraw/element"; import DragInput from "./DragInput"; import { getStepSizedValue, isPropertyEditable } from "./utils"; -import type { DragInputCallbackType } from "./DragInput"; +import type { + DragFinishedCallbackType, + DragInputCallbackType, +} from "./DragInput"; import type { AppState } from "../../types"; interface DimensionDragInputProps { @@ -43,6 +49,8 @@ const handleDimensionChange: DragInputCallbackType< originalAppState, instantChange, scene, + app, + setAppState, }) => { const elementsMap = scene.getNonDeletedElementsMap(); const origElement = originalElements[0]; @@ -153,6 +161,7 @@ const handleDimensionChange: DragInputCallbackType< return; } + // User types in a value to stats then presses Enter if (nextValue !== undefined) { const nextWidth = Math.max( property === "width" @@ -184,52 +193,123 @@ const handleDimensionChange: DragInputCallbackType< }, ); + // Handle frame membership update for resized frames + if (isFrameLikeElement(latestElement)) { + const nextElementsInFrame = getElementsInResizingFrame( + scene.getElementsIncludingDeleted(), + latestElement, + originalAppState, + scene.getNonDeletedElementsMap(), + ); + + const updatedElements = replaceAllElementsInFrame( + scene.getElementsIncludingDeleted(), + nextElementsInFrame, + latestElement, + app, + ); + + scene.replaceAllElements(updatedElements); + } + return; } - const changeInWidth = property === "width" ? accumulatedChange : 0; - const changeInHeight = property === "height" ? accumulatedChange : 0; - let nextWidth = Math.max(0, origElement.width + changeInWidth); - if (property === "width") { - if (shouldChangeByStepSize) { - nextWidth = getStepSizedValue(nextWidth, STEP_SIZE); - } else { - nextWidth = Math.round(nextWidth); - } - } + // Stats slider is dragged + { + const changeInWidth = property === "width" ? accumulatedChange : 0; + const changeInHeight = property === "height" ? accumulatedChange : 0; - let nextHeight = Math.max(0, origElement.height + changeInHeight); - if (property === "height") { - if (shouldChangeByStepSize) { - nextHeight = getStepSizedValue(nextHeight, STEP_SIZE); - } else { - nextHeight = Math.round(nextHeight); - } - } - - if (keepAspectRatio) { + let nextWidth = Math.max(0, origElement.width + changeInWidth); if (property === "width") { - nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100; - } else { - nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100; + if (shouldChangeByStepSize) { + nextWidth = getStepSizedValue(nextWidth, STEP_SIZE); + } else { + nextWidth = Math.round(nextWidth); + } + } + + let nextHeight = Math.max(0, origElement.height + changeInHeight); + if (property === "height") { + if (shouldChangeByStepSize) { + nextHeight = getStepSizedValue(nextHeight, STEP_SIZE); + } else { + nextHeight = Math.round(nextHeight); + } + } + + if (keepAspectRatio) { + if (property === "width") { + nextHeight = Math.round((nextWidth / aspectRatio) * 100) / 100; + } else { + nextWidth = Math.round(nextHeight * aspectRatio * 100) / 100; + } + } + + nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight); + nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth); + + resizeSingleElement( + nextWidth, + nextHeight, + latestElement, + origElement, + originalElementsMap, + scene, + property === "width" ? "e" : "s", + { + shouldMaintainAspectRatio: keepAspectRatio, + }, + ); + + // Handle highlighting frame element candidates + if (isFrameLikeElement(latestElement)) { + const nextElementsInFrame = getElementsInResizingFrame( + scene.getElementsIncludingDeleted(), + latestElement, + originalAppState, + scene.getNonDeletedElementsMap(), + ); + + setAppState({ + elementsToHighlight: nextElementsInFrame, + }); } } + } +}; - nextHeight = Math.max(MIN_WIDTH_OR_HEIGHT, nextHeight); - nextWidth = Math.max(MIN_WIDTH_OR_HEIGHT, nextWidth); +const handleDragFinished: DragFinishedCallbackType = ({ + setAppState, + app, + originalElements, + originalAppState, +}) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); + const origElement = originalElements?.[0]; + const latestElement = origElement && elementsMap.get(origElement.id); - resizeSingleElement( - nextWidth, - nextHeight, + // Handle frame membership update for resized frames + if (latestElement && isFrameLikeElement(latestElement)) { + const nextElementsInFrame = getElementsInResizingFrame( + app.scene.getElementsIncludingDeleted(), latestElement, - origElement, - originalElementsMap, - scene, - property === "width" ? "e" : "s", - { - shouldMaintainAspectRatio: keepAspectRatio, - }, + originalAppState, + app.scene.getNonDeletedElementsMap(), ); + + const updatedElements = replaceAllElementsInFrame( + app.scene.getElementsIncludingDeleted(), + nextElementsInFrame, + latestElement, + app, + ); + + app.scene.replaceAllElements(updatedElements); + + setAppState({ + elementsToHighlight: null, + }); } }; @@ -269,6 +349,7 @@ const DimensionDragInput = ({ scene={scene} appState={appState} property={property} + dragFinishedCallback={handleDragFinished} /> ); }; diff --git a/packages/excalidraw/components/Stats/DragInput.tsx b/packages/excalidraw/components/Stats/DragInput.tsx index 56138d8103..259cb47180 100644 --- a/packages/excalidraw/components/Stats/DragInput.tsx +++ b/packages/excalidraw/components/Stats/DragInput.tsx @@ -11,7 +11,7 @@ import type { ElementsMap, ExcalidrawElement } from "@excalidraw/element/types"; import type { Scene } from "@excalidraw/element"; -import { useApp } from "../App"; +import { useApp, useExcalidrawSetAppState } from "../App"; import { InlineIcon } from "../InlineIcon"; import { SMALLEST_DELTA } from "./utils"; @@ -36,6 +36,15 @@ export type DragInputCallbackType< property: P; originalAppState: AppState; setInputValue: (value: number) => void; + app: ReturnType; + setAppState: ReturnType; +}) => void; + +export type DragFinishedCallbackType = (props: { + app: ReturnType; + setAppState: ReturnType; + originalElements: readonly E[] | null; + originalAppState: AppState; }) => void; interface StatsDragInputProps< @@ -54,6 +63,7 @@ interface StatsDragInputProps< appState: AppState; /** how many px you need to drag to get 1 unit change */ sensitivity?: number; + dragFinishedCallback?: DragFinishedCallbackType; } const StatsDragInput = < @@ -71,8 +81,10 @@ const StatsDragInput = < scene, appState, sensitivity = 1, + dragFinishedCallback, }: StatsDragInputProps) => { const app = useApp(); + const setAppState = useExcalidrawSetAppState(); const inputRef = useRef(null); const labelRef = useRef(null); @@ -137,6 +149,8 @@ const StatsDragInput = < property, originalAppState: appState, setInputValue: (value) => setInputValue(String(value)), + app, + setAppState, }); app.syncActionResult({ captureUpdate: CaptureUpdateAction.IMMEDIATELY, @@ -263,6 +277,8 @@ const StatsDragInput = < scene, originalAppState, setInputValue: (value) => setInputValue(String(value)), + app, + setAppState, }); stepChange = 0; @@ -287,6 +303,14 @@ const StatsDragInput = < captureUpdate: CaptureUpdateAction.IMMEDIATELY, }); + // Notify implementors + dragFinishedCallback?.({ + app, + setAppState, + originalElements, + originalAppState, + }); + lastPointer = null; accumulatedChange = 0; stepChange = 0; diff --git a/packages/excalidraw/components/Stats/MultiDimension.tsx b/packages/excalidraw/components/Stats/MultiDimension.tsx index 65f59ffe31..539a2ad59e 100644 --- a/packages/excalidraw/components/Stats/MultiDimension.tsx +++ b/packages/excalidraw/components/Stats/MultiDimension.tsx @@ -2,7 +2,12 @@ import { pointFrom, type GlobalPoint } from "@excalidraw/math"; import { useMemo } from "react"; import { MIN_WIDTH_OR_HEIGHT } from "@excalidraw/common"; -import { updateBoundElements } from "@excalidraw/element"; +import { + getElementsInResizingFrame, + isFrameLikeElement, + replaceAllElementsInFrame, + updateBoundElements, +} from "@excalidraw/element"; import { rescalePointsInElement, resizeSingleElement, @@ -25,7 +30,10 @@ import DragInput from "./DragInput"; import { getAtomicUnits, getStepSizedValue, isPropertyEditable } from "./utils"; import { getElementsInAtomicUnit } from "./utils"; -import type { DragInputCallbackType } from "./DragInput"; +import type { + DragFinishedCallbackType, + DragInputCallbackType, +} from "./DragInput"; import type { AtomicUnit } from "./utils"; import type { AppState } from "../../types"; @@ -153,6 +161,8 @@ const handleDimensionChange: DragInputCallbackType< nextValue, scene, property, + setAppState, + app, }) => { const elementsMap = scene.getNonDeletedElementsMap(); const atomicUnits = getAtomicUnits(originalElements, originalAppState); @@ -239,6 +249,25 @@ const handleDimensionChange: DragInputCallbackType< shouldInformMutation: false, }, ); + + // Handle frame membership update for resized frames + if (isFrameLikeElement(latestElement)) { + const nextElementsInFrame = getElementsInResizingFrame( + scene.getElementsIncludingDeleted(), + latestElement, + originalAppState, + scene.getNonDeletedElementsMap(), + ); + + const updatedElements = replaceAllElementsInFrame( + scene.getElementsIncludingDeleted(), + nextElementsInFrame, + latestElement, + app, + ); + + scene.replaceAllElements(updatedElements); + } } } } @@ -250,6 +279,7 @@ const handleDimensionChange: DragInputCallbackType< const changeInWidth = property === "width" ? accumulatedChange : 0; const changeInHeight = property === "height" ? accumulatedChange : 0; + const elementsToHighlight: ExcalidrawElement[] = []; for (const atomicUnit of atomicUnits) { const elementsInUnit = getElementsInAtomicUnit( @@ -342,13 +372,63 @@ const handleDimensionChange: DragInputCallbackType< shouldInformMutation: false, }, ); + + // Handle highlighting frame element candidates + if (isFrameLikeElement(latestElement)) { + const nextElementsInFrame = getElementsInResizingFrame( + scene.getElementsIncludingDeleted(), + latestElement, + originalAppState, + scene.getNonDeletedElementsMap(), + ); + + elementsToHighlight.push(...nextElementsInFrame); + } } } } + setAppState({ + elementsToHighlight, + }); + scene.triggerUpdate(); }; +const handleDragFinished: DragFinishedCallbackType = ({ + setAppState, + app, + originalElements, + originalAppState, +}) => { + const elementsMap = app.scene.getNonDeletedElementsMap(); + const origElement = originalElements?.[0]; + const latestElement = origElement && elementsMap.get(origElement.id); + + // Handle frame membership update for resized frames + if (latestElement && isFrameLikeElement(latestElement)) { + const nextElementsInFrame = getElementsInResizingFrame( + app.scene.getElementsIncludingDeleted(), + latestElement, + originalAppState, + app.scene.getNonDeletedElementsMap(), + ); + + const updatedElements = replaceAllElementsInFrame( + app.scene.getElementsIncludingDeleted(), + nextElementsInFrame, + latestElement, + app, + ); + + app.scene.replaceAllElements(updatedElements); + + setAppState({ + elementsToHighlight: null, + }); + } +}; + const MultiDimension = ({ property, elements, @@ -396,6 +476,7 @@ const MultiDimension = ({ appState={appState} property={property} scene={scene} + dragFinishedCallback={handleDragFinished} /> ); }; diff --git a/packages/excalidraw/components/Stats/stats.test.tsx b/packages/excalidraw/components/Stats/stats.test.tsx index 40cb598a0e..c52a721bff 100644 --- a/packages/excalidraw/components/Stats/stats.test.tsx +++ b/packages/excalidraw/components/Stats/stats.test.tsx @@ -737,3 +737,196 @@ describe("stats for multiple elements", () => { expect(newGroupHeight).toBeCloseTo(500, 4); }); }); + +describe("frame resizing behavior", () => { + beforeEach(async () => { + localStorage.clear(); + renderStaticScene.mockClear(); + reseed(7); + setDateTimeForTests("201933152653"); + + await render(); + + API.setElements([]); + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { + button: 2, + clientX: 1, + clientY: 1, + }); + const contextMenu = UI.queryContextMenu(); + fireEvent.click(queryByTestId(contextMenu!, "stats")!); + stats = UI.queryStats(); + }); + + beforeAll(() => { + mockBoundingClientRect(); + }); + + afterAll(() => { + restoreOriginalGetBoundingClientRect(); + }); + + it("should add shapes to frame when resizing frame to encompass them", () => { + // Create a frame + const frame = API.createElement({ + type: "frame", + x: 0, + y: 0, + width: 100, + height: 100, + }); + + // Create a rectangle outside the frame + const rectangle = API.createElement({ + type: "rectangle", + x: 150, + y: 50, + width: 50, + height: 50, + }); + + API.setElements([frame, rectangle]); + + // Initially, rectangle should not be in the frame + expect(rectangle.frameId).toBe(null); + + // Select the frame + API.setAppState({ + selectedElementIds: { + [frame.id]: true, + }, + }); + + elementStats = stats?.querySelector("#elementStats"); + + // Find the width input and update it to encompass the rectangle + const widthInput = UI.queryStatsProperty("W")?.querySelector( + ".drag-input", + ) as HTMLInputElement; + + expect(widthInput).toBeDefined(); + expect(widthInput.value).toBe("100"); + + // Resize frame to width 250, which should encompass the rectangle + UI.updateInput(widthInput, "250"); + + // After resizing, the rectangle should now be part of the frame + expect(h.elements.find((el) => el.id === rectangle.id)?.frameId).toBe( + frame.id, + ); + }); + + it("should add multiple shapes when frame encompasses them through height resize", () => { + const frame = API.createElement({ + type: "frame", + x: 0, + y: 0, + width: 200, + height: 100, + }); + + const rectangle1 = API.createElement({ + type: "rectangle", + x: 50, + y: 150, + width: 50, + height: 50, + }); + + const rectangle2 = API.createElement({ + type: "rectangle", + x: 100, + y: 180, + width: 40, + height: 40, + }); + + API.setElements([frame, rectangle1, rectangle2]); + + // Initially, rectangles should not be in the frame + expect(rectangle1.frameId).toBe(null); + expect(rectangle2.frameId).toBe(null); + + // Select the frame + API.setAppState({ + selectedElementIds: { + [frame.id]: true, + }, + }); + + elementStats = stats?.querySelector("#elementStats"); + + // Resize frame height to encompass both rectangles + const heightInput = UI.queryStatsProperty("H")?.querySelector( + ".drag-input", + ) as HTMLInputElement; + + // Resize frame to height 250, which should encompass both rectangles + UI.updateInput(heightInput, "250"); + + // After resizing, both rectangles should now be part of the frame + expect(h.elements.find((el) => el.id === rectangle1.id)?.frameId).toBe( + frame.id, + ); + expect(h.elements.find((el) => el.id === rectangle2.id)?.frameId).toBe( + frame.id, + ); + }); + + it("should not affect shapes that remain outside frame after resize", () => { + const frame = API.createElement({ + type: "frame", + x: 0, + y: 0, + width: 100, + height: 100, + }); + + const insideRect = API.createElement({ + type: "rectangle", + x: 120, + y: 50, + width: 30, + height: 30, + }); + + const outsideRect = API.createElement({ + type: "rectangle", + x: 300, + y: 50, + width: 30, + height: 30, + }); + + API.setElements([frame, insideRect, outsideRect]); + + // Initially, both rectangles should not be in the frame + expect(insideRect.frameId).toBe(null); + expect(outsideRect.frameId).toBe(null); + + // Select the frame + API.setAppState({ + selectedElementIds: { + [frame.id]: true, + }, + }); + + elementStats = stats?.querySelector("#elementStats"); + + // Resize frame width to 200, which should only encompass insideRect + const widthInput = UI.queryStatsProperty("W")?.querySelector( + ".drag-input", + ) as HTMLInputElement; + + UI.updateInput(widthInput, "200"); + + // After resizing, only insideRect should be in the frame + expect(h.elements.find((el) => el.id === insideRect.id)?.frameId).toBe( + frame.id, + ); + expect(h.elements.find((el) => el.id === outsideRect.id)?.frameId).toBe( + null, + ); + }); +}); From c1415004005d74961d43f7bc80cd0a3f2e1ab0cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1rk=20Tolm=C3=A1cs?= Date: Wed, 18 Jun 2025 14:45:51 +0200 Subject: [PATCH 22/22] chore: Relocate visualdebug so ESLint doesn't complain (#9668) --- excalidraw-app/components/DebugCanvas.tsx | 4 ++-- packages/{excalidraw => utils/src}/visualdebug.ts | 0 2 files changed, 2 insertions(+), 2 deletions(-) rename packages/{excalidraw => utils/src}/visualdebug.ts (100%) diff --git a/excalidraw-app/components/DebugCanvas.tsx b/excalidraw-app/components/DebugCanvas.tsx index e83a62647d..385b9b140e 100644 --- a/excalidraw-app/components/DebugCanvas.tsx +++ b/excalidraw-app/components/DebugCanvas.tsx @@ -18,10 +18,10 @@ import { } from "@excalidraw/math"; import { isCurve } from "@excalidraw/math/curve"; -import type { DebugElement } from "@excalidraw/excalidraw/visualdebug"; - import type { Curve } from "@excalidraw/math"; +import type { DebugElement } from "@excalidraw/utils/visualdebug"; + import { STORAGE_KEYS } from "../app_constants"; const renderLine = ( diff --git a/packages/excalidraw/visualdebug.ts b/packages/utils/src/visualdebug.ts similarity index 100% rename from packages/excalidraw/visualdebug.ts rename to packages/utils/src/visualdebug.ts