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 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/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/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/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 707e7292e3..1054960650 100644 --- a/packages/common/src/utils.ts +++ b/packages/common/src/utils.ts @@ -1,10 +1,9 @@ -import { average, pointFrom, type GlobalPoint } from "@excalidraw/math"; +import { average } from "@excalidraw/math"; import type { ExcalidrawBindableElement, FontFamilyValues, FontString, - ExcalidrawElement, } from "@excalidraw/element/types"; import type { @@ -101,7 +100,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("")}`; @@ -712,8 +710,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 }); @@ -1238,20 +1236,6 @@ export const escapeDoubleQuotes = (str: string) => { export const castArray = (value: T | T[]): T[] => Array.isArray(value) ? value : [value]; -export const elementCenterPoint = ( - element: ExcalidrawElement, - xOffset: number = 0, - yOffset: number = 0, -) => { - const { x, y, width, height } = element; - - const centerXPoint = x + width / 2 + xOffset; - - const centerYPoint = y + height / 2 + yOffset; - - return pointFrom(centerXPoint, centerYPoint); -}; - /** 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/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 2ea05510b4..9d97801f2e 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 { @@ -27,8 +26,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"; @@ -36,12 +33,12 @@ import type { AppState } from "@excalidraw/excalidraw/types"; import type { MapEntry, Mutable } from "@excalidraw/common/utility-types"; import { + doBoundsIntersect, getCenterForBounds, getElementBounds, - doBoundsIntersect, } from "./bounds"; import { intersectElementWithLineSegment } from "./collision"; -import { distanceToBindableElement } from "./distance"; +import { distanceToElement } from "./distance"; import { headingForPointFromElement, headingIsHorizontal, @@ -63,7 +60,7 @@ import { isTextElement, } from "./typeChecks"; -import { aabbForElement, getElementShape, pointInsideBounds } from "./shapes"; +import { aabbForElement, elementCenterPoint } from "./bounds"; import { updateElbowArrowPoints } from "./elbowArrow"; import type { Scene } from "./Scene"; @@ -109,7 +106,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 +127,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 +138,7 @@ export const bindOrUnbindLinearElement = ( boundToElementIds, unboundFromElementIds, scene, + elementsMap, ); bindOrUnbindLinearElementEdge( linearElement, @@ -150,6 +148,7 @@ export const bindOrUnbindLinearElement = ( boundToElementIds, unboundFromElementIds, scene, + elementsMap, ); const onlyUnbound = Array.from(unboundFromElementIds).filter( @@ -176,6 +175,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 +216,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 +254,7 @@ const getBindingStrategyForDraggingArrowEndpoints = ( const endDragged = draggingPoints.findIndex((i) => i === endIdx) > -1; const start = startDragged ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "start", elementsMap, @@ -279,7 +265,7 @@ const getBindingStrategyForDraggingArrowEndpoints = ( : "keep"; const end = endDragged ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "end", elementsMap, @@ -311,7 +297,7 @@ const getBindingStrategyForDraggingArrowOrJoints = ( ); const start = startIsClose ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "start", elementsMap, @@ -322,7 +308,7 @@ const getBindingStrategyForDraggingArrowOrJoints = ( : null; const end = endIsClose ? isBindingEnabled - ? getElligibleElementForBindingElement( + ? getEligibleElementForBindingElement( selectedElement, "end", elementsMap, @@ -398,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, @@ -441,22 +469,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 +507,7 @@ export const bindLinearElement = ( linearElement, hoveredElement, startOrEnd, + scene.getNonDeletedElementsMap(), ), }; } @@ -535,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, @@ -703,8 +723,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 +899,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 +908,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 +927,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 +945,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 +959,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 +978,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 +1029,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 +1130,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 +1167,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 +1178,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 +1188,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 +1205,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 +1261,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 +1289,7 @@ const updateBoundPoint = ( ); const focusPointAbsolute = determineFocusPoint( bindableElement, + elementsMap, binding.focus, adjacentPoint, ); @@ -1284,7 +1308,7 @@ const updateBoundPoint = ( elementsMap, ); - const center = elementCenterPoint(bindableElement); + const center = elementCenterPoint(bindableElement, elementsMap); const interceptorLength = pointDistance(adjacentPoint, edgePointAbsolute) + pointDistance(adjacentPoint, center) + @@ -1292,6 +1316,7 @@ const updateBoundPoint = ( const intersections = [ ...intersectElementWithLineSegment( bindableElement, + elementsMap, lineSegment( adjacentPoint, pointFromVector( @@ -1342,6 +1367,7 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", + elementsMap: ElementsMap, ): { fixedPoint: FixedPoint } => { const bounds = [ hoveredElement.x, @@ -1353,6 +1379,7 @@ export const calculateFixedPointForElbowArrowBinding = ( linearElement, hoveredElement, startOrEnd, + elementsMap, ); const globalMidPoint = pointFrom( bounds[0] + (bounds[2] - bounds[0]) / 2, @@ -1396,7 +1423,7 @@ const maybeCalculateNewGapWhenScaling = ( return { ...currentBinding, gap: newGap }; }; -const getElligibleElementForBindingElement = ( +const getEligibleElementForBindingElement = ( linearElement: NonDeleted, startOrEnd: "start" | "end", elementsMap: NonDeletedSceneElementsMap, @@ -1548,14 +1575,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 +1626,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 +1637,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 +1768,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 +2197,7 @@ export class BindableElement { export const getGlobalFixedPointForBindableElement = ( fixedPointRatio: [number, number], element: ExcalidrawBindableElement, + elementsMap: ElementsMap, ): GlobalPoint => { const [fixedX, fixedY] = normalizeFixedPoint(fixedPointRatio); @@ -2152,7 +2206,7 @@ export const getGlobalFixedPointForBindableElement = ( element.x + element.width * fixedX, element.y + element.height * fixedY, ), - elementCenterPoint(element), + elementCenterPoint(element, elementsMap), element.angle, ); }; @@ -2176,6 +2230,7 @@ export const getGlobalFixedPoints = ( ? getGlobalFixedPointForBindableElement( arrow.startBinding.fixedPoint, startElement as ExcalidrawBindableElement, + elementsMap, ) : pointFrom( arrow.x + arrow.points[0][0], @@ -2186,6 +2241,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..2c07631a7a 100644 --- a/packages/element/src/bounds.ts +++ b/packages/element/src/bounds.ts @@ -33,8 +33,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 +45,7 @@ import { isTextElement, } from "./typeChecks"; -import { getElementShape } from "./shapes"; +import { getElementShape } from "./shape"; import { deconstructDiamondElement, @@ -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, { @@ -553,7 +584,7 @@ const solveQuadratic = ( return [s1, s2]; }; -const getCubicBezierCurveBound = ( +export const getCubicBezierCurveBound = ( p0: GlobalPoint, p1: GlobalPoint, p2: GlobalPoint, @@ -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 = ( @@ -1133,6 +1165,71 @@ export const getCenterForBounds = (bounds: Bounds): GlobalPoint => bounds[1] + (bounds[3] - bounds[1]) / 2, ); +/** + * 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 doBoundsIntersect = ( bounds1: Bounds | null, bounds2: Bounds | null, @@ -1146,3 +1243,14 @@ export const doBoundsIntersect = ( return minX1 < maxX2 && maxX1 > minX2 && minY1 < maxY2 && maxY1 > minY2; }; + +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 07b17bfde5..cc15947edb 100644 --- a/packages/element/src/collision.ts +++ b/packages/element/src/collision.ts @@ -1,52 +1,68 @@ -import { isTransparent, elementCenterPoint } from "@excalidraw/common"; +import { isTransparent } 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 { + Curve, GlobalPoint, LineSegment, - LocalPoint, - Polygon, Radians, } from "@excalidraw/math"; import type { FrameNameBounds } from "@excalidraw/excalidraw/types"; -import { getBoundTextShape, isPathALoop } from "./shapes"; -import { getElementBounds } from "./bounds"; +import { isPathALoop } from "./utils"; +import { + type Bounds, + doBoundsIntersect, + elementCenterPoint, + getCenterForBounds, + getCubicBezierCurveBound, + 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 +88,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 +155,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 +203,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,67 +230,196 @@ 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, line, onlyFirst); } }; +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: GlobalPoint[] = []; + + for (const l of lines) { + const intersection = lineSegmentIntersectionPoints(l, segment); + if (intersection) { + intersections.push(intersection); + + if (onlyFirst) { + return intersections; + } + } + } + + 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) { + intersections.push(...hits); + + if (onlyFirst) { + return intersections; + } + } + } + + return intersections; +}; + const intersectRectanguloidWithLineSegment = ( element: ExcalidrawRectanguloidElement, - l: LineSegment, + elementsMap: ElementsMap, + segment: 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( - 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); - 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[] = []; + + lineIntersections( + sides, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, ); + + if (onlyFirst && intersections.length > 0) { + return intersections; + } + + curveIntersections( + corners, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); + + return intersections; }; /** @@ -245,43 +431,45 @@ 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 rotatedIntersector = lineSegment(rotatedA, rotatedB); - const [sides, curves] = deconstructDiamondElement(element, offset); + const [sides, corners] = deconstructDiamondElement(element, offset); + const intersections: GlobalPoint[] = []; - 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, - ) + lineIntersections( + sides, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, ); + + if (onlyFirst && intersections.length > 0) { + return intersections; + } + + curveIntersections( + corners, + rotatedIntersector, + intersections, + center, + element.angle, + onlyFirst, + ); + + return intersections; }; /** @@ -293,16 +481,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..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"; @@ -34,6 +33,7 @@ export const MINIMAL_CROP_SIZE = 10; export const cropElement = ( element: ExcalidrawImageElement, + elementsMap: ElementsMap, transformHandle: TransformHandleType, naturalWidth: number, naturalHeight: number, @@ -63,7 +63,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/delta.ts b/packages/element/src/delta.ts index 45b1fc3487..9504237b51 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; } @@ -617,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": @@ -858,10 +941,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 +1034,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 +1096,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 +1117,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 +1207,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 +1264,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 +1300,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 +1317,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 +1383,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 +1398,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 +1415,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 +1431,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 +1451,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 +1490,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 +1796,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 +1834,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/distance.ts b/packages/element/src/distance.ts index d261faf7df..4766ac9eef 100644 --- a/packages/element/src/distance.ts +++ b/packages/element/src/distance.ts @@ -6,27 +6,33 @@ import { import { ellipse, ellipseDistanceFromPoint } from "@excalidraw/math/ellipse"; -import { elementCenterPoint } from "@excalidraw/common"; - import type { GlobalPoint, Radians } from "@excalidraw/math"; import { deconstructDiamondElement, + deconstructLinearOrFreeDrawElement, deconstructRectanguloidElement, } from "./utils"; +import { elementCenterPoint } from "./bounds"; + 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, 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,24 @@ 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, + p: GlobalPoint, +) => { + 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 73c82a8980..0021851645 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"; @@ -29,10 +30,9 @@ import { FIXED_BINDING_DISTANCE, getHeadingForElbowArrowSnap, getGlobalFixedPointForBindableElement, - snapToMid, getHoveredElementForBinding, } from "./binding"; -import { distanceToBindableElement } from "./distance"; +import { distanceToElement } from "./distance"; import { compareHeading, flipHeading, @@ -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"; @@ -898,50 +898,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 +1229,7 @@ const getElbowArrowData = ( arrow.startBinding?.fixedPoint, origStartGlobalPoint, hoveredStartElement, + elementsMap, options?.isDragging, ); const endGlobalPoint = getGlobalPoint( @@ -1286,6 +1243,7 @@ const getElbowArrowData = ( arrow.endBinding?.fixedPoint, origEndGlobalPoint, hoveredEndElement, + elementsMap, options?.isDragging, ); const startHeading = getBindPointHeading( @@ -1293,12 +1251,14 @@ const getElbowArrowData = ( endGlobalPoint, hoveredStartElement, origStartGlobalPoint, + elementsMap, ); const endHeading = getBindPointHeading( endGlobalPoint, startGlobalPoint, hoveredEndElement, origEndGlobalPoint, + elementsMap, ); const startPointBounds = [ startGlobalPoint[0] - 2, @@ -1315,6 +1275,7 @@ const getElbowArrowData = ( const startElementBounds = hoveredStartElement ? aabbForElement( hoveredStartElement, + elementsMap, offsetFromHeading( startHeading, arrow.startArrowhead @@ -1327,6 +1288,7 @@ const getElbowArrowData = ( const endElementBounds = hoveredEndElement ? aabbForElement( hoveredEndElement, + elementsMap, offsetFromHeading( endHeading, arrow.endArrowhead @@ -1342,6 +1304,7 @@ const getElbowArrowData = ( hoveredEndElement ? aabbForElement( hoveredEndElement, + elementsMap, offsetFromHeading(endHeading, BASE_PADDING, BASE_PADDING), ) : endPointBounds, @@ -1351,6 +1314,7 @@ const getElbowArrowData = ( hoveredStartElement ? aabbForElement( hoveredStartElement, + elementsMap, offsetFromHeading(startHeading, BASE_PADDING, BASE_PADDING), ) : startPointBounds, @@ -1397,8 +1361,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,35 +2193,28 @@ 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) { - const fixedGlobalPoint = getGlobalFixedPointForBindableElement( + return getGlobalFixedPointForBindableElement( fixedPointRatio || [0, 0], element, + elementsMap ?? arrayToMap([element]), ); - - // NOTE: Resize scales the binding position point too, so we need to update it - return Math.abs( - distanceToBindableElement(element, fixedGlobalPoint) - - FIXED_BINDING_DISTANCE, - ) > 0.01 - ? bindPointToSnapToElementOutline(arrow, element, startOrEnd) - : fixedGlobalPoint; } return initialPoint; @@ -2268,6 +2225,7 @@ const getBindPointHeading = ( otherPoint: GlobalPoint, hoveredElement: ExcalidrawBindableElement | null | undefined, origPoint: GlobalPoint, + elementsMap: ElementsMap, ): Heading => getHeadingForElbowArrowSnap( p, @@ -2276,7 +2234,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 +2243,7 @@ const getBindPointHeading = ( ], ), origPoint, + elementsMap, ); const getHoveredElement = ( diff --git a/packages/element/src/flowchart.ts b/packages/element/src/flowchart.ts index 5194e54259..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, @@ -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/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/index.ts b/packages/element/src/index.ts index b62fc9834e..cf4a4e1caa 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 "./snapping"; diff --git a/packages/element/src/linearElementEditor.ts b/packages/element/src/linearElementEditor.ts index c2e69e42df..c28255c118 100644 --- a/packages/element/src/linearElementEditor.ts +++ b/packages/element/src/linearElementEditor.ts @@ -9,6 +9,8 @@ import { vectorFromPoint, line, linesIntersectAt, + curveLength, + curvePointAtLength, } from "@excalidraw/math"; import { getCurvePathOps } from "@excalidraw/utils/shape"; @@ -20,14 +22,16 @@ import { getGridPoint, invariant, tupleToCoors, + viewportCoordsToSceneCoords, } from "@excalidraw/common"; import { - type SnapLine, + deconstructLinearOrFreeDrawElement, + isPathALoop, snapLinearElementPoint, -} from "@excalidraw/element/snapping"; - -import { ShapeCache, type Store } from "@excalidraw/element"; + type SnapLine, + type Store, +} from "@excalidraw/element"; import type { Radians } from "@excalidraw/math"; @@ -46,6 +50,7 @@ import { bindOrUnbindLinearElement, getHoveredElementForBinding, isBindingEnabled, + maybeSuggestBindingsForLinearElementAtCoords, } from "./binding"; import { getElementAbsoluteCoords, @@ -62,14 +67,7 @@ import { isFixedPointBinding, } from "./typeChecks"; -import { - isPathALoop, - getBezierCurveLength, - getControlPointsForBezierCurve, - mapIntervalToBezierT, - getBezierXY, - toggleLinePolygonState, -} from "./shapes"; +import { ShapeCache, toggleLinePolygonState } from "./shape"; import { getLockedLinearCursorAlignSize } from "./sizeHelpers"; @@ -155,7 +153,6 @@ export class LinearElementEditor { public readonly segmentMidPointHoveredCoords: GlobalPoint | null; public readonly elbowed: boolean; public readonly customLineAngle: number | null; - public readonly snapLines: readonly SnapLine[]; constructor( element: NonDeleted, @@ -194,7 +191,6 @@ export class LinearElementEditor { this.segmentMidPointHoveredCoords = null; this.elbowed = isElbowArrow(element) && element.elbowed; this.customLineAngle = null; - this.snapLines = []; } // --------------------------------------------------------------------------- @@ -285,18 +281,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) { @@ -386,7 +377,7 @@ export class LinearElementEditor { if (!isElbowArrow(element)) { const { snapOffset, snapLines } = snapLinearElementPoint( - scene.getNonDeletedElements(), + app.scene.getNonDeletedElements(), element, lastClickedPoint, { x: effectiveGridX, y: effectiveGridY }, @@ -464,7 +455,7 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, - scene, + app.scene, new Map([ [ selectedIndex, @@ -483,7 +474,7 @@ export class LinearElementEditor { scenePointerY - linearElementEditor.pointerOffset.y; const { snapOffset, snapLines } = snapLinearElementPoint( - scene.getNonDeletedElements(), + app.scene.getNonDeletedElements(), element, lastClickedPoint, { x: originalPointerX, y: originalPointerY }, @@ -512,7 +503,7 @@ export class LinearElementEditor { LinearElementEditor.movePoints( element, - scene, + app.scene, new Map( selectedPointsIndices.map((pointIndex) => { const newPointPosition: LocalPoint = @@ -536,46 +527,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: @@ -587,7 +591,6 @@ export class LinearElementEditor { elementsMap, ) : null, - snapLines: _snapLines, hoverPointIndex: lastClickedPoint === 0 || lastClickedPoint === element.points.length - 1 @@ -596,6 +599,16 @@ export class LinearElementEditor { isDragging: true, customLineAngle, }; + + return { + ...app.state, + editingLinearElement: app.state.editingLinearElement + ? newLinearElementEditor + : null, + selectedLinearElement: newLinearElementEditor, + suggestedBindings, + snapLines: _snapLines, + }; } return null; @@ -609,6 +622,7 @@ export class LinearElementEditor { ): LinearElementEditor { const elementsMap = scene.getNonDeletedElementsMap(); const elements = scene.getNonDeletedElements(); + const pointerCoords = viewportCoordsToSceneCoords(event, appState); const { elementId, selectedPointsIndices, isDragging, pointerDownState } = editingLinearElement; @@ -664,13 +678,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, @@ -756,10 +772,7 @@ export class LinearElementEditor { } const segmentMidPoint = LinearElementEditor.getSegmentMidPoint( element, - points[index], - points[index + 1], index + 1, - elementsMap, ); midpoints.push(segmentMidPoint); index++; @@ -861,7 +874,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; @@ -869,39 +893,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( @@ -1118,7 +1145,10 @@ export class LinearElementEditor { scenePointerX: number, scenePointerY: number, app: AppClassProperties, - ): LinearElementEditor | null { + ): { + linearElementEditor: LinearElementEditor; + snapLines: readonly SnapLine[]; + } | null { const appState = app.state; if (!appState.editingLinearElement) { return null; @@ -1127,7 +1157,10 @@ export class LinearElementEditor { const elementsMap = app.scene.getNonDeletedElementsMap(); const element = LinearElementEditor.getElement(elementId, elementsMap); if (!element) { - return appState.editingLinearElement; + return { + linearElementEditor: appState.editingLinearElement, + snapLines: appState.snapLines, + }; } const { points } = element; @@ -1138,8 +1171,12 @@ export class LinearElementEditor { LinearElementEditor.deletePoints(element, app, [points.length - 1]); } return { - ...appState.editingLinearElement, - lastUncommittedPoint: null, + linearElementEditor: { + ...appState.editingLinearElement, + lastUncommittedPoint: null, + isDragging: false, + pointerOffset: { x: 0, y: 0 }, + }, snapLines: [], }; } @@ -1315,8 +1352,10 @@ export class LinearElementEditor { } return { - ...appState.editingLinearElement, - lastUncommittedPoint: element.points[element.points.length - 1], + linearElementEditor: { + ...appState.editingLinearElement, + lastUncommittedPoint: element.points[element.points.length - 1], + }, snapLines, }; } @@ -1924,10 +1963,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 84785c31c9..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"; @@ -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/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/resizeElements.ts b/packages/element/src/resizeElements.ts index dea6e3d75d..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); @@ -1518,11 +1387,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, diff --git a/packages/element/src/Shape.ts b/packages/element/src/shape.ts similarity index 61% rename from packages/element/src/Shape.ts rename to packages/element/src/shape.ts index 4def419574..7a8cd351a1 100644 --- a/packages/element/src/Shape.ts +++ b/packages/element/src/shape.ts @@ -1,26 +1,65 @@ import { simplify } from "points-on-curve"; -import { pointFrom, pointDistance, type LocalPoint } from "@excalidraw/math"; -import { ROUGHNESS, isTransparent, assertNever } from "@excalidraw/common"; +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, + COLOR_PALETTE, + LINE_POLYGON_POINT_MERGE_DISTANCE, +} 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"; -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"; import { generateFreeDrawShape } from "./renderElement"; -import { getArrowheadPoints, getDiamondPoints } from "./bounds"; +import { + getArrowheadPoints, + getCenterForBounds, + getDiamondPoints, + getElementAbsoluteCoords, +} from "./bounds"; +import { shouldTestInside } from "./collision"; import type { ExcalidrawElement, @@ -28,12 +67,89 @@ import type { ExcalidrawSelectionElement, ExcalidrawLinearElement, Arrowhead, + ExcalidrawFreeDrawElement, + ElementsMap, + ExcalidrawLineElement, } 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"; +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]; @@ -303,6 +419,182 @@ const getArrowheadShapes = ( } }; +export const generateLinearCollisionShape = ( + element: ExcalidrawLinearElement | ExcalidrawFreeDrawElement, +) => { + const generator = new RoughGenerator(); + const options: Options = { + seed: element.seed, + disableMultiStroke: true, + disableMultiStrokeFill: true, + roughness: 0, + preserveVertices: true, + }; + const center = getCenterForBounds( + // 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) { + 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. * @@ -310,7 +602,7 @@ const getArrowheadShapes = ( * * @private */ -export const _generateElementShape = ( +const generateElementShape = ( element: Exclude, generator: RoughGenerator, { @@ -611,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 20041ce1bc..0000000000 --- a/packages/element/src/shapes.ts +++ /dev/null @@ -1,446 +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, - 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); - 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/store.ts b/packages/element/src/store.ts index fb8926d88b..0f5933422c 100644 --- a/packages/element/src/store.ts +++ b/packages/element/src/store.ts @@ -19,9 +19,19 @@ import { newElementWith } from "./mutateElement"; import { ElementsDelta, AppStateDelta, Delta } from "./delta"; -import { hashElementsVersion, hashString } from "./index"; +import { + syncInvalidIndicesImmutable, + hashElementsVersion, + hashString, + isInitializedImageElement, + isImageElement, +} from "./index"; -import type { OrderedExcalidrawElement, SceneElementsMap } from "./types"; +import type { + ExcalidrawElement, + OrderedExcalidrawElement, + SceneElementsMap, +} from "./types"; export const CaptureUpdateAction = { /** @@ -105,7 +115,7 @@ export class Store { params: | { action: CaptureUpdateActionType; - elements: SceneElementsMap | undefined; + elements: readonly ExcalidrawElement[] | undefined; appState: AppState | ObservedAppState | undefined; } | { @@ -129,13 +139,21 @@ 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, ); + 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 +231,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 +514,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 +551,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 +559,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 +570,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 +583,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 +714,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 @@ -847,7 +873,7 @@ export class StoreSnapshot { } /** - * Detect if there any changed elements. + * Detect if there are any changed elements. */ private detectChangedElements( nextElements: SceneElementsMap, @@ -882,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); } } @@ -944,18 +978,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/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/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/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..44b0fe79c6 100644 --- a/packages/element/src/utils.ts +++ b/packages/element/src/utils.ts @@ -1,259 +1,346 @@ +import { + DEFAULT_ADAPTIVE_RADIUS, + DEFAULT_PROPORTIONAL_RADIUS, + LINE_CONFIRM_THRESHOLD, + ROUNDNESS, +} from "@excalidraw/common"; + import { curve, + curveCatmullRomCubicApproxPoints, + curveOffsetPoints, lineSegment, + pointDistance, pointFrom, - pointFromVector, + pointFromArray, rectangle, - vectorFromPoint, - vectorNormalize, - vectorScale, type GlobalPoint, } from "@excalidraw/math"; -import { elementCenterPoint } from "@excalidraw/common"; +import type { Curve, LineSegment, LocalPoint } from "@excalidraw/math"; -import type { Curve, LineSegment } from "@excalidraw/math"; - -import { getCornerRadius } from "./shapes"; +import type { NormalizedZoomValue, Zoom } from "@excalidraw/excalidraw/types"; import { getDiamondPoints } from "./bounds"; +import { generateLinearCollisionShape } from "./shape"; + import type { 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); +}; + +/** + * 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[]] { + const cachedShape = getElementShapesCacheEntry(element, 0); + + if (cachedShape) { + return cachedShape; + } + + const ops = generateLinearCollisionShape(element) 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. + * 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, 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; } /** - * 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, 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 +349,135 @@ 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; } + +// 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/__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/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..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; @@ -172,12 +176,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); @@ -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/collision.test.tsx b/packages/element/tests/collision.test.tsx new file mode 100644 index 0000000000..72996bdb1f --- /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(88, -68), + element: window.h.elements[0], + threshold: 10, + elementsMap: window.h.scene.getNonDeletedElementsMap(), + }); + expect(hit).toBe(true); + }); +}); 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/element/tests/linearElementEditor.test.tsx b/packages/element/tests/linearElementEditor.test.tsx index b8e49c6339..4b957022c6 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,11 @@ import { getBoundTextElementPosition, getBoundTextMaxWidth } from "../src"; import { LinearElementEditor } from "../src"; import { newArrowElement } from "../src"; +import { + getTextEditor, + TEXT_EDITOR_SELECTOR, +} from "../../excalidraw/tests/queries/dom"; + import type { ExcalidrawElement, ExcalidrawLinearElement, @@ -252,7 +256,49 @@ 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 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(); + + 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 +308,37 @@ 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(); + }); + + 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(TEXT_EDITOR_SELECTOR)).toBe(null); + }); + describe("Inside editor", () => { it("should not drag line and add midpoint when dragged irrespective of threshold", () => { createTwoPointerLinearElement("line"); @@ -346,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", ], ] `); @@ -411,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", ], ] `); @@ -727,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", ], ] `); @@ -816,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", ], ] `); @@ -983,19 +1060,17 @@ describe("Test Linear Elements", () => { ); expect(position).toMatchInlineSnapshot(` { - "x": "85.82202", - "y": "75.63461", + "x": "86.17305", + "y": "76.11251", } `); }); }); - 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(); }); @@ -1012,9 +1087,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 }, @@ -1042,9 +1115,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 }, @@ -1063,13 +1134,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 @@ -1234,9 +1299,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); @@ -1262,7 +1325,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/actionFinalize.tsx b/packages/excalidraw/actions/actionFinalize.tsx index cd3960a537..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, @@ -122,18 +131,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(); } @@ -216,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); } } } @@ -280,7 +282,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/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 30c9e74343..63cfe76727 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"; @@ -55,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"; @@ -138,8 +137,6 @@ import { isSomeElementSelected, } from "../scene"; -import { toggleLinePolygonState } from "../../element/src/shapes"; - import { register } from "./register"; import type { AppClassProperties, AppState, Primitive } from "../types"; @@ -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..dcc3fba11b 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, @@ -108,7 +109,6 @@ export const getDefaultAppState = (): Omit< value: 1 as NormalizedZoomValue, }, viewModeEnabled: false, - pendingImageElementId: null, showHyperlinkPopup: false, selectedLinearElement: null, snapLines: [], @@ -237,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 5ce8632329..c824e53e7c 100644 --- a/packages/excalidraw/components/App.tsx +++ b/packages/excalidraw/components/App.tsx @@ -19,8 +19,6 @@ import { line, linesIntersectAt, } from "@excalidraw/math"; -import { isPointInShape } from "@excalidraw/utils/collision"; -import { getSelectionBoxShape } from "@excalidraw/utils/shape"; import { COLOR_PALETTE, @@ -106,24 +104,20 @@ import { Emitter, } from "@excalidraw/common"; -import { getCommonBounds, getElementAbsoluteCoords } from "@excalidraw/element"; - import { + getObservedAppState, + getCommonBounds, + maybeSuggestBindingsForLinearElementAtCoords, + getElementAbsoluteCoords, bindOrUnbindLinearElements, fixBindingsAfterDeletion, getHoveredElementForBinding, isBindingEnabled, - isLinearElementSimpleAndAlreadyBound, shouldEnableBindingForPointerEvent, updateBoundElements, getSuggestedBindingsForArrows, -} from "@excalidraw/element"; - -import { LinearElementEditor } from "@excalidraw/element"; - -import { newElementWith } from "@excalidraw/element"; - -import { + LinearElementEditor, + newElementWith, newFrameElement, newFreeDrawElement, newEmbeddableElement, @@ -135,11 +129,8 @@ import { newLinearElement, newTextElement, refreshTextDimensions, -} from "@excalidraw/element"; - -import { deepCopyElement, duplicateElements } from "@excalidraw/element"; - -import { + deepCopyElement, + duplicateElements, hasBoundTextElement, isArrowElement, isBindingElement, @@ -160,48 +151,26 @@ 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, @@ -216,29 +185,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, @@ -246,13 +203,8 @@ import { getApproxMinLineWidth, getApproxMinLineHeight, getMinTextElementWidth, -} from "@excalidraw/element"; - -import { ShapeCache } from "@excalidraw/element"; - -import { getRenderOpacity } from "@excalidraw/element"; - -import { + ShapeCache, + getRenderOpacity, editGroupForSelectedElement, getElementsInGroup, getSelectedGroupIdForElement, @@ -260,60 +212,44 @@ 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, -} from "@excalidraw/element"; - -import { isNonDeletedElement } from "@excalidraw/element"; - -import { Scene } from "@excalidraw/element"; - -import { Store, CaptureUpdateAction } from "@excalidraw/element"; - -import { - getSnapLinesAtPointer, - snapDraggedElements, + isNonDeletedElement, + Scene, + Store, + CaptureUpdateAction, + type ElementUpdate, + hitElementBoundingBox, + isLineElement, + isSimpleArrow, + isGridModeEnabled, + SnapCache, isActiveToolNonLinearSnappable, + getSnapLinesAtPointer, + snapLinearElementPoint, + isSnappingEnabled, + getReferenceSnapPoints, + getVisibleGaps, + snapDraggedElements, snapNewElement, snapResizingElements, - isSnappingEnabled, - getVisibleGaps, - getReferenceSnapPoints, - SnapCache, - isGridModeEnabled, - snapLinearElementPoint, } from "@excalidraw/element"; -import type { LocalPoint, GlobalPoint, Radians } from "@excalidraw/math"; - -import type { ElementUpdate } from "@excalidraw/element"; +import type { GlobalPoint, LocalPoint, Radians } from "@excalidraw/math"; import type { - ExcalidrawBindableElement, ExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawGenericElement, @@ -333,10 +269,8 @@ import type { ExcalidrawEmbeddableElement, Ordered, MagicGenerationData, - ExcalidrawNonSelectionElement, ExcalidrawArrowElement, ExcalidrawElbowArrowElement, - SceneElementsMap, } from "@excalidraw/element/types"; import type { Mutable, ValueOf } from "@excalidraw/common/utility-types"; @@ -414,7 +348,6 @@ import { } from "../scene"; import { getStateForZoom } from "../scene/zoom"; import { - dataURLToFile, dataURLToString, generateIdFromFile, getDataURL, @@ -504,7 +437,6 @@ import type { AppProps, AppState, BinaryFileData, - DataURL, ExcalidrawImperativeAPI, BinaryFiles, Gesture, @@ -766,6 +698,8 @@ class App extends React.Component { addFiles: this.addFiles, resetScene: this.resetScene, getSceneElementsIncludingDeleted: this.getSceneElementsIncludingDeleted, + getSceneElementsMapIncludingDeleted: + this.getSceneElementsMapIncludingDeleted, history: { clear: this.resetHistory, }, @@ -1582,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; @@ -3068,6 +3001,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; @@ -3129,17 +3063,7 @@ class App extends React.Component { return; } - const imageElement = this.createImageElement({ sceneX, sceneY }); - this.insertImageElement(imageElement, file); - this.initializeImageDimensions(imageElement); - this.setState({ - selectedElementIds: makeNextSelectedElementIds( - { - [imageElement.id]: true, - }, - this.state, - ), - }); + this.createImageElement({ sceneX, sceneY, imageFile: file }); return; } @@ -3242,6 +3166,7 @@ class App extends React.Component { } } if (embeddables.length) { + this.store.scheduleCapture(); this.setState({ selectedElementIds: Object.fromEntries( embeddables.map((embeddable) => [embeddable.id, true]), @@ -3354,11 +3279,10 @@ class App extends React.Component { this.addMissingFiles(opts.files); } - this.store.scheduleCapture(); - const nextElementsToSelect = excludeElementsInFramesFromSelection(duplicatedElements); + this.store.scheduleCapture(); this.setState( { ...this.state, @@ -3445,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) { @@ -3468,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; } } } @@ -3592,7 +3513,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])), @@ -3614,8 +3535,6 @@ class App extends React.Component { }); PLAIN_PASTE_TOAST_SHOWN = true; } - - this.store.scheduleCapture(); } setAppState: React.Component["setState"] = ( @@ -3973,22 +3892,19 @@ 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 nextElements = elements ? elements : undefined; + const observedAppState = appState + ? getObservedAppState({ + ...this.store.snapshot.appState, + ...appState, + }) : undefined; this.store.scheduleMicroAction({ action: captureUpdate, - elements: nextElementsMap, - appState: nextAppState, + elements: nextElements, + appState: observedAppState, }); } @@ -3996,8 +3912,8 @@ class App extends React.Component { this.setState(appState); } - if (nextElements) { - this.scene.replaceAllElements(nextElements); + if (elements) { + this.scene.replaceAllElements(elements); } if (collaborators) { @@ -4494,12 +4410,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)) { @@ -4778,16 +4693,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)) { @@ -4813,10 +4722,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) => { @@ -5099,6 +5005,7 @@ class App extends React.Component { return null; } + // NOTE: Hot path for hit testing, so avoid unnecessary computations private getElementAtPosition( x: number, y: number, @@ -5138,16 +5045,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, @@ -5162,6 +5065,7 @@ class App extends React.Component { return null; } + // NOTE: Hot path for hit testing, so avoid unnecessary computations private getElementsAtPosition( x: number, y: number, @@ -5212,8 +5116,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( @@ -5228,35 +5138,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, @@ -5284,14 +5194,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]; @@ -5504,17 +5410,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(), ), }); @@ -5581,6 +5487,13 @@ class App extends React.Component { return; } + } else if ( + this.state.editingLinearElement && + this.state.editingLinearElement.elementId === + selectedLinearElement.id && + isLineElement(selectedLinearElement) + ) { + return; } } @@ -5629,40 +5542,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({ - x: sceneX, - y: sceneY, - element: container, - shape: getElementShape( + if (!this.state.editingLinearElement) { + const container = this.getTextBindableContainerAtPosition( + sceneX, + sceneY, + ); + + 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(), - ), - threshold: this.getElementHitThreshold(), - }) - ) { - const midPoint = getContainerCenter( - container, - this.state, - this.scene.getNonDeletedElementsMap(), - ); + ); - sceneX = midPoint.x; - sceneY = midPoint.y; + sceneX = midPoint.x; + sceneY = midPoint.y; + } } - } - this.startTextEditing({ - sceneX, - sceneY, - insertAtParentCenter: !event.altKey, - container, - }); + this.startTextEditing({ + sceneX, + sceneY, + insertAtParentCenter: !event.altKey, + container, + }); + } } }; @@ -5933,37 +5849,41 @@ class App extends React.Component { this.state.editingLinearElement && !this.state.editingLinearElement.isDragging ) { - const editingLinearElement = LinearElementEditor.handlePointerMove( + const result = LinearElementEditor.handlePointerMove( event, scenePointerX, scenePointerY, this, ); - if ( - editingLinearElement && - editingLinearElement !== this.state.editingLinearElement - ) { - // Since we are reading from previous state which is not possible with - // automatic batching in React 18 hence using flush sync to synchronously - // update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details. - flushSync(() => { - this.setState({ - editingLinearElement, - snapLines: editingLinearElement.snapLines, + if (result) { + const { linearElementEditor: editingLinearElement, snapLines } = result; + + if ( + editingLinearElement && + editingLinearElement !== this.state.editingLinearElement + ) { + // Since we are reading from previous state which is not possible with + // automatic batching in React 18 hence using flush sync to synchronously + // update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details. + flushSync(() => { + this.setState({ + editingLinearElement, + snapLines, + }); }); - }); - } - if (editingLinearElement?.lastUncommittedPoint != null) { - this.maybeSuggestBindingAtCursor( - scenePointer, - editingLinearElement.elbowed, - ); - } else { - // causes stack overflow if not sync - flushSync(() => { - this.setState({ suggestedBindings: [] }); - }); + } + if (editingLinearElement?.lastUncommittedPoint != null) { + this.maybeSuggestBindingAtCursor( + scenePointer, + editingLinearElement.elbowed, + ); + } else { + // causes stack overflow if not sync + flushSync(() => { + this.setState({ suggestedBindings: [] }); + }); + } } } @@ -5972,11 +5892,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); } @@ -6312,7 +6236,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, @@ -6427,13 +6354,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( @@ -6749,34 +6673,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, @@ -6800,7 +6696,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, @@ -7603,7 +7500,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 && @@ -7816,14 +7716,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, @@ -7840,10 +7742,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, @@ -7854,9 +7756,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 = ( @@ -8408,32 +8319,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, - snapLines: newLinearElementEditor.snapLines, - }); + this.setState(newState); return; } @@ -8999,11 +8897,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; @@ -9198,16 +9100,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, @@ -9217,6 +9120,7 @@ class App extends React.Component { ); this.store.scheduleCapture(); + if (hitLockedElement?.locked) { this.setState({ activeLockedId: @@ -9307,6 +9211,7 @@ class App extends React.Component { } else if (this.state.selectedLinearElement.isDragging) { this.actionManager.executeAction(actionFinalize, "ui", { event: childEvent, + sceneCoords, }); } } @@ -9330,10 +9235,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, @@ -9371,32 +9272,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) { @@ -9431,7 +9306,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) { @@ -9954,14 +9832,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, @@ -10015,7 +9892,8 @@ class App extends React.Component { } if ( - pointerDownState.drag.hasOccurred || + (pointerDownState.drag.hasOccurred && + !this.state.selectedLinearElement) || isResizing || isRotating || isCropping @@ -10109,15 +9987,10 @@ class App extends React.Component { } }; - private initializeImage = async ({ - imageFile, - imageElement: _imageElement, - showCursorImagePreview = false, - }: { - imageFile: File; - imageElement: ExcalidrawImageElement; - showCursorImagePreview?: boolean; - }) => { + 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)) { @@ -10174,30 +10047,17 @@ 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)); - const imageElement = this.scene.mutateElement( - _imageElement, - { - fileId, - }, - { informMutation: false, isDragging: false }, - ) as NonDeleted; - return new Promise>( async (resolve, reject) => { try { + let initializedImageElement = this.getLatestInitializedImageElement( + placeholderImageElement, + fileId, + ); + this.addMissingFiles([ { mimeType, @@ -10207,40 +10067,74 @@ class App extends React.Component { lastRetrieved: Date.now(), }, ]); - const cachedImageData = this.imageCache.get(fileId); - if (!cachedImageData) { + + if (!this.imageCache.get(fileId)) { this.addNewImagesToImageCache(); - await this.updateImageCache([imageElement]); - } - if (cachedImageData?.image instanceof Promise) { - await cachedImageData.image; + + const { erroredFiles } = await this.updateImageCache([ + initializedImageElement, + ]); + + if (erroredFiles.size) { + throw new Error("Image cache update resulted with an error."); + } } + + const imageHTML = await this.imageCache.get(fileId)?.image; + if ( - this.state.pendingImageElementId !== imageElement.id && - this.state.newElement?.id !== imageElement.id + imageHTML && + this.state.newElement?.id !== initializedImageElement.id ) { - this.initializeImageDimensions(imageElement, true); + initializedImageElement = this.getLatestInitializedImageElement( + placeholderImageElement, + fileId, + ); + + const naturalDimensions = this.getImageNaturalDimensions( + initializedImageElement, + imageHTML, + ); + + // 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"))); - } finally { - if (!showCursorImagePreview) { - resetCursor(this.interactiveCanvas); - } } }, ); }; + /** + * 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, - showCursorImagePreview?: boolean, ) => { // we should be handling all cases upstream, but in case we forget to handle // a future case, let's throw here @@ -10249,16 +10143,39 @@ class App extends React.Component { return; } - this.scene.insertElement(imageElement); + this.scene.insertElement(placeholderImageElement); try { - return await this.initializeImage({ + const initializedImageElement = await this.initializeImage( + placeholderImageElement, imageFile, - imageElement, - showCursorImagePreview, + ); + + const nextElements = this.scene + .getElementsIncludingDeleted() + .map((element) => { + if (element.id === initializedImageElement.id) { + return initializedImageElement; + } + + return element; + }); + + this.updateScene({ + captureUpdate: CaptureUpdateAction.IMMEDIATELY, + elements: nextElements, + appState: { + selectedElementIds: makeNextSelectedElementIds( + { [initializedImageElement.id]: true }, + this.state, + ), + }, }); + + return initializedImageElement; } catch (error: any) { - this.scene.mutateElement(imageElement, { + this.store.scheduleAction(CaptureUpdateAction.NEVER); + this.scene.mutateElement(placeholderImageElement, { isDeleted: true, }); this.actionManager.executeAction(actionFinalize); @@ -10269,58 +10186,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; @@ -10337,40 +10203,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, }); - if (insertOnCanvasDirectly) { - this.insertImageElement(imageElement, imageFile); - this.initializeImageDimensions(imageElement); - 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, - ); - }, - ); - } + // avoid being batched (just in case) + this.setState({}, () => { + this.actionManager.executeAction(actionFinalize); + }); } catch (error: any) { if (error.name !== "AbortError") { console.error(error); @@ -10379,7 +10222,6 @@ class App extends React.Component { } this.setState( { - pendingImageElementId: null, newElement: null, activeTool: updateActiveTool(this.state, { type: "selection" }), }, @@ -10390,62 +10232,32 @@ class App extends React.Component { } }; - initializeImageDimensions = ( + private getImageNaturalDimensions = ( imageElement: ExcalidrawImageElement, - forceNaturalSize = false, + imageHTML: HTMLImageElement, ) => { - const image = - isInitializedImageElement(imageElement) && - this.imageCache.get(imageElement.fileId)?.image; + 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, + ); - if (!image || image 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, - }); - } + const height = Math.min(imageHTML.naturalHeight, maxHeight); + const width = height * (imageHTML.naturalWidth / imageHTML.naturalHeight); - return; - } + // 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; - 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) - ) { - 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(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, - }); - } + return { + x, + y, + width, + height, + crop: null, + }; }; /** updates image cache, refreshing updated elements and/or setting status @@ -10459,16 +10271,11 @@ 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.store.scheduleAction(CaptureUpdateAction.NEVER); this.scene.replaceAllElements( - this.scene.getElementsIncludingDeleted().map((element) => { + elements.map((element) => { if ( isInitializedImageElement(element) && erroredFiles.has(element.fileId) @@ -10501,6 +10308,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(); } @@ -10543,49 +10359,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), @@ -10680,16 +10453,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.setState({ - selectedElementIds: makeNextSelectedElementIds( - { [imageElement.id]: true }, - this.state, - ), - }); + this.createImageElement({ sceneX, sceneY, imageFile: file }); return; } @@ -10734,6 +10498,7 @@ class App extends React.Component { link: normalizeLink(text), }); if (embeddable) { + this.store.scheduleCapture(); this.setState({ selectedElementIds: { [embeddable.id]: true } }); } } @@ -10788,7 +10553,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, }); @@ -11068,6 +10833,7 @@ class App extends React.Component { croppingElement, cropElement( croppingElement, + this.scene.getNonDeletedElementsMap(), transformHandleType, image.naturalWidth, image.naturalHeight, 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 ae491cecff..26a5b09844 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"; @@ -72,10 +73,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 ( @@ -137,7 +134,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/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 cc1cfce984..c52a721bff 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")); @@ -382,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(); @@ -403,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"); @@ -576,8 +586,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(); @@ -630,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"); @@ -657,6 +665,7 @@ describe("stats for multiple elements", () => { mouse.reset(); Keyboard.withModifierKeys({ shift: true }, () => { + mouse.moveTo(10, 0); mouse.click(); }); @@ -728,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, + ); + }); +}); 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; } 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/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..f00a51817d 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, @@ -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/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/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/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/locales/en.json b/packages/excalidraw/locales/en.json index 736c417225..b89e8ae5b8 100644 --- a/packages/excalidraw/locales/en.json +++ b/packages/excalidraw/locales/en.json @@ -344,9 +344,9 @@ "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", "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/renderer/helpers.ts b/packages/excalidraw/renderer/helpers.ts index e42f4166b0..d357822ec6 100644 --- a/packages/excalidraw/renderer/helpers.ts +++ b/packages/excalidraw/renderer/helpers.ts @@ -1,24 +1,22 @@ -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 { - bezierEquation, curve, - curveTangent, + curveCatmullRomCubicApproxPoints, + curveCatmullRomQuadraticApproxPoints, + curveOffsetPoints, type GlobalPoint, + offsetPointsForQuadraticBezier, pointFrom, - pointFromVector, pointRotateRads, - vector, - vectorNormal, - vectorNormalize, - vectorScale, } from "@excalidraw/math"; import type { + ElementsMap, ExcalidrawDiamondElement, ExcalidrawRectanguloidElement, } from "@excalidraw/element/types"; @@ -102,25 +100,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 +115,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); } } } @@ -164,11 +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), + elementCenterPoint(element, elementsMap), element.angle, ); @@ -187,25 +153,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 +196,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), @@ -322,10 +288,11 @@ export const drawHighlightForDiamondWithRotation = ( context: CanvasRenderingContext2D, padding: number, element: ExcalidrawDiamondElement, + elementsMap: ElementsMap, ) => { const [x, y] = pointRotateRads( pointFrom(element.x, element.y), - elementCenterPoint(element), + elementCenterPoint(element, elementsMap), element.angle, ); context.save(); @@ -343,32 +310,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 +351,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 +373,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 +414,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..1f3e0ff21d 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": @@ -211,15 +205,23 @@ 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": - 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 +232,7 @@ const renderBindingHighlightForBindableElement = ( element.angle, ); break; + } } }; 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/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/__snapshots__/contextmenu.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/contextmenu.test.tsx.snap index 23f4ccb4f2..5f609f1e46 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", @@ -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, @@ -1016,9 +1015,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 +1049,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 +1096,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", @@ -1157,12 +1152,11 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, - "scrolledOutside": false, + "scrolledOutside": true, "searchMatches": null, "selectedElementIds": { "id0": true, @@ -1205,7 +1199,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 +1207,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,26 +1255,26 @@ 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, + "version": 3, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -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", @@ -1373,7 +1365,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -1427,17 +1418,15 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1014066025, + "roundness": null, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -1461,9 +1450,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", @@ -1471,7 +1458,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, @@ -1518,19 +1505,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1572,19 +1559,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1614,9 +1601,11 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "id0": { "deleted": { "index": "a2", + "version": 4, }, "inserted": { "index": "a0", + "version": 3, }, }, }, @@ -1650,7 +1639,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", @@ -1706,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, @@ -1760,17 +1748,15 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1014066025, + "roundness": null, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -1794,9 +1780,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", @@ -1804,7 +1788,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, @@ -1851,19 +1835,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1905,19 +1889,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1947,9 +1931,11 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "id0": { "deleted": { "index": "a2", + "version": 4, }, "inserted": { "index": "a0", + "version": 3, }, }, }, @@ -1983,7 +1969,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", @@ -2039,12 +2025,11 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, - "scrolledOutside": false, + "scrolledOutside": true, "searchMatches": null, "selectedElementIds": { "id0": true, @@ -2087,7 +2072,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 +2080,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,26 +2128,26 @@ 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, + "version": 3, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2199,7 +2182,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", @@ -2255,7 +2238,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2299,7 +2281,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,10 +2289,8 @@ 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, @@ -2318,9 +2298,9 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "updated": 1, "version": 4, "versionNonce": 1014066025, - "width": 20, - "x": -10, - "y": 0, + "width": 10, + "x": -20, + "y": -10, } `; @@ -2357,26 +2337,26 @@ 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, + "version": 3, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2402,9 +2382,11 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "id0": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "isDeleted": false, + "version": 3, }, }, }, @@ -2440,7 +2422,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", @@ -2496,7 +2478,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2542,7 +2523,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 +2531,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 +2555,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,20 +2563,18 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1014066025, + "roundness": null, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 400692809, - "width": 20, - "x": 0, - "y": 10, + "versionNonce": 1604849351, + "width": 10, + "x": -10, + "y": 0, } `; @@ -2634,26 +2611,26 @@ 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, + "version": 3, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2688,26 +2665,26 @@ 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, + "version": 5, + "width": 10, + "x": -10, + "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -2742,7 +2719,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", @@ -2798,7 +2775,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -2859,9 +2835,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", @@ -2869,7 +2843,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, @@ -2895,17 +2869,15 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1014066025, + "roundness": null, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 747212839, "width": 20, "x": 20, "y": 30, @@ -2952,19 +2924,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3006,19 +2978,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3068,9 +3040,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -3078,9 +3052,11 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -3114,7 +3090,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", @@ -3170,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, @@ -3226,9 +3201,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 +3209,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 941653321, + "versionNonce": 1359939303, "width": 20, "x": -10, "y": 0, @@ -3260,17 +3233,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": 640725609, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 9, - "versionNonce": 640725609, + "versionNonce": 908564423, "width": 20, "x": 20, "y": 30, @@ -3317,19 +3288,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3371,19 +3342,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3405,9 +3376,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "strokeColor": "#e03131", + "version": 4, }, "inserted": { "strokeColor": "#1e1e1e", + "version": 3, }, }, }, @@ -3428,9 +3401,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "backgroundColor": "#a5d8ff", + "version": 5, }, "inserted": { "backgroundColor": "transparent", + "version": 4, }, }, }, @@ -3451,9 +3426,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "fillStyle": "cross-hatch", + "version": 6, }, "inserted": { "fillStyle": "solid", + "version": 5, }, }, }, @@ -3474,9 +3451,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "strokeStyle": "dotted", + "version": 7, }, "inserted": { "strokeStyle": "solid", + "version": 6, }, }, }, @@ -3497,9 +3476,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "roughness": 2, + "version": 8, }, "inserted": { "roughness": 1, + "version": 7, }, }, }, @@ -3520,9 +3501,11 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "id3": { "deleted": { "opacity": 60, + "version": 9, }, "inserted": { "opacity": 100, + "version": 8, }, }, }, @@ -3556,6 +3539,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roughness": 2, "strokeColor": "#e03131", "strokeStyle": "dotted", + "version": 4, }, "inserted": { "backgroundColor": "transparent", @@ -3564,6 +3548,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roughness": 1, "strokeColor": "#1e1e1e", "strokeStyle": "solid", + "version": 3, }, }, }, @@ -3597,7 +3582,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", @@ -3653,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, @@ -3707,10 +3691,8 @@ 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": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3741,17 +3723,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,19 +3778,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3852,19 +3832,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3886,9 +3866,11 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "id3": { "deleted": { "index": "Zz", + "version": 4, }, "inserted": { "index": "a1", + "version": 3, }, }, }, @@ -3922,7 +3904,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", @@ -3978,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, @@ -4032,17 +4013,15 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1014066025, + "roundness": null, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 915032327, "width": 20, "x": 20, "y": 30, @@ -4066,9 +4045,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,19 +4100,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4177,19 +4154,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4211,9 +4188,11 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "id3": { "deleted": { "index": "Zz", + "version": 4, }, "inserted": { "index": "a1", + "version": 3, }, }, }, @@ -4247,7 +4226,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", @@ -4303,7 +4282,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -4360,9 +4338,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", @@ -4370,7 +4346,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, @@ -4394,17 +4370,15 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 238820263, + "roundness": null, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 760410951, + "versionNonce": 289600103, "width": 20, "x": 20, "y": 30, @@ -4451,19 +4425,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4505,19 +4479,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 20, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4567,9 +4541,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -4577,9 +4553,11 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "groupIds": [ "id9", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -4606,21 +4584,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, }, }, }, @@ -5528,7 +5510,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", @@ -5584,7 +5566,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -5641,9 +5622,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 +5654,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": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 915032327, "width": 10, "x": 12, "y": 0, @@ -5732,19 +5709,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5786,19 +5763,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 12, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6749,7 +6726,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", @@ -6805,7 +6782,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -6866,9 +6842,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", @@ -6876,7 +6850,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, @@ -6902,17 +6876,15 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 238820263, + "roundness": null, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 747212839, + "versionNonce": 760410951, "width": 10, "x": 12, "y": 0, @@ -6959,19 +6931,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": -10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7013,19 +6985,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 12, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7097,9 +7069,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -7107,9 +7081,11 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -7684,7 +7660,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", @@ -7743,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, @@ -8684,7 +8659,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", @@ -8740,12 +8715,11 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, - "scrolledOutside": false, + "scrolledOutside": true, "searchMatches": null, "selectedElementIds": { "id0": true, @@ -9675,7 +9649,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", @@ -9734,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, @@ -9780,7 +9753,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 +9761,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 +9793,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 +9825,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,26 +9879,26 @@ 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, + "version": 3, + "width": 10, + "x": -20, + "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, 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..f03bea54d3 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==😀" +"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/__snapshots__/history.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/history.test.tsx.snap index 080d8fbf05..077897575f 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", @@ -79,16 +79,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "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,16 +125,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -158,16 +155,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -189,7 +184,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, @@ -200,8 +195,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "102.45605", - "id": "id691", + "height": "99.19972", + "id": "id4", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -214,8 +209,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "102.80179", - "102.45605", + "98.40611", + "99.19972", ], ], "roughness": 1, @@ -229,9 +224,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 37, - "width": "102.80179", - "x": "-0.42182", + "version": 35, + "width": "98.40611", + "x": 1, "y": 0, } `; @@ -242,7 +237,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id691", + "id": "id4", "type": "arrow", }, ], @@ -251,16 +246,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -290,23 +283,48 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id688": { + "id0": { + "deleted": { + "version": 17, + }, + "inserted": { + "version": 15, + }, + }, + "id1": { "deleted": { "boundElements": [], + "version": 9, }, "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, @@ -314,63 +332,52 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": 0, "gap": 1, }, - "height": "70.45017", + "height": "68.58402", "points": [ [ 0, 0, ], [ - "100.70774", - "70.45017", + 98, + "68.58402", ], ], "startBinding": { - "elementId": "id687", + "elementId": "id0", "focus": "0.02970", "gap": 1, }, + "version": 33, }, "inserted": { "endBinding": { - "elementId": "id688", + "elementId": "id1", "focus": "-0.02000", "gap": 1, }, - "height": "0.09250", + "height": "0.00656", "points": [ [ 0, 0, ], [ - "98.58579", - "0.09250", + "98.00000", + "-0.00656", ], ], "startBinding": { - "elementId": "id687", + "elementId": "id0", "focus": "0.02000", "gap": 1, }, - }, - }, - "id702": { - "deleted": { - "boundElements": [ - { - "id": "id691", - "type": "arrow", - }, - ], - }, - "inserted": { - "boundElements": [], + "version": 30, }, }, }, }, - "id": "id709", + "id": "id22", }, { "appState": AppStateDelta { @@ -383,58 +390,70 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id687": { + "id0": { "deleted": { "boundElements": [], + "version": 18, }, "inserted": { "boundElements": [ { - "id": "id691", + "id": "id4", "type": "arrow", }, ], + "version": 17, }, }, - "id691": { + "id15": { "deleted": { - "height": "102.45584", + "version": 14, + }, + "inserted": { + "version": 12, + }, + }, + "id4": { + "deleted": { + "height": "99.19972", "points": [ [ 0, 0, ], [ - "102.79971", - "102.45584", + "98.40611", + "99.19972", ], ], "startBinding": null, + "version": 35, "y": 0, }, "inserted": { - "height": "70.33521", + "height": "68.58402", "points": [ [ 0, 0, ], [ - "100.78887", - "70.33521", + 98, + "68.58402", ], ], "startBinding": { - "elementId": "id687", + "elementId": "id0", "focus": "0.02970", "gap": 1, }, - "y": "35.20327", + "version": 33, + "y": "35.82151", }, }, }, }, - "id": "id710", + "id": "id23", }, ] `; @@ -451,7 +470,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id687": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -467,22 +486,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id688": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -498,34 +517,34 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id690", + "id": "id3", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id691": true, + "id4": true, }, - "selectedLinearElementId": "id691", + "selectedLinearElementId": "id4", }, "inserted": { "selectedElementIds": {}, @@ -536,7 +555,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id691": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -575,18 +594,20 @@ 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, }, }, }, "updated": {}, }, - "id": "id693", + "id": "id6", }, ] `; @@ -615,7 +636,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", @@ -670,16 +691,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "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": {}, @@ -717,16 +737,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -749,16 +767,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -784,7 +800,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, @@ -797,7 +813,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, + 0, 0, ], ], @@ -812,9 +828,9 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeWidth": 2, "type": "arrow", "updated": 1, - "version": 33, - "width": 100, - "x": "149.29289", + "version": 31, + "width": 0, + "x": 149, "y": 0, } `; @@ -836,54 +852,46 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id665": { + "id0": { + "deleted": { + "version": 18, + }, + "inserted": { + "version": 16, + }, + }, + "id1": { "deleted": { "boundElements": [], + "version": 9, }, "inserted": { "boundElements": [ { - "id": "id668", + "id": "id4", "type": "arrow", }, ], + "version": 8, }, }, - "id668": { + "id4": { "deleted": { "endBinding": null, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], + "version": 30, }, "inserted": { "endBinding": { - "elementId": "id665", + "elementId": "id1", "focus": -0, "gap": 1, }, - "points": [ - [ - 0, - 0, - ], - [ - 0, - 0, - ], - ], + "version": 28, }, }, }, }, - "id": "id685", + "id": "id21", }, { "appState": AppStateDelta { @@ -896,54 +904,38 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "added": {}, "removed": {}, "updated": { - "id664": { + "id0": { "deleted": { "boundElements": [], + "version": 19, }, "inserted": { "boundElements": [ { - "id": "id668", + "id": "id4", "type": "arrow", }, ], + "version": 18, }, }, - "id668": { + "id4": { "deleted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": null, + "version": 31, }, "inserted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": { - "elementId": "id664", + "elementId": "id0", "focus": 0, "gap": 1, }, + "version": 30, }, }, }, }, - "id": "id686", + "id": "id22", }, ] `; @@ -960,7 +952,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id664": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -976,22 +968,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id665": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1007,34 +999,34 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id667", + "id": "id3", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id668": true, + "id4": true, }, - "selectedLinearElementId": "id668", + "selectedLinearElementId": "id4", }, "inserted": { "selectedElementIds": {}, @@ -1045,7 +1037,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id668": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1084,18 +1076,20 @@ 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, }, }, }, "updated": {}, }, - "id": "id670", + "id": "id6", }, ] `; @@ -1124,7 +1118,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", @@ -1182,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, @@ -1224,7 +1217,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, @@ -1235,8 +1228,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.30038", - "id": "id715", + "height": "1.36342", + "id": "id4", "index": "Zz", "isDeleted": false, "lastCommittedPoint": null, @@ -1249,17 +1242,15 @@ 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", + "elementId": "id0", "fixedPoint": [ 1, "0.50000", @@ -1273,8 +1264,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, } `; @@ -1285,7 +1276,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id715", + "id": "id4", "type": "arrow", }, ], @@ -1294,16 +1285,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1322,7 +1311,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id715", + "id": "id4", "type": "arrow", }, ], @@ -1331,16 +1320,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1371,7 +1358,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id711": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1387,22 +1374,22 @@ 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, "type": "rectangle", + "version": 7, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 6, }, }, - "id712": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1418,27 +1405,27 @@ 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, "type": "rectangle", + "version": 7, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, "updated": { - "id715": { + "id4": { "deleted": { "endBinding": { - "elementId": "id712", + "elementId": "id1", "fixedPoint": [ "0.50000", 1, @@ -1447,7 +1434,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "gap": 1, }, "startBinding": { - "elementId": "id711", + "elementId": "id0", "fixedPoint": [ 1, "0.50000", @@ -1455,15 +1442,17 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "focus": 0, "gap": 1, }, + "version": 11, }, "inserted": { "endBinding": null, "startBinding": null, + "version": 8, }, }, }, }, - "id": "id719", + "id": "id8", }, ] `; @@ -1492,7 +1481,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", @@ -1550,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, @@ -1592,7 +1580,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", @@ -1603,8 +1591,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "1.30038", - "id": "id725", + "height": "1.36342", + "id": "id5", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1617,17 +1605,15 @@ 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", + "elementId": "id0", "fixedPoint": [ "0.50000", 1, @@ -1641,8 +1627,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, } `; @@ -1653,7 +1639,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], @@ -1662,16 +1648,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1690,7 +1674,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], @@ -1699,16 +1683,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1739,7 +1721,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id725": { + "id5": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -1748,7 +1730,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", @@ -1759,7 +1741,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "11.27227", + "height": "1.36342", "index": "a0", "isDeleted": false, "lastCommittedPoint": null, @@ -1772,17 +1754,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "98.58579", - "11.27227", + 98, + "1.36342", ], ], "roughness": 1, - "roundness": { - "type": 2, - }, + "roundness": null, "startArrowhead": null, "startBinding": { - "elementId": "id720", + "elementId": "id0", "fixedPoint": [ "0.50000", 1, @@ -1794,45 +1774,51 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "strokeStyle": "solid", "strokeWidth": 2, "type": "arrow", - "width": "98.58579", - "x": "0.70711", + "version": 11, + "width": 98, + "x": 1, "y": 0, }, "inserted": { "isDeleted": true, + "version": 8, }, }, }, "updated": { - "id720": { + "id0": { "deleted": { "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], + "version": 12, }, "inserted": { "boundElements": [], + "version": 9, }, }, - "id721": { + "id1": { "deleted": { "boundElements": [ { - "id": "id725", + "id": "id5", "type": "arrow", }, ], + "version": 11, }, "inserted": { "boundElements": [], + "version": 8, }, }, }, }, - "id": "id731", + "id": "id11", }, ] `; @@ -1861,7 +1847,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", @@ -1919,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, @@ -1962,16 +1947,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -1994,16 +1977,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2034,7 +2015,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id732": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2050,22 +2031,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id733": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2081,25 +2062,25 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id735", + "id": "id3", }, ] `; @@ -2128,7 +2109,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", @@ -2183,14 +2164,13 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id740": true, + "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -2224,7 +2204,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], @@ -2233,16 +2213,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2261,7 +2239,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "backgroundColor": "transparent", "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], @@ -2270,16 +2248,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -2301,15 +2277,15 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id737", + "elementId": "id1", "focus": -0, "gap": 1, }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": "374.05754", - "id": "id740", + "height": "370.26975", + "id": "id4", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -2322,8 +2298,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - "502.78936", - "-374.05754", + "498.00000", + "-370.26975", ], ], "roughness": 1, @@ -2332,7 +2308,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "startArrowhead": null, "startBinding": { - "elementId": "id736", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -2342,9 +2318,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", } `; @@ -2366,7 +2342,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id736": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2382,22 +2358,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id737": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2413,34 +2389,34 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id739", + "id": "id3", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id740": true, + "id4": true, }, - "selectedLinearElementId": "id740", + "selectedLinearElementId": "id4", }, "inserted": { "selectedElementIds": {}, @@ -2451,7 +2427,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elements": { "added": {}, "removed": { - "id740": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -2460,14 +2436,14 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id737", + "elementId": "id1", "focus": -0, "gap": 1, }, "fillStyle": "solid", "frameId": null, "groupIds": [], - "height": 0, + "height": "370.26975", "index": "a2", "isDeleted": false, "lastCommittedPoint": null, @@ -2480,8 +2456,8 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl 0, ], [ - 100, - 0, + "498.00000", + "-370.26975", ], ], "roughness": 1, @@ -2490,7 +2466,7 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl }, "startArrowhead": null, "startBinding": { - "elementId": "id736", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -2498,45 +2474,51 @@ 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, }, }, }, "updated": { - "id736": { + "id0": { "deleted": { "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], + "version": 7, }, "inserted": { "boundElements": [], + "version": 4, }, }, - "id737": { + "id1": { "deleted": { "boundElements": [ { - "id": "id740", + "id": "id4", "type": "arrow", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 5, }, }, }, }, - "id": "id744", + "id": "id8", }, ] `; @@ -2565,7 +2547,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", @@ -2623,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, @@ -2662,7 +2643,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id618", + "id": "id5", "type": "text", }, ], @@ -2671,22 +2652,20 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 7, + "version": 6, "width": 100, "x": 10, "y": 10, @@ -2707,7 +2686,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", @@ -2716,9 +2695,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, @@ -2740,7 +2717,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, @@ -2748,7 +2725,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", @@ -2757,9 +2734,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, @@ -2767,7 +2742,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, @@ -2792,48 +2767,17 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id613": { + "id5": { "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": { - "type": 3, - }, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 100, - "x": 10, - "y": 10, - }, - }, - "id614": { - "deleted": { - "containerId": null, - }, - "inserted": { - "containerId": null, + "version": 5, }, }, }, }, - "id": "id622", + "id": "id9", }, ] `; @@ -2864,7 +2808,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", @@ -2922,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, @@ -2961,7 +2904,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id628", + "id": "id5", "type": "text", }, ], @@ -2970,22 +2913,20 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 11, + "version": 7, "width": 100, "x": 10, "y": 10, @@ -2998,7 +2939,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, @@ -3006,7 +2947,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", @@ -3015,9 +2956,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, @@ -3039,7 +2978,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, @@ -3047,7 +2986,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", @@ -3056,9 +2995,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, @@ -3066,7 +3003,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, @@ -3089,35 +3026,23 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "elements": { "added": { - "id624": { + "id1": { "deleted": { - "containerId": "id623", + "containerId": "id0", "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", + "id": "id9", }, ] `; @@ -3148,7 +3073,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", @@ -3206,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, @@ -3245,7 +3169,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id582", + "id": "id5", "type": "text", }, ], @@ -3254,16 +3178,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3282,7 +3204,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, @@ -3290,7 +3212,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", @@ -3299,9 +3221,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, @@ -3331,7 +3251,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", @@ -3340,9 +3260,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, @@ -3375,43 +3293,49 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id577": { + "id0": { "deleted": { "boundElements": [ { - "id": "id582", + "id": "id5", "type": "text", }, ], + "version": 10, }, "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": { "containerId": null, + "version": 6, }, }, }, }, - "id": "id586", + "id": "id9", }, ] `; @@ -3442,7 +3366,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", @@ -3500,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, @@ -3543,16 +3466,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3571,7 +3492,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id588", + "id": "id1", "type": "text", }, ], @@ -3580,16 +3501,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3608,7 +3527,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, @@ -3616,7 +3535,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", @@ -3625,9 +3544,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, @@ -3661,43 +3578,49 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id587": { + "id0": { "deleted": { "boundElements": [], + "version": 8, }, "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", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 7, }, }, }, }, - "id": "id596", + "id": "id9", }, ] `; @@ -3728,7 +3651,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", @@ -3786,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, @@ -3829,16 +3751,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -3865,7 +3785,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", @@ -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, @@ -3909,30 +3827,34 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id568": { + "id0": { "deleted": { "boundElements": [], + "version": 9, }, "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", }, ] `; @@ -3963,7 +3885,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", @@ -4021,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, @@ -4060,7 +3981,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id598", + "id": "id1", "type": "text", }, ], @@ -4069,16 +3990,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4097,7 +4016,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, @@ -4105,7 +4024,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", @@ -4114,9 +4033,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, @@ -4150,7 +4067,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "elements": { "added": {}, "removed": { - "id597": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -4166,34 +4083,36 @@ 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, "type": "rectangle", + "version": 7, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, "updated": { - "id598": { + "id1": { "deleted": { - "containerId": "id597", + "containerId": "id0", + "version": 12, }, "inserted": { "containerId": null, + "version": 9, }, }, }, }, - "id": "id604", + "id": "id7", }, ] `; @@ -4222,7 +4141,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", @@ -4280,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, @@ -4319,7 +4237,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id606", + "id": "id1", "type": "text", }, ], @@ -4328,16 +4246,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4356,7 +4272,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, @@ -4364,7 +4280,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", @@ -4373,9 +4289,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, @@ -4409,13 +4323,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, @@ -4431,15 +4345,14 @@ 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, "text": "que pasa", "textAlign": "left", "type": "text", + "version": 8, "verticalAlign": "top", "width": 80, "x": 15, @@ -4447,26 +4360,29 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, "updated": { - "id605": { + "id0": { "deleted": { "boundElements": [ { - "id": "id606", + "id": "id1", "type": "text", }, ], + "version": 11, }, "inserted": { "boundElements": [], + "version": 8, }, }, }, }, - "id": "id612", + "id": "id7", }, ] `; @@ -4495,7 +4411,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", @@ -4553,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, @@ -4592,7 +4507,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id658", + "id": "id1", "type": "text", }, ], @@ -4601,16 +4516,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4629,7 +4542,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, @@ -4637,7 +4550,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", @@ -4646,9 +4559,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, @@ -4681,21 +4592,23 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id658": { + "id1": { "deleted": { "angle": 0, + "version": 5, "x": 15, "y": 15, }, "inserted": { "angle": 0, + "version": 7, "x": 15, "y": 15, }, }, }, }, - "id": "id663", + "id": "id6", }, ] `; @@ -4726,7 +4639,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", @@ -4784,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, @@ -4823,7 +4735,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id650", + "id": "id1", "type": "text", }, ], @@ -4832,16 +4744,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4860,7 +4770,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, @@ -4868,7 +4778,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", @@ -4877,9 +4787,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, @@ -4914,21 +4822,23 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "added": {}, "removed": {}, "updated": { - "id649": { + "id0": { "deleted": { "angle": 90, + "version": 8, "x": 200, "y": 200, }, "inserted": { "angle": 0, + "version": 7, "x": 10, "y": 10, }, }, }, }, - "id": "id656", + "id": "id7", }, ] `; @@ -4957,7 +4867,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", @@ -5015,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, @@ -5058,16 +4967,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5086,7 +4993,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, @@ -5094,7 +5001,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", @@ -5103,9 +5010,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, @@ -5139,25 +5044,27 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "elements": { "added": {}, "removed": { - "id633": { + "id0": { "deleted": { "boundElements": [], "isDeleted": false, + "version": 8, }, "inserted": { "boundElements": [ { - "id": "id634", + "id": "id1", "type": "text", }, ], "isDeleted": true, + "version": 7, }, }, }, "updated": {}, }, - "id": "id640", + "id": "id7", }, ] `; @@ -5186,7 +5093,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", @@ -5244,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, @@ -5283,7 +5189,7 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and "backgroundColor": "transparent", "boundElements": [ { - "id": "id642", + "id": "id1", "type": "text", }, ], @@ -5292,16 +5198,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5328,7 +5232,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", @@ -5337,9 +5241,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, @@ -5373,20 +5275,22 @@ 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, }, }, }, "updated": {}, }, - "id": "id648", + "id": "id7", }, ] `; @@ -5415,7 +5319,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", @@ -5473,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, @@ -5516,16 +5419,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5548,7 +5449,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, @@ -5556,9 +5457,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, @@ -5589,7 +5488,7 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "elements": { "added": {}, "removed": { - "id746": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -5605,25 +5504,25 @@ 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, "type": "rectangle", + "version": 8, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, "updated": {}, }, - "id": "id755", + "id": "id10", }, { "appState": AppStateDelta { @@ -5636,17 +5535,17 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre "added": {}, "removed": {}, "updated": { - "id746": { + "id1": { "deleted": { - "frameId": "id745", + "version": 10, }, "inserted": { - "frameId": null, + "version": 8, }, }, }, }, - "id": "id756", + "id": "id11", }, ] `; @@ -5675,7 +5574,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", @@ -5730,9 +5629,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { - "id469": true, + "id1": true, }, "resizingElement": null, "scrollX": 0, @@ -5777,22 +5675,20 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id468", + "id": "id0", "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", "updated": 1, - "version": 5, + "version": 4, "width": 100, "x": 0, "y": 0, @@ -5811,22 +5707,20 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id469", + "id": "id1", "index": "a1", "isDeleted": true, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, - "version": 4, + "version": 3, "width": 100, "x": 100, "y": 100, @@ -5846,8 +5740,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, @@ -5862,76 +5756,9 @@ 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": { - "type": 3, - }, - "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": { - "type": 3, - }, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 100, - "x": 100, - "y": 100, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, - "id": "id481", + "id": "id13", }, { "appState": AppStateDelta { @@ -5944,7 +5771,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "inserted": { "editingGroupId": null, "selectedElementIds": { - "id468": true, + "id0": true, }, "selectedGroupIds": { "A": true, @@ -5957,7 +5784,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id482", + "id": "id14", }, { "appState": AppStateDelta { @@ -5969,7 +5796,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "inserted": { "editingGroupId": "A", "selectedElementIds": { - "id469": true, + "id1": true, }, }, }, @@ -5979,7 +5806,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id485", + "id": "id17", }, ] `; @@ -6008,7 +5835,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", @@ -6063,9 +5890,8 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { - "id418": true, + "id8": true, }, "resizingElement": null, "scrollX": 0, @@ -6108,16 +5934,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6140,16 +5964,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6172,16 +5994,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6207,7 +6027,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id410": true, + "id0": true, }, }, "inserted": { @@ -6218,7 +6038,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": { - "id410": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6234,37 +6054,37 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id412", + "id": "id2", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id413": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id410": true, + "id0": true, }, }, }, @@ -6272,41 +6092,9 @@ 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": { - "type": 3, - }, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 10, - "x": 20, - "y": 0, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, - "id": "id428", + "id": "id18", }, { "appState": AppStateDelta { @@ -6319,29 +6107,31 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id413": { + "id3": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 7, }, "inserted": { "backgroundColor": "transparent", + "version": 6, }, }, }, }, - "id": "id429", + "id": "id19", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id418": true, + "id8": true, }, }, "inserted": { "selectedElementIds": { - "id413": true, + "id3": true, }, }, }, @@ -6349,41 +6139,9 @@ 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": { - "type": 3, - }, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 10, - "x": 50, - "y": 50, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, - "id": "id430", + "id": "id20", }, { "appState": AppStateDelta { @@ -6396,19 +6154,21 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id418": { + "id8": { "deleted": { + "version": 7, "x": 50, "y": 50, }, "inserted": { + "version": 6, "x": 30, "y": 30, }, }, }, }, - "id": "id431", + "id": "id21", }, ] `; @@ -6437,7 +6197,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", @@ -6492,17 +6252,16 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "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": {}, @@ -6540,16 +6299,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6572,16 +6329,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6604,16 +6359,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6639,7 +6392,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id432": true, + "id0": true, }, }, "inserted": { @@ -6650,7 +6403,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": { - "id432": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6666,22 +6419,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id433": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6697,22 +6450,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id434": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -6728,37 +6481,37 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 30, "y": 30, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id437", + "id": "id5", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id433": true, + "id1": true, }, }, "inserted": { "selectedElementIds": { - "id432": true, + "id0": true, }, }, }, @@ -6768,14 +6521,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": { @@ -6788,7 +6541,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id451", + "id": "id19", }, ] `; @@ -6817,7 +6570,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", @@ -6875,17 +6628,16 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id452": true, - "id453": true, - "id454": true, - "id455": true, + "id0": true, + "id1": true, + "id2": true, + "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": { @@ -6928,16 +6680,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id452", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6962,16 +6712,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "A", ], "height": 100, - "id": "id453", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -6996,16 +6744,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "B", ], "height": 100, - "id": "id454", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7030,16 +6776,14 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "B", ], "height": 100, - "id": "id455", + "id": "id3", "index": "a3", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7065,8 +6809,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, @@ -7083,15 +6827,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, @@ -7108,7 +6852,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id467", + "id": "id15", }, ] `; @@ -7137,7 +6881,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", @@ -7192,14 +6936,13 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id486": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -7240,7 +6983,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": [ @@ -7291,7 +7034,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id486": true, + "id0": true, }, }, "inserted": { @@ -7302,7 +7045,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "elements": { "added": {}, "removed": { - "id486": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -7344,24 +7087,26 @@ 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, }, }, }, "updated": {}, }, - "id": "id488", + "id": "id2", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "selectedLinearElementId": "id486", + "selectedLinearElementId": "id0", }, "inserted": { "selectedLinearElementId": null, @@ -7373,13 +7118,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, @@ -7391,7 +7136,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id499", + "id": "id13", }, { "appState": AppStateDelta { @@ -7400,7 +7145,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "editingLinearElementId": null, }, "inserted": { - "editingLinearElementId": "id486", + "editingLinearElementId": "id0", }, }, }, @@ -7409,7 +7154,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id500", + "id": "id14", }, ] `; @@ -7438,7 +7183,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", @@ -7493,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, @@ -7536,16 +7280,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7571,7 +7313,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "delta": Delta { "deleted": { "selectedElementIds": { - "id401": true, + "id0": true, }, }, "inserted": { @@ -7582,41 +7324,9 @@ 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": { - "type": 3, - }, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "width": 10, - "x": 10, - "y": 0, - }, - "inserted": { - "isDeleted": true, - }, - }, - }, + "updated": {}, }, - "id": "id408", + "id": "id7", }, { "appState": AppStateDelta { @@ -7629,17 +7339,19 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id401": { + "id0": { "deleted": { "backgroundColor": "#ffec99", + "version": 7, }, "inserted": { "backgroundColor": "transparent", + "version": 6, }, }, }, }, - "id": "id409", + "id": "id8", }, ] `; @@ -7668,7 +7380,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", @@ -7723,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, @@ -7766,16 +7477,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7798,16 +7507,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7830,16 +7537,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -7869,17 +7574,19 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id514": { + "id1": { "deleted": { "index": "a1", + "version": 7, }, "inserted": { "index": "a3", + "version": 6, }, }, }, }, - "id": "id523", + "id": "id10", }, { "appState": AppStateDelta { @@ -7889,16 +7596,17 @@ 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, }, "inserted": { "angle": 0, @@ -7915,21 +7623,21 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 10, "y": 10, }, }, - "id514": { + "id1": { "deleted": { "isDeleted": true, + "version": 8, }, "inserted": { "angle": 0, @@ -7940,27 +7648,27 @@ 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, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 7, "width": 100, "x": 20, "y": 20, }, }, - "id515": { + "id2": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -7977,13 +7685,12 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 30, "y": 30, @@ -7993,7 +7700,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id524", + "id": "id11", }, ] `; @@ -8024,7 +7731,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", @@ -8079,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, @@ -8122,16 +7828,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8154,16 +7858,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8186,16 +7888,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8225,17 +7925,19 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "added": {}, "removed": {}, "updated": { - "id502": { + "id1": { "deleted": { "index": "a1", + "version": 6, }, "inserted": { "index": "Zz", + "version": 5, }, }, }, }, - "id": "id511", + "id": "id10", }, { "appState": AppStateDelta { @@ -8245,16 +7947,17 @@ 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, }, "inserted": { "angle": 0, @@ -8271,21 +7974,21 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 10, "y": 10, }, }, - "id502": { + "id1": { "deleted": { "isDeleted": true, + "version": 7, }, "inserted": { "angle": 0, @@ -8296,27 +7999,27 @@ 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, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", + "version": 6, "width": 100, "x": 20, "y": 20, }, }, - "id503": { + "id2": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -8333,13 +8036,12 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 30, "y": 30, @@ -8349,7 +8051,7 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh "removed": {}, "updated": {}, }, - "id": "id512", + "id": "id11", }, ] `; @@ -8380,7 +8082,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", @@ -8435,18 +8137,17 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "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": {}, @@ -8484,16 +8185,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8516,16 +8215,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -8548,16 +8245,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -8583,7 +8278,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "delta": Delta { "deleted": { "selectedElementIds": { - "id542": true, + "id0": true, }, }, "inserted": { @@ -8594,7 +8289,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id542": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -8610,37 +8305,37 @@ 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, "type": "rectangle", + "version": 8, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, "updated": {}, }, - "id": "id563", + "id": "id21", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id545": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id542": true, + "id0": true, }, }, }, @@ -8648,7 +8343,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id545": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -8664,37 +8359,37 @@ 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, "type": "rectangle", + "version": 8, "width": 10, "x": 30, "y": 30, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, "updated": {}, }, - "id": "id564", + "id": "id22", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id542": true, + "id0": true, }, }, "inserted": { "selectedElementIds": { - "id545": true, + "id3": true, }, }, }, @@ -8704,14 +8399,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": { @@ -8724,7 +8419,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "removed": {}, "updated": {}, }, - "id": "id566", + "id": "id24", }, { "appState": AppStateDelta { @@ -8737,29 +8432,33 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "added": {}, "removed": {}, "updated": { - "id542": { + "id0": { "deleted": { + "version": 9, "x": 90, "y": 90, }, "inserted": { + "version": 8, "x": 10, "y": 10, }, }, - "id545": { + "id3": { "deleted": { + "version": 9, "x": 110, "y": 110, }, "inserted": { + "version": 8, "x": 30, "y": 30, }, }, }, }, - "id": "id567", + "id": "id25", }, ] `; @@ -8788,7 +8487,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", @@ -8843,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, @@ -8886,7 +8584,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": [ @@ -8945,16 +8643,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -8985,7 +8681,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id525": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9035,18 +8731,20 @@ 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, }, }, }, "updated": {}, }, - "id": "id530", + "id": "id5", }, ] `; @@ -9075,7 +8773,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", @@ -9130,14 +8828,13 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id531": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9175,16 +8872,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9207,16 +8902,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -9242,7 +8935,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "delta": Delta { "deleted": { "selectedElementIds": { - "id531": true, + "id0": true, }, }, "inserted": { @@ -9253,7 +8946,7 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "elements": { "added": {}, "removed": { - "id531": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9269,25 +8962,25 @@ 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, "type": "rectangle", + "version": 8, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 7, }, }, }, "updated": {}, }, - "id": "id540", + "id": "id9", }, { "appState": AppStateDelta { @@ -9300,19 +8993,21 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte "added": {}, "removed": {}, "updated": { - "id531": { + "id0": { "deleted": { "height": 90, + "version": 9, "width": 90, }, "inserted": { "height": 10, + "version": 8, "width": 10, }, }, }, }, - "id": "id541", + "id": "id10", }, ] `; @@ -9341,7 +9036,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", @@ -9396,14 +9091,13 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id333": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9441,16 +9135,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9473,16 +9165,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#a5d8ff", "strokeStyle": "solid", "strokeWidth": 2, @@ -9512,17 +9202,19 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "added": {}, "removed": {}, "updated": { - "id333": { + "id0": { "deleted": { "backgroundColor": "transparent", + "version": 7, }, "inserted": { "backgroundColor": "#ffc9c9", + "version": 6, }, }, }, }, - "id": "id341", + "id": "id8", }, ] `; @@ -9534,7 +9226,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "delta": Delta { "deleted": { "selectedElementIds": { - "id333": true, + "id0": true, }, }, "inserted": { @@ -9545,7 +9237,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "elements": { "added": {}, "removed": { - "id333": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9561,25 +9253,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id335", + "id": "id2", }, ] `; @@ -9608,7 +9300,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", @@ -9663,14 +9355,13 @@ exports[`history > multiplayer undo/redo > should not override remote changes on }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id342": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -9708,16 +9399,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#ffec99", "strokeStyle": "solid", "strokeWidth": 2, @@ -9743,7 +9432,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "delta": Delta { "deleted": { "selectedElementIds": { - "id342": true, + "id0": true, }, }, "inserted": { @@ -9754,7 +9443,7 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "elements": { "added": {}, "removed": { - "id342": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -9770,25 +9459,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id344", + "id": "id2", }, { "appState": AppStateDelta { @@ -9801,17 +9490,19 @@ exports[`history > multiplayer undo/redo > should not override remote changes on "added": {}, "removed": {}, "updated": { - "id342": { + "id0": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 7, }, "inserted": { "backgroundColor": "transparent", + "version": 6, }, }, }, }, - "id": "id348", + "id": "id6", }, ] `; @@ -9840,7 +9531,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", @@ -9898,7 +9589,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9944,16 +9634,14 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id371", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -9979,16 +9667,14 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id372", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10013,16 +9699,14 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id375", + "id": "id4", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10047,16 +9731,14 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "B", ], "height": 100, - "id": "id376", + "id": "id5", "index": "a3", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10088,31 +9770,35 @@ exports[`history > multiplayer undo/redo > should override remotely added groups "added": {}, "removed": {}, "updated": { - "id371": { + "id0": { "deleted": { "groupIds": [ "A", "B", ], + "version": 6, }, "inserted": { "groupIds": [], + "version": 5, }, }, - "id372": { + "id1": { "deleted": { "groupIds": [ "A", "B", ], + "version": 6, }, "inserted": { "groupIds": [], + "version": 5, }, }, }, }, - "id": "id378", + "id": "id7", }, ] `; @@ -10141,7 +9827,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", @@ -10196,14 +9882,13 @@ exports[`history > multiplayer undo/redo > should override remotely added points }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id379": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -10244,7 +9929,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": [ @@ -10307,7 +9992,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "delta": Delta { "deleted": { "selectedElementIds": { - "id379": true, + "id0": true, }, }, "inserted": { @@ -10318,7 +10003,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "elements": { "added": {}, "removed": { - "id379": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -10360,18 +10045,20 @@ 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, }, }, }, "updated": {}, }, - "id": "id389", + "id": "id10", }, { "appState": AppStateDelta { @@ -10384,7 +10071,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "added": {}, "removed": {}, "updated": { - "id379": { + "id0": { "deleted": { "height": 30, "lastCommittedPoint": [ @@ -10413,6 +10100,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points 20, ], ], + "version": 13, "width": 30, }, "inserted": { @@ -10431,18 +10119,19 @@ exports[`history > multiplayer undo/redo > should override remotely added points 10, ], ], + "version": 12, "width": 10, }, }, }, }, - "id": "id390", + "id": "id11", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "selectedLinearElementId": "id379", + "selectedLinearElementId": "id0", }, "inserted": { "selectedLinearElementId": null, @@ -10454,7 +10143,7 @@ exports[`history > multiplayer undo/redo > should override remotely added points "removed": {}, "updated": {}, }, - "id": "id391", + "id": "id12", }, ] `; @@ -10483,7 +10172,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", @@ -10538,7 +10227,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10581,16 +10269,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -10616,7 +10302,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "delta": Delta { "deleted": { "selectedElementIds": { - "id392": true, + "id0": true, }, }, "inserted": { @@ -10627,7 +10313,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "elements": { "added": {}, "removed": { - "id392": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "#ffec99", @@ -10643,25 +10329,25 @@ 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, "type": "rectangle", + "version": 7, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, "updated": {}, }, - "id": "id399", + "id": "id7", }, { "appState": AppStateDelta { @@ -10671,7 +10357,7 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme }, "inserted": { "selectedElementIds": { - "id392": true, + "id0": true, }, }, }, @@ -10679,18 +10365,9 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme "elements": { "added": {}, "removed": {}, - "updated": { - "id392": { - "deleted": { - "isDeleted": false, - }, - "inserted": { - "isDeleted": false, - }, - }, - }, + "updated": {}, }, - "id": "id400", + "id": "id8", }, ] `; @@ -10719,7 +10396,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", @@ -10777,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, @@ -10827,9 +10503,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 +10533,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 +10591,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", @@ -10963,6 +10633,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "6Rm4g567UQM4WjLwej2Vc": { "deleted": { "isDeleted": true, + "version": 3, }, "inserted": { "angle": 0, @@ -11007,9 +10678,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", @@ -11025,6 +10694,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", @@ -11036,6 +10706,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "KPrBI4g_v9qUB1XxYLgSz": { "deleted": { "boundElements": [], + "version": 3, }, "inserted": { "boundElements": [ @@ -11044,11 +10715,13 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "type": "arrow", }, ], + "version": 2, }, }, "u2JGnnmoJ0VATV4vCNJE5": { "deleted": { "boundElements": [], + "version": 3, }, "inserted": { "boundElements": [ @@ -11057,11 +10730,12 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "type": "arrow", }, ], + "version": 2, }, }, }, }, - "id": "id369", + "id": "id7", }, { "appState": AppStateDelta { @@ -11075,6 +10749,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "KPrBI4g_v9qUB1XxYLgSz": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -11091,21 +10766,21 @@ 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, "type": "rectangle", + "version": 3, "width": 157, - "x": 600, - "y": 0, + "x": 873, + "y": 212, }, }, "u2JGnnmoJ0VATV4vCNJE5": { "deleted": { "isDeleted": true, + "version": 4, }, "inserted": { "angle": 0, @@ -11122,13 +10797,12 @@ 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, "type": "diamond", + "version": 3, "width": 124, "x": 1152, "y": 516, @@ -11138,7 +10812,7 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o "removed": {}, "updated": {}, }, - "id": "id370", + "id": "id8", }, ] `; @@ -11169,7 +10843,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", @@ -11224,14 +10898,13 @@ exports[`history > multiplayer undo/redo > should update history entries after r }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id349": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11269,16 +10942,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11308,17 +10979,19 @@ exports[`history > multiplayer undo/redo > should update history entries after r "added": {}, "removed": {}, "updated": { - "id349": { + "id0": { "deleted": { "backgroundColor": "#d0bfff", + "version": 12, }, "inserted": { "backgroundColor": "#ffec99", + "version": 11, }, }, }, }, - "id": "id360", + "id": "id11", }, { "appState": AppStateDelta { @@ -11331,17 +11004,19 @@ exports[`history > multiplayer undo/redo > should update history entries after r "added": {}, "removed": {}, "updated": { - "id349": { + "id0": { "deleted": { "backgroundColor": "transparent", + "version": 13, }, "inserted": { "backgroundColor": "#d0bfff", + "version": 12, }, }, }, }, - "id": "id361", + "id": "id12", }, ] `; @@ -11353,7 +11028,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "delta": Delta { "deleted": { "selectedElementIds": { - "id349": true, + "id0": true, }, }, "inserted": { @@ -11364,7 +11039,7 @@ exports[`history > multiplayer undo/redo > should update history entries after r "elements": { "added": {}, "removed": { - "id349": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -11380,25 +11055,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id351", + "id": "id2", }, ] `; @@ -11427,7 +11102,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", @@ -11482,7 +11157,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -11532,9 +11206,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, @@ -11557,16 +11229,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11593,16 +11263,17 @@ 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, }, "inserted": { "angle": 0, @@ -11619,13 +11290,12 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, @@ -11635,7 +11305,7 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should "removed": {}, "updated": {}, }, - "id": "id332", + "id": "id4", }, ] `; @@ -11666,7 +11336,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", @@ -11721,14 +11391,13 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id50": true, + "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -11766,16 +11435,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11798,16 +11465,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -11833,7 +11498,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "delta": Delta { "deleted": { "selectedElementIds": { - "id50": true, + "id4": true, }, }, "inserted": { @@ -11844,7 +11509,7 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme "elements": { "added": {}, "removed": { - "id50": { + "id4": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -11860,25 +11525,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 20, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id52", + "id": "id6", }, ] `; @@ -11907,7 +11572,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", @@ -11962,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, @@ -12005,16 +11669,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12037,7 +11699,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": [ @@ -12091,7 +11753,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": [ @@ -12148,7 +11810,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "delta": Delta { "deleted": { "selectedElementIds": { - "id148": true, + "id0": true, }, }, "inserted": { @@ -12159,7 +11821,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "elements": { "added": {}, "removed": { - "id148": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -12175,25 +11837,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": -10, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id150", + "id": "id2", }, { "appState": AppStateDelta { @@ -12203,7 +11865,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f }, "inserted": { "selectedElementIds": { - "id148": true, + "id0": true, }, }, }, @@ -12213,7 +11875,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "removed": {}, "updated": {}, }, - "id": "id152", + "id": "id4", }, { "appState": AppStateDelta { @@ -12225,7 +11887,7 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f "elements": { "added": {}, "removed": { - "id157": { + "id9": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -12270,23 +11932,25 @@ 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, }, }, }, "updated": {}, }, - "id": "id159", + "id": "id11", }, ] `; -exports[`history > singleplayer undo/redo > should create new history entry on scene import via drag&drop > [end of test] appState 1`] = ` +exports[`history > singleplayer undo/redo > should create new history entry on embeddable link drag&drop > [end of test] appState 1`] = ` { "activeEmbeddable": null, "activeLockedId": null, @@ -12310,7 +11974,871 @@ 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", + "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, + "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, + "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, + "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": 5, + "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": 5, + "width": 318, + "x": -159, + "y": "-167.50000", + }, + "inserted": { + "isDeleted": true, + "version": 4, + }, + }, + }, + "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, + "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": 5, + "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": 5, + "width": 56, + "x": -28, + "y": "-38.50000", + }, + "inserted": { + "isDeleted": true, + "version": 4, + }, + }, + }, + "updated": {}, + }, + "id": "id4", + }, +] +`; + +exports[`history > singleplayer undo/redo > should create new history entry on scene import via 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", @@ -12368,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, @@ -12418,9 +12945,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 +12974,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, @@ -12488,9 +13011,11 @@ exports[`history > singleplayer undo/redo > should create new history entry on s "A": { "deleted": { "isDeleted": true, + "version": 5, }, "inserted": { "isDeleted": false, + "version": 4, }, }, }, @@ -12510,25 +13035,25 @@ 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, "type": "rectangle", + "version": 5, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, "updated": {}, }, - "id": "id80", + "id": "id5", }, ] `; @@ -12557,7 +13082,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", @@ -12612,14 +13137,13 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id323": true, + "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12664,9 +13188,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, @@ -12689,16 +13211,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12724,7 +13244,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "delta": Delta { "deleted": { "selectedElementIds": { - "id323": true, + "id1": true, }, }, "inserted": { @@ -12735,7 +13255,7 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe "elements": { "added": {}, "removed": { - "id323": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -12751,25 +13271,25 @@ 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, "type": "rectangle", + "version": 5, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, "updated": {}, }, - "id": "id327", + "id": "id5", }, ] `; @@ -12798,7 +13318,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", @@ -12853,14 +13373,13 @@ exports[`history > singleplayer undo/redo > should end up with no history entry }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id70": true, + "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -12905,9 +13424,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, @@ -12930,16 +13447,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -12965,7 +13480,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "delta": Delta { "deleted": { "selectedElementIds": { - "id70": true, + "id1": true, }, }, "inserted": { @@ -12976,7 +13491,7 @@ exports[`history > singleplayer undo/redo > should end up with no history entry "elements": { "added": {}, "removed": { - "id70": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -12992,25 +13507,25 @@ 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, "type": "rectangle", + "version": 5, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, "updated": {}, }, - "id": "id74", + "id": "id5", }, ] `; @@ -13039,7 +13554,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", @@ -13094,9 +13609,8 @@ exports[`history > singleplayer undo/redo > should iterate through the history w }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { - "id53": true, + "id0": true, }, "resizingElement": null, "scrollX": 0, @@ -13139,16 +13653,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13172,7 +13684,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "delta": Delta { "deleted": { "selectedElementIds": { - "id53": true, + "id0": true, }, }, "inserted": { @@ -13185,14 +13697,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": { @@ -13205,7 +13717,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "removed": {}, "updated": {}, }, - "id": "id67", + "id": "id14", }, { "appState": AppStateDelta { @@ -13215,16 +13727,17 @@ 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, }, "inserted": { "angle": 0, @@ -13241,13 +13754,12 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, @@ -13257,7 +13769,7 @@ exports[`history > singleplayer undo/redo > should iterate through the history w "removed": {}, "updated": {}, }, - "id": "id68", + "id": "id15", }, ] `; @@ -13288,7 +13800,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", @@ -13343,14 +13855,13 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id18": true, + "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -13388,16 +13899,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13420,16 +13929,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13455,7 +13962,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "delta": Delta { "deleted": { "selectedElementIds": { - "id15": true, + "id0": true, }, }, "inserted": { @@ -13466,7 +13973,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "elements": { "added": {}, "removed": { - "id15": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13482,25 +13989,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id17", + "id": "id2", }, { "appState": AppStateDelta { @@ -13510,7 +14017,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s }, "inserted": { "selectedElementIds": { - "id15": true, + "id0": true, }, }, }, @@ -13520,14 +14027,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": { @@ -13540,19 +14047,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, }, }, }, @@ -13560,7 +14067,7 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s "elements": { "added": {}, "removed": { - "id18": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -13576,25 +14083,25 @@ 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, "type": "rectangle", + "version": 5, "width": 10, "x": 20, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, "updated": {}, }, - "id": "id28", + "id": "id13", }, ] `; @@ -13623,7 +14130,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", @@ -13681,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, @@ -13731,9 +14237,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 +14299,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", @@ -13850,15 +14354,14 @@ exports[`history > singleplayer undo/redo > should not end up with history entry }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id4": true, - "id5": true, + "id0": true, + "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": { @@ -13900,16 +14403,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13934,16 +14435,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13969,8 +14468,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, @@ -13985,7 +14484,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "elements": { "added": {}, "removed": { - "id4": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14003,22 +14502,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id5": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14036,25 +14535,25 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id8", + "id": "id4", }, ] `; @@ -14083,7 +14582,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", @@ -14141,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, @@ -14184,16 +14682,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14216,16 +14712,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14256,7 +14750,7 @@ exports[`history > singleplayer undo/redo > should not end up with history entry "elements": { "added": {}, "removed": { - "id10": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14272,22 +14766,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id11": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14303,29 +14797,181 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id13", + "id": "id3", }, ] `; +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, @@ -14350,7 +14996,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", @@ -14405,16 +15051,15 @@ exports[`history > singleplayer undo/redo > should not override appstate changes }, "penDetected": false, "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": {}, @@ -14452,16 +15097,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14491,17 +15134,19 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "added": {}, "removed": {}, "updated": { - "id29": { + "id0": { "deleted": { "backgroundColor": "#ffc9c9", + "version": 10, }, "inserted": { "backgroundColor": "#a5d8ff", + "version": 9, }, }, }, }, - "id": "id43", + "id": "id14", }, { "appState": AppStateDelta { @@ -14514,24 +15159,26 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "added": {}, "removed": {}, "updated": { - "id29": { + "id0": { "deleted": { "backgroundColor": "transparent", + "version": 11, }, "inserted": { "backgroundColor": "#ffc9c9", + "version": 10, }, }, }, }, - "id": "id44", + "id": "id15", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id29": true, + "id0": true, }, }, "inserted": { @@ -14544,7 +15191,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "removed": {}, "updated": {}, }, - "id": "id45", + "id": "id16", }, ] `; @@ -14556,7 +15203,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "delta": Delta { "deleted": { "selectedElementIds": { - "id29": true, + "id0": true, }, }, "inserted": { @@ -14567,7 +15214,7 @@ exports[`history > singleplayer undo/redo > should not override appstate changes "elements": { "added": {}, "removed": { - "id29": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -14583,25 +15230,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id31", + "id": "id2", }, ] `; @@ -14630,7 +15277,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", @@ -14688,7 +15335,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14745,7 +15391,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "removed": {}, "updated": {}, }, - "id": "id88", + "id": "id7", }, { "appState": AppStateDelta { @@ -14763,7 +15409,7 @@ exports[`history > singleplayer undo/redo > should support appstate name or view "removed": {}, "updated": {}, }, - "id": "id89", + "id": "id8", }, ] `; @@ -14792,7 +15438,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", @@ -14847,16 +15493,15 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "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": {}, @@ -14890,11 +15535,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", }, ], @@ -14903,16 +15548,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id230", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14931,7 +15574,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, @@ -14939,7 +15582,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", @@ -14948,9 +15591,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, @@ -14972,7 +15613,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], @@ -14981,16 +15622,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id232", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15012,7 +15651,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id232", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -15020,7 +15659,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, @@ -15033,7 +15672,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -15043,7 +15682,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id230", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -15053,8 +15692,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, } `; @@ -15070,9 +15709,9 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "delta": Delta { "deleted": { "selectedElementIds": { - "id243": true, + "id13": true, }, - "selectedLinearElementId": "id243", + "selectedLinearElementId": "id13", }, "inserted": { "selectedElementIds": {}, @@ -15083,65 +15722,59 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id243": { + "id13": { "deleted": { "isDeleted": false, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], + "version": 10, }, "inserted": { "isDeleted": true, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], + "version": 7, }, }, }, "updated": { - "id230": { + "id0": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 5, }, }, - "id232": { + "id1": { + "deleted": { + "version": 6, + }, + "inserted": { + "version": 4, + }, + }, + "id2": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], + "version": 7, }, "inserted": { "boundElements": [], + "version": 4, }, }, }, }, - "id": "id248", + "id": "id18", }, ] `; @@ -15158,7 +15791,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id230": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15174,22 +15807,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id231": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -15211,15 +15844,14 @@ 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, "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15227,9 +15859,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id232": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15245,32 +15878,32 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id234", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id230": true, + "id0": true, }, }, "inserted": { @@ -15283,14 +15916,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": { @@ -15303,7 +15936,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id240", + "id": "id10", }, { "appState": AppStateDelta { @@ -15313,7 +15946,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id231": true, + "id1": true, }, }, }, @@ -15322,24 +15955,27 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id230": { + "id0": { "deleted": { "boundElements": [ { - "id": "id231", + "id": "id1", "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, - "id231": { + "id1": { "deleted": { - "containerId": "id230", + "containerId": "id0", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -15349,6 +15985,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15357,20 +15994,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, }, @@ -15379,7 +16016,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id243": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15388,7 +16025,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id232", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -15418,7 +16055,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id230", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -15426,45 +16063,51 @@ 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, }, }, }, "updated": { - "id230": { + "id0": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], + "version": 4, }, "inserted": { "boundElements": [], + "version": 3, }, }, - "id232": { + "id2": { "deleted": { "boundElements": [ { - "id": "id243", + "id": "id13", "type": "arrow", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, }, }, - "id": "id245", + "id": "id15", }, ] `; @@ -15493,7 +16136,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", @@ -15548,16 +16191,15 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "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": {}, @@ -15591,11 +16233,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", }, ], @@ -15604,16 +16246,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id212", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15632,7 +16272,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, @@ -15640,7 +16280,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", @@ -15649,9 +16289,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, @@ -15673,7 +16311,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id225", + "id": "id13", "type": "arrow", }, ], @@ -15682,16 +16320,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id214", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -15713,7 +16349,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id214", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -15721,7 +16357,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, @@ -15734,7 +16370,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -15744,7 +16380,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id212", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -15754,8 +16390,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, } `; @@ -15778,7 +16414,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id212": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15794,22 +16430,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id213": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -15831,15 +16467,14 @@ 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, "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15847,9 +16482,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id214": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -15865,32 +16501,32 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id216", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id212": true, + "id0": true, }, }, "inserted": { @@ -15903,14 +16539,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": { @@ -15923,7 +16559,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id222", + "id": "id10", }, { "appState": AppStateDelta { @@ -15933,7 +16569,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id213": true, + "id1": true, }, }, }, @@ -15942,24 +16578,27 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id212": { + "id0": { "deleted": { "boundElements": [ { - "id": "id213", + "id": "id1", "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, - "id213": { + "id1": { "deleted": { - "containerId": "id212", + "containerId": "id0", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -15969,6 +16608,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -15977,20 +16617,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, }, @@ -15999,7 +16639,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id225": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16008,7 +16648,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id214", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16028,7 +16668,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + 98, 0, ], ], @@ -16038,7 +16678,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id212", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16046,45 +16686,59 @@ 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, }, }, }, "updated": { - "id212": { + "id0": { "deleted": { "boundElements": [ { - "id": "id225", + "id": "id13", "type": "arrow", }, ], + "version": 8, }, "inserted": { "boundElements": [], + "version": 5, }, }, - "id214": { + "id1": { + "deleted": { + "version": 8, + }, + "inserted": { + "version": 6, + }, + }, + "id2": { "deleted": { "boundElements": [ { - "id": "id225", + "id": "id13", "type": "arrow", }, ], + "version": 7, }, "inserted": { "boundElements": [], + "version": 4, }, }, }, }, - "id": "id229", + "id": "id17", }, ] `; @@ -16113,7 +16767,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", @@ -16168,16 +16822,15 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "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": {}, @@ -16211,11 +16864,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", }, ], @@ -16224,16 +16877,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id249", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16252,7 +16903,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, @@ -16260,7 +16911,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", @@ -16269,9 +16920,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, @@ -16293,7 +16942,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id262", + "id": "id13", "type": "arrow", }, ], @@ -16302,16 +16951,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id251", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16333,7 +16980,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id251", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16341,7 +16988,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, @@ -16354,7 +17001,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -16364,7 +17011,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id249", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16374,8 +17021,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, } `; @@ -16398,7 +17045,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id249": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16414,22 +17061,22 @@ 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, "type": "rectangle", + "version": 8, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 7, }, }, - "id250": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -16451,15 +17098,14 @@ 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, "text": "ola", "textAlign": "left", "type": "text", + "version": 9, "verticalAlign": "top", "width": 100, "x": -200, @@ -16467,9 +17113,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 8, }, }, - "id251": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16485,32 +17132,32 @@ 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, "type": "rectangle", + "version": 6, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, "updated": {}, }, - "id": "id270", + "id": "id21", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id249": true, + "id0": true, }, }, "inserted": { @@ -16523,14 +17170,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": { @@ -16543,7 +17190,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id272", + "id": "id23", }, { "appState": AppStateDelta { @@ -16553,7 +17200,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id250": true, + "id1": true, }, }, }, @@ -16562,24 +17209,27 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id249": { + "id0": { "deleted": { "boundElements": [ { - "id": "id250", + "id": "id1", "type": "text", }, ], + "version": 9, }, "inserted": { "boundElements": [], + "version": 8, }, }, - "id250": { + "id1": { "deleted": { - "containerId": "id249", + "containerId": "id0", "height": 25, "textAlign": "center", + "version": 10, "verticalAlign": "middle", "width": 30, "x": -65, @@ -16589,6 +17239,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 9, "verticalAlign": "top", "width": 100, "x": -200, @@ -16597,20 +17248,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, }, @@ -16619,7 +17270,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id262": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -16628,7 +17279,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id251", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16648,7 +17299,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - 100, + 98, 0, ], ], @@ -16658,7 +17309,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id249", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16666,45 +17317,59 @@ 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, }, }, }, "updated": { - "id249": { + "id0": { "deleted": { "boundElements": [ { - "id": "id262", + "id": "id13", "type": "arrow", }, ], + "version": 12, }, "inserted": { "boundElements": [], + "version": 9, }, }, - "id251": { + "id1": { + "deleted": { + "version": 12, + }, + "inserted": { + "version": 10, + }, + }, + "id2": { "deleted": { "boundElements": [ { - "id": "id262", + "id": "id13", "type": "arrow", }, ], + "version": 9, }, "inserted": { "boundElements": [], + "version": 6, }, }, }, }, - "id": "id274", + "id": "id25", }, ] `; @@ -16733,7 +17398,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", @@ -16788,14 +17453,13 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, "scrollY": 0, "searchMatches": null, "selectedElementIds": { - "id275": true, + "id0": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": {}, @@ -16829,11 +17493,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", }, ], @@ -16842,16 +17506,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id275", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16870,7 +17532,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, @@ -16878,7 +17540,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", @@ -16887,9 +17549,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, @@ -16911,7 +17571,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, ], @@ -16920,16 +17580,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id277", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -16951,7 +17609,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id277", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -16959,7 +17617,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, @@ -16972,7 +17630,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -16982,7 +17640,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id275", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -16992,8 +17650,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, } `; @@ -17009,7 +17667,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "delta": Delta { "deleted": { "selectedElementIds": { - "id275": true, + "id0": true, }, }, "inserted": { @@ -17020,59 +17678,53 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id275": { + "id0": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, - "id276": { + "id1": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, "updated": { - "id288": { + "id13": { "deleted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": { - "elementId": "id275", + "elementId": "id0", "focus": 0, "gap": 1, }, + "version": 10, }, "inserted": { - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": null, + "version": 7, + }, + }, + "id2": { + "deleted": { + "version": 5, + }, + "inserted": { + "version": 3, }, }, }, }, - "id": "id296", + "id": "id21", }, ] `; @@ -17089,7 +17741,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id275": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17105,22 +17757,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id276": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -17142,15 +17794,14 @@ 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, "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -17158,9 +17809,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id277": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17176,32 +17828,32 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id279", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id275": true, + "id0": true, }, }, "inserted": { @@ -17214,14 +17866,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": { @@ -17234,7 +17886,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id285", + "id": "id10", }, { "appState": AppStateDelta { @@ -17244,7 +17896,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id276": true, + "id1": true, }, }, }, @@ -17253,24 +17905,27 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id275": { + "id0": { "deleted": { "boundElements": [ { - "id": "id276", + "id": "id1", "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, - "id276": { + "id1": { "deleted": { - "containerId": "id275", + "containerId": "id0", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -17280,6 +17935,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -17288,20 +17944,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, }, @@ -17310,7 +17966,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id288": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17319,7 +17975,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id277", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -17349,7 +18005,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id275", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -17357,60 +18013,66 @@ 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, }, }, }, "updated": { - "id275": { + "id0": { "deleted": { "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, ], + "version": 4, }, "inserted": { "boundElements": [], + "version": 3, }, }, - "id277": { + "id2": { "deleted": { "boundElements": [ { - "id": "id288", + "id": "id13", "type": "arrow", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, }, }, - "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", }, }, }, @@ -17419,7 +18081,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id293", + "id": "id18", }, ] `; @@ -17448,7 +18110,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", @@ -17503,17 +18165,16 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "penDetected": false, "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": {}, @@ -17547,11 +18208,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", }, ], @@ -17560,16 +18221,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id297", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17588,7 +18247,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, @@ -17596,7 +18255,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", @@ -17605,9 +18264,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, @@ -17629,7 +18286,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "backgroundColor": "transparent", "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, ], @@ -17638,16 +18295,14 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "frameId": null, "groupIds": [], "height": 100, - "id": "id299", + "id": "id2", "index": "a2", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -17669,7 +18324,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id299", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -17677,7 +18332,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, @@ -17690,7 +18345,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding 0, ], [ - "98.58579", + 98, 0, ], ], @@ -17700,7 +18355,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id297", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -17710,8 +18365,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, } `; @@ -17727,8 +18382,8 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "delta": Delta { "deleted": { "selectedElementIds": { - "id297": true, - "id299": true, + "id0": true, + "id2": true, }, }, "inserted": { @@ -17739,73 +18394,61 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id297": { + "id0": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, - "id298": { + "id1": { "deleted": { "isDeleted": false, + "version": 8, }, "inserted": { "isDeleted": true, + "version": 5, }, }, - "id299": { + "id2": { "deleted": { "isDeleted": false, + "version": 5, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, "updated": { - "id310": { + "id13": { "deleted": { "endBinding": { - "elementId": "id299", + "elementId": "id2", "focus": -0, "gap": 1, }, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": { - "elementId": "id297", + "elementId": "id0", "focus": 0, "gap": 1, }, + "version": 11, }, "inserted": { "endBinding": null, - "points": [ - [ - 0, - 0, - ], - [ - 100, - 0, - ], - ], "startBinding": null, + "version": 8, }, }, }, }, - "id": "id321", + "id": "id24", }, ] `; @@ -17822,7 +18465,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id297": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17838,22 +18481,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": -100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id298": { + "id1": { "deleted": { "angle": 0, "autoResize": true, @@ -17875,15 +18518,14 @@ 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, "text": "ola", "textAlign": "left", "type": "text", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -17891,9 +18533,10 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id299": { + "id2": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -17909,32 +18552,32 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": -50, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "updated": {}, }, - "id": "id301", + "id": "id4", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id297": true, + "id0": true, }, }, "inserted": { @@ -17947,14 +18590,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": { @@ -17967,7 +18610,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id307", + "id": "id10", }, { "appState": AppStateDelta { @@ -17977,7 +18620,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "inserted": { "selectedElementIds": { - "id298": true, + "id1": true, }, }, }, @@ -17986,24 +18629,27 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "added": {}, "removed": {}, "updated": { - "id297": { + "id0": { "deleted": { "boundElements": [ { - "id": "id298", + "id": "id1", "type": "text", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, - "id298": { + "id1": { "deleted": { - "containerId": "id297", + "containerId": "id0", "height": 25, "textAlign": "center", + "version": 4, "verticalAlign": "middle", "width": 30, "x": -65, @@ -18013,6 +18659,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "containerId": null, "height": 100, "textAlign": "left", + "version": 2, "verticalAlign": "top", "width": 100, "x": -200, @@ -18021,20 +18668,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, }, @@ -18043,7 +18690,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elements": { "added": {}, "removed": { - "id310": { + "id13": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18052,7 +18699,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "elbowed": false, "endArrowhead": "arrow", "endBinding": { - "elementId": "id299", + "elementId": "id2", "focus": -0, "gap": 1, }, @@ -18082,7 +18729,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding }, "startArrowhead": null, "startBinding": { - "elementId": "id297", + "elementId": "id0", "focus": 0, "gap": 1, }, @@ -18090,60 +18737,66 @@ 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, }, }, }, "updated": { - "id297": { + "id0": { "deleted": { "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, ], + "version": 4, }, "inserted": { "boundElements": [], + "version": 3, }, }, - "id299": { + "id2": { "deleted": { "boundElements": [ { - "id": "id310", + "id": "id13", "type": "arrow", }, ], + "version": 3, }, "inserted": { "boundElements": [], + "version": 2, }, }, }, }, - "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", }, }, }, @@ -18152,14 +18805,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": { @@ -18172,7 +18825,7 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding "removed": {}, "updated": {}, }, - "id": "id318", + "id": "id21", }, ] `; @@ -18201,7 +18854,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", @@ -18256,17 +18909,16 @@ exports[`history > singleplayer undo/redo > should support changes in elements' }, "penDetected": false, "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": {}, @@ -18304,16 +18956,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18336,16 +18986,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18368,16 +19016,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18403,7 +19049,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "delta": Delta { "deleted": { "selectedElementIds": { - "id189": true, + "id0": true, }, }, "inserted": { @@ -18414,7 +19060,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "elements": { "added": {}, "removed": { - "id189": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18430,37 +19076,37 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id191", + "id": "id2", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id192": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id189": true, + "id0": true, }, }, }, @@ -18468,7 +19114,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "elements": { "added": {}, "removed": { - "id192": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18484,37 +19130,37 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id194", + "id": "id5", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id195": true, + "id6": true, }, }, "inserted": { "selectedElementIds": { - "id192": true, + "id3": true, }, }, }, @@ -18522,7 +19168,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "elements": { "added": {}, "removed": { - "id195": { + "id6": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -18538,25 +19184,25 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 40, "y": 40, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, "updated": {}, }, - "id": "id197", + "id": "id8", }, { "appState": AppStateDelta { @@ -18569,29 +19215,31 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "added": {}, "removed": {}, "updated": { - "id195": { + "id6": { "deleted": { "index": "a0V", + "version": 6, }, "inserted": { "index": "a2", + "version": 5, }, }, }, }, - "id": "id201", + "id": "id12", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id189": true, + "id0": true, }, }, "inserted": { "selectedElementIds": { - "id195": true, + "id6": true, }, }, }, @@ -18601,14 +19249,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": { @@ -18621,7 +19269,7 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "removed": {}, "updated": {}, }, - "id": "id207", + "id": "id18", }, { "appState": AppStateDelta { @@ -18634,25 +19282,29 @@ exports[`history > singleplayer undo/redo > should support changes in elements' "added": {}, "removed": {}, "updated": { - "id189": { + "id0": { "deleted": { "index": "a2", + "version": 7, }, "inserted": { "index": "Zz", + "version": 6, }, }, - "id195": { + "id6": { "deleted": { "index": "a3", + "version": 10, }, "inserted": { "index": "a0", + "version": 9, }, }, }, }, - "id": "id211", + "id": "id22", }, ] `; @@ -18681,7 +19333,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", @@ -18736,21 +19388,20 @@ exports[`history > singleplayer undo/redo > should support duplication of groups }, "penDetected": false, "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, @@ -18788,16 +19439,14 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "A", ], "height": 100, - "id": "id160", + "id": "id0", "index": "a0", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18822,16 +19471,14 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "A", ], "height": 100, - "id": "id161", + "id": "id1", "index": "a1", "isDeleted": false, "link": null, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18853,19 +19500,17 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18887,19 +19532,17 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18921,19 +19564,17 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18955,19 +19596,17 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -18993,8 +19632,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, @@ -19009,7 +19648,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "elements": { "added": {}, "removed": { - "id160": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19027,22 +19666,22 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, - "id161": { + "id1": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19060,42 +19699,42 @@ 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, "type": "rectangle", + "version": 2, "width": 100, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, "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, @@ -19106,7 +19745,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "elements": { "added": {}, "removed": { - "id184": { + "id24": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19115,7 +19754,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id185", + "id25", ], "height": 100, "index": "a1G", @@ -19124,22 +19763,22 @@ 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, "type": "rectangle", + "version": 4, "width": 100, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, - "id186": { + "id26": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19148,7 +19787,7 @@ exports[`history > singleplayer undo/redo > should support duplication of groups "fillStyle": "solid", "frameId": null, "groupIds": [ - "id185", + "id25", ], "height": 100, "index": "a1V", @@ -19157,25 +19796,25 @@ 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, "type": "rectangle", + "version": 4, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, "updated": {}, }, - "id": "id188", + "id": "id28", }, ] `; @@ -19204,7 +19843,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", @@ -19259,9 +19898,8 @@ exports[`history > singleplayer undo/redo > should support element creation, del }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { - "id93": true, + "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -19304,16 +19942,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19336,16 +19972,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19368,16 +20002,14 @@ 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, "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, + "roundness": null, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -19403,7 +20035,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "delta": Delta { "deleted": { "selectedElementIds": { - "id90": true, + "id0": true, }, }, "inserted": { @@ -19414,7 +20046,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "elements": { "added": {}, "removed": { - "id90": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19430,37 +20062,37 @@ 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, "type": "rectangle", + "version": 5, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, "updated": {}, }, - "id": "id113", + "id": "id23", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id93": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id90": true, + "id0": true, }, }, }, @@ -19468,7 +20100,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "elements": { "added": {}, "removed": { - "id93": { + "id3": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19484,37 +20116,37 @@ 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, "type": "rectangle", + "version": 7, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, "updated": {}, }, - "id": "id114", + "id": "id24", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id96": true, + "id6": true, }, }, "inserted": { "selectedElementIds": { - "id93": true, + "id3": true, }, }, }, @@ -19522,7 +20154,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "elements": { "added": {}, "removed": { - "id96": { + "id6": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19538,37 +20170,37 @@ 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, "type": "rectangle", + "version": 7, "width": 10, "x": 40, "y": 40, }, "inserted": { "isDeleted": true, + "version": 6, }, }, }, "updated": {}, }, - "id": "id115", + "id": "id25", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { "selectedElementIds": { - "id93": true, + "id3": true, }, }, "inserted": { "selectedElementIds": { - "id96": true, + "id6": true, }, }, }, @@ -19578,14 +20210,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": { @@ -19598,7 +20230,7 @@ exports[`history > singleplayer undo/redo > should support element creation, del "removed": {}, "updated": {}, }, - "id": "id117", + "id": "id27", }, { "appState": AppStateDelta { @@ -19608,35 +20240,39 @@ 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, }, "inserted": { "isDeleted": false, + "version": 7, }, }, - "id96": { + "id6": { "deleted": { "isDeleted": true, + "version": 8, }, "inserted": { "isDeleted": false, + "version": 7, }, }, }, "removed": {}, "updated": {}, }, - "id": "id118", + "id": "id28", }, ] `; @@ -19665,7 +20301,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", @@ -19720,16 +20356,15 @@ exports[`history > singleplayer undo/redo > should support linear element creati }, "penDetected": false, "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": {}, @@ -19770,7 +20405,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": [ @@ -19825,7 +20460,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "delta": Delta { "deleted": { "selectedElementIds": { - "id119": true, + "id0": true, }, }, "inserted": { @@ -19836,7 +20471,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "elements": { "added": {}, "removed": { - "id119": { + "id0": { "deleted": { "angle": 0, "backgroundColor": "transparent", @@ -19878,18 +20513,20 @@ 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, }, }, }, "updated": {}, }, - "id": "id142", + "id": "id23", }, { "appState": AppStateDelta { @@ -19902,7 +20539,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "added": {}, "removed": {}, "updated": { - "id119": { + "id0": { "deleted": { "lastCommittedPoint": [ 20, @@ -19922,6 +20559,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati 0, ], ], + "version": 14, "width": 20, }, "inserted": { @@ -19939,18 +20577,19 @@ exports[`history > singleplayer undo/redo > should support linear element creati 10, ], ], + "version": 13, "width": 10, }, }, }, }, - "id": "id143", + "id": "id24", }, { "appState": AppStateDelta { "delta": Delta { "deleted": { - "selectedLinearElementId": "id119", + "selectedLinearElementId": "id0", }, "inserted": { "selectedLinearElementId": null, @@ -19962,13 +20601,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, @@ -19980,7 +20619,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "removed": {}, "updated": {}, }, - "id": "id145", + "id": "id26", }, { "appState": AppStateDelta { @@ -19993,7 +20632,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "added": {}, "removed": {}, "updated": { - "id119": { + "id0": { "deleted": { "height": 20, "points": [ @@ -20010,6 +20649,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati 20, ], ], + "version": 15, }, "inserted": { "height": 10, @@ -20027,11 +20667,12 @@ exports[`history > singleplayer undo/redo > should support linear element creati 0, ], ], + "version": 14, }, }, }, }, - "id": "id146", + "id": "id27", }, { "appState": AppStateDelta { @@ -20040,7 +20681,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "editingLinearElementId": null, }, "inserted": { - "editingLinearElementId": "id119", + "editingLinearElementId": "id0", }, }, }, @@ -20049,7 +20690,7 @@ exports[`history > singleplayer undo/redo > should support linear element creati "removed": {}, "updated": {}, }, - "id": "id147", + "id": "id28", }, ] `; diff --git a/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap b/packages/excalidraw/tests/__snapshots__/move.test.tsx.snap index 729e53d225..52614ed5f4 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", @@ -27,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, @@ -51,17 +49,15 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1604849351, + "roundness": null, + "seed": 1505387817, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 7, - "versionNonce": 915032327, + "versionNonce": 81784553, "width": 30, "x": -10, "y": 60, @@ -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", @@ -95,7 +89,7 @@ exports[`move element > rectangle 5`] = ` "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1116226695, + "versionNonce": 1014066025, "width": 30, "x": 0, "y": 40, @@ -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", @@ -134,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, @@ -163,17 +155,15 @@ exports[`move element > rectangles with binding arrow 6`] = ` "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1150084233, + "roundness": null, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 7, - "versionNonce": 1051383431, + "versionNonce": 1984422985, "width": 300, "x": 201, "y": 2, @@ -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,15 +200,15 @@ exports[`move element > rectangles with binding arrow 7`] = ` 0, ], [ - "86.85786", - "87.29887", + "81.00000", + "81.40630", ], ], "roughness": 1, "roundness": { "type": 2, }, - "seed": 1604849351, + "seed": 23633383, "startArrowhead": null, "startBinding": { "elementId": "id0", @@ -231,9 +221,9 @@ exports[`move element > rectangles with binding arrow 7`] = ` "type": "arrow", "updated": 1, "version": 11, - "versionNonce": 1996028265, - "width": "86.85786", - "x": "107.07107", - "y": "47.07107", + "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 5f2a82e4e8..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, @@ -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, @@ -107,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 faf96d82d8..238242a167 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", @@ -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, @@ -164,19 +163,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -218,19 +217,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 30, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -272,19 +271,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 60, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -357,10 +356,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": { @@ -369,10 +370,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, }, }, }, @@ -446,7 +449,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", @@ -502,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, @@ -588,19 +590,19 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -642,19 +644,19 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -696,19 +698,19 @@ 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, "type": "rectangle", + "version": 3, "width": 100, "x": 220, "y": 220, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -759,10 +761,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": { @@ -771,10 +775,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, }, }, }, @@ -858,7 +864,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", @@ -914,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, @@ -991,19 +996,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1045,19 +1050,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1129,9 +1134,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -1139,9 +1146,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -1210,19 +1219,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 60, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1302,11 +1311,13 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "id12", "id28", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, "id19": { @@ -1314,9 +1325,11 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "groupIds": [ "id28", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -1325,11 +1338,13 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "id12", "id28", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, }, @@ -1414,7 +1429,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", @@ -1470,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, @@ -1547,19 +1561,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1580,10 +1594,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, }, @@ -1619,7 +1635,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", @@ -1675,7 +1691,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -1757,19 +1772,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1811,19 +1826,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1865,19 +1880,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -1950,10 +1965,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": { @@ -1962,10 +1979,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, }, }, }, @@ -1999,7 +2018,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", @@ -2055,7 +2074,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -2134,19 +2152,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2188,23 +2206,32 @@ 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, "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", }, @@ -2235,7 +2262,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", @@ -2291,7 +2318,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = ` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -2368,19 +2394,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2415,7 +2441,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", @@ -2471,7 +2497,6 @@ exports[`regression tests > can drag element that covers another element, while }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id6": true, }, @@ -2550,19 +2575,19 @@ 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, "type": "rectangle", + "version": 3, "width": 200, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2604,19 +2629,19 @@ 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, "type": "rectangle", + "version": 3, "width": 200, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2658,19 +2683,19 @@ 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, "type": "ellipse", + "version": 3, "width": 350, "x": 300, "y": 300, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2699,10 +2724,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, }, @@ -2738,7 +2765,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", @@ -2794,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, @@ -2871,19 +2897,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -2905,9 +2931,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, }, }, }, @@ -2928,9 +2956,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, }, }, }, @@ -2951,9 +2981,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, }, }, }, @@ -2987,7 +3019,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", @@ -3043,7 +3075,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -3099,9 +3130,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", @@ -3109,7 +3138,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, @@ -3156,19 +3185,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3189,10 +3218,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, }, @@ -3228,7 +3259,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", @@ -3284,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, }, @@ -3363,19 +3393,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3396,10 +3426,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, }, @@ -3421,10 +3453,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, }, @@ -3460,7 +3494,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", @@ -3516,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, }, @@ -3595,19 +3628,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3649,19 +3682,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3718,7 +3751,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", @@ -3774,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, }, @@ -3854,19 +3886,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3908,19 +3940,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -3962,19 +3994,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4032,7 +4064,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", @@ -4088,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, @@ -4167,19 +4198,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4221,19 +4252,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4305,9 +4336,11 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -4315,9 +4348,11 @@ exports[`regression tests > deleting last but one element in editing group shoul "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -4376,9 +4411,11 @@ exports[`regression tests > deleting last but one element in editing group shoul "id0": { "deleted": { "isDeleted": true, + "version": 5, }, "inserted": { "isDeleted": false, + "version": 4, }, }, }, @@ -4462,7 +4499,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", @@ -4518,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, @@ -4548,10 +4584,8 @@ exports[`regression tests > deselects group of selected elements on pointer down "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, - "seed": 1505387817, + "roundness": null, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -4626,19 +4660,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4680,19 +4714,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4747,7 +4781,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", @@ -4803,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, @@ -4881,19 +4914,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -4935,19 +4968,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5023,7 +5056,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", @@ -5079,7 +5112,6 @@ exports[`regression tests > deselects selected element on pointer down when poin }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -5108,10 +5140,8 @@ exports[`regression tests > deselects selected element on pointer down when poin "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, - "seed": 1150084233, + "roundness": null, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -5186,19 +5216,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5233,7 +5263,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", @@ -5289,7 +5319,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -5366,19 +5395,19 @@ 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, "type": "ellipse", + "version": 3, "width": 100, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5433,7 +5462,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", @@ -5489,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, @@ -5566,19 +5594,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5620,19 +5648,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5674,19 +5702,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -5737,9 +5765,11 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -5747,9 +5777,11 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -5757,9 +5789,11 @@ exports[`regression tests > double click to edit a group > [end of test] undo st "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -5820,7 +5854,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", @@ -5876,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, @@ -5957,19 +5990,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6011,19 +6044,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6064,20 +6097,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, }, @@ -6113,7 +6150,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", @@ -6169,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, @@ -6244,19 +6280,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 10, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6298,19 +6334,19 @@ 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, "type": "diamond", + "version": 3, "width": 20, "x": 40, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6352,19 +6388,19 @@ 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, "type": "ellipse", + "version": 3, "width": 20, "x": 70, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -6431,12 +6467,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, }, }, }, @@ -6494,21 +6532,21 @@ 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", "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 4, "width": 50, "x": 220, "y": -10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -6578,12 +6616,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, }, }, }, @@ -6623,6 +6663,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 20, ], ], + "version": 8, "width": 80, }, "inserted": { @@ -6641,6 +6682,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 10, ], ], + "version": 6, "width": 50, }, }, @@ -6719,21 +6761,21 @@ 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", "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 6, "width": 50, "x": 430, "y": -10, }, "inserted": { "isDeleted": true, + "version": 5, }, }, }, @@ -6773,6 +6815,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 20, ], ], + "version": 8, "width": 80, }, "inserted": { @@ -6791,6 +6834,7 @@ exports[`regression tests > draw every type of shape > [end of test] undo stack 10, ], ], + "version": 6, "width": 50, }, }, @@ -6895,12 +6939,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, }, }, }, @@ -6935,7 +6981,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", @@ -6991,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, @@ -7071,19 +7116,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7125,19 +7170,19 @@ 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, "type": "ellipse", + "version": 3, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7179,19 +7224,19 @@ 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, "type": "diamond", + "version": 3, "width": 100, "x": 310, "y": 310, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7269,7 +7314,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", @@ -7325,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, }, @@ -7405,19 +7449,19 @@ 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, "type": "rectangle", + "version": 3, "width": 1000, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7459,19 +7503,19 @@ 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, "type": "ellipse", + "version": 3, "width": 1000, "x": 500, "y": 500, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -7548,7 +7592,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", @@ -7604,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, }, @@ -7683,19 +7726,19 @@ 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, "type": "rectangle", + "version": 2, "width": 1000, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id1": { @@ -7714,19 +7757,19 @@ 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, "type": "rectangle", + "version": 2, "width": 500, "x": 500, "y": 500, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -7783,7 +7826,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", @@ -7839,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, }, @@ -7918,19 +7960,19 @@ 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, "type": "rectangle", + "version": 2, "width": 1000, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 1, }, }, "id1": { @@ -7949,19 +7991,19 @@ 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, "type": "rectangle", + "version": 2, "width": 500, "x": 500, "y": 500, }, "inserted": { "isDeleted": true, + "version": 1, }, }, }, @@ -7982,10 +8024,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, }, @@ -8021,7 +8065,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", @@ -8077,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, @@ -8154,19 +8197,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -8201,7 +8244,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", @@ -8257,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, @@ -8334,19 +8376,19 @@ 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, "type": "diamond", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -8381,7 +8423,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", @@ -8437,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, @@ -8514,19 +8555,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -8561,7 +8602,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", @@ -8620,7 +8661,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, @@ -8749,12 +8789,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, }, }, }, @@ -8789,7 +8831,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", @@ -8845,7 +8887,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, @@ -8965,21 +9006,21 @@ 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", "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -9014,7 +9055,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", @@ -9070,7 +9111,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9168,12 +9208,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, }, }, }, @@ -9208,7 +9250,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", @@ -9267,7 +9309,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, @@ -9396,12 +9437,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, }, }, }, @@ -9436,7 +9479,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", @@ -9492,7 +9535,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9569,19 +9611,19 @@ 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, "type": "diamond", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -9616,7 +9658,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", @@ -9672,7 +9714,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, @@ -9792,21 +9833,21 @@ 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", "strokeStyle": "solid", "strokeWidth": 2, "type": "line", + "version": 4, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 3, }, }, }, @@ -9841,7 +9882,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", @@ -9897,7 +9938,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -9974,19 +10014,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10021,7 +10061,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", @@ -10077,7 +10117,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10175,12 +10214,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,7 +10256,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", @@ -10271,7 +10312,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -10348,19 +10388,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10395,7 +10435,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", @@ -10451,7 +10491,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, @@ -10536,19 +10575,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10590,19 +10629,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10644,19 +10683,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -10707,9 +10746,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -10717,9 +10758,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -10727,9 +10770,11 @@ exports[`regression tests > make a group and duplicate it > [end of test] undo s "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -10782,19 +10827,19 @@ 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, "type": "rectangle", + "version": 7, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, }, }, "id18": { @@ -10815,19 +10860,19 @@ 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, "type": "rectangle", + "version": 7, "width": 10, "x": 40, "y": 20, }, "inserted": { "isDeleted": true, + "version": 6, }, }, "id19": { @@ -10848,23 +10893,48 @@ 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, "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", }, @@ -10895,7 +10965,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", @@ -10951,7 +11021,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -11030,19 +11099,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11084,19 +11153,19 @@ 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, "type": "rectangle", + "version": 5, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -11175,7 +11244,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", @@ -11231,7 +11300,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", @@ -11298,7 +11366,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", @@ -11354,7 +11422,6 @@ exports[`regression tests > shift click on selected element should deselect it o }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -11431,19 +11498,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11498,7 +11565,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", @@ -11554,7 +11621,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -11635,19 +11701,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11689,19 +11755,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -11764,20 +11830,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, }, @@ -11813,7 +11883,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", @@ -11869,7 +11939,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, "id3": true, @@ -11952,19 +12021,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12006,19 +12075,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 30, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12060,19 +12129,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12123,9 +12192,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -12133,9 +12204,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -12143,9 +12216,11 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -12172,31 +12247,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, }, }, }, @@ -12230,7 +12311,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", @@ -12286,7 +12367,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, @@ -12373,19 +12453,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12427,19 +12507,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12511,9 +12591,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -12521,9 +12603,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id12", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -12569,19 +12653,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 10, "y": 50, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12623,19 +12707,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 50, "y": 50, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -12707,9 +12791,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id27", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id18": { @@ -12717,9 +12803,11 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "groupIds": [ "id27", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -12777,11 +12865,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id12", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, "id15": { @@ -12790,11 +12880,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id27", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id27", ], + "version": 4, }, }, "id18": { @@ -12803,11 +12895,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id27", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id27", ], + "version": 4, }, }, "id3": { @@ -12816,11 +12910,13 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "id12", "id32", ], + "version": 5, }, "inserted": { "groupIds": [ "id12", ], + "version": 4, }, }, }, @@ -12854,7 +12950,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", @@ -12913,7 +13009,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 60, @@ -12980,7 +13075,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", @@ -13036,7 +13131,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id0": true, }, @@ -13115,19 +13209,19 @@ 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, "type": "rectangle", + "version": 3, "width": 50, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13169,19 +13263,19 @@ 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, "type": "rectangle", + "version": 3, "width": 50, "x": 100, "y": 100, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13223,19 +13317,19 @@ 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, "type": "rectangle", + "version": 3, "width": 50, "x": 200, "y": 200, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13286,9 +13380,11 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id3": { @@ -13296,9 +13392,11 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, "id6": { @@ -13306,9 +13404,11 @@ exports[`regression tests > supports nested groups > [end of test] undo stack 1` "groupIds": [ "id11", ], + "version": 4, }, "inserted": { "groupIds": [], + "version": 3, }, }, }, @@ -13386,12 +13486,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": { @@ -13401,12 +13503,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, }, }, }, @@ -13601,7 +13705,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", @@ -13657,7 +13761,6 @@ exports[`regression tests > switches from group of selected elements to another }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, "id6": true, @@ -13689,10 +13792,8 @@ exports[`regression tests > switches from group of selected elements to another "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, - "seed": 1723083209, + "roundness": null, + "seed": 289600103, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -13767,19 +13868,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13821,19 +13922,19 @@ 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, "type": "ellipse", + "version": 3, "width": 100, "x": 110, "y": 110, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13875,19 +13976,19 @@ 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, "type": "diamond", + "version": 3, "width": 100, "x": 310, "y": 310, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -13942,7 +14043,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", @@ -13998,7 +14099,6 @@ exports[`regression tests > switches selected element on pointer down > [end of }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": { "id3": true, }, @@ -14029,10 +14129,8 @@ exports[`regression tests > switches selected element on pointer down > [end of "locked": false, "opacity": 100, "roughness": 1, - "roundness": { - "type": 2, - }, - "seed": 1604849351, + "roundness": null, + "seed": 23633383, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 2, @@ -14107,19 +14205,19 @@ 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, "type": "rectangle", + "version": 3, "width": 10, "x": 0, "y": 0, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -14161,19 +14259,19 @@ 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, "type": "ellipse", + "version": 3, "width": 10, "x": 20, "y": 20, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -14208,7 +14306,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", @@ -14264,7 +14362,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`] }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 20, @@ -14331,7 +14428,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", @@ -14387,7 +14484,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14476,6 +14572,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st 10, ], ], + "version": 9, "width": 60, }, "inserted": { @@ -14498,6 +14595,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st 20, ], ], + "version": 8, "width": 100, }, }, @@ -14525,6 +14623,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] redo st "id6": { "deleted": { "isDeleted": true, + "version": 10, }, "inserted": { "angle": 0, @@ -14567,6 +14666,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, @@ -14615,19 +14715,19 @@ 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, "type": "rectangle", + "version": 3, "width": 20, "x": 10, "y": -10, }, "inserted": { "isDeleted": true, + "version": 2, }, }, }, @@ -14669,19 +14769,19 @@ 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, "type": "rectangle", + "version": 5, "width": 30, "x": 40, "y": 0, }, "inserted": { "isDeleted": true, + "version": 4, }, }, }, @@ -14716,7 +14816,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", @@ -14772,7 +14872,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, @@ -14839,7 +14938,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", @@ -14898,7 +14997,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/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/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..e965a00686 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,10 @@ import { waitFor, } from "./test-utils"; +import { getTextEditor } from "./queries/dom"; + +import { mockHTMLImageElement } from "./helpers/mocks"; + import type { NormalizedZoomValue } from "../types"; const { h } = window; @@ -741,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] : [] }); @@ -846,9 +871,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 +883,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/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/helpers/ui.ts b/packages/excalidraw/tests/helpers/ui.ts index abfadf3316..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"; @@ -32,10 +36,11 @@ import type { ExcalidrawTextContainer, ExcalidrawTextElementWithContainer, ExcalidrawImageElement, + ElementsMap, } 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"; @@ -146,6 +151,7 @@ export class Keyboard { const getElementPointForSelection = ( element: ExcalidrawElement, + elementsMap: ElementsMap, ): GlobalPoint => { const { x, y, width, angle } = element; const target = pointFrom( @@ -162,7 +168,7 @@ const getElementPointForSelection = ( (bounds[1] + bounds[3]) / 2, ); } else { - center = elementCenterPoint(element); + center = elementCenterPoint(element, elementsMap); } if (isTextElement(element)) { @@ -299,7 +305,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 +319,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(); } } @@ -532,16 +553,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"); } @@ -598,6 +618,7 @@ export class UI { const mutations = cropElement( element, + h.scene.getNonDeletedElementsMap(), handle, naturalWidth, naturalHeight, diff --git a/packages/excalidraw/tests/history.test.tsx b/packages/excalidraw/tests/history.test.tsx index 328dab7c4f..ba013e29d8 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(() => { @@ -221,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(); @@ -559,6 +612,252 @@ 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, + }), + ]); + + // 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(); + 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, + }), + ]); + // 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(); + 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( { ?.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/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/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/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" `; diff --git a/packages/excalidraw/tests/test-utils.ts b/packages/excalidraw/tests/test-utils.ts index bc137a1d85..78e11d1821 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( @@ -435,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, @@ -457,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`); @@ -469,10 +470,28 @@ 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`); }; + +/** + * 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/types.ts b/packages/excalidraw/types.ts index 24d80261e6..c4b5291ba1 100644 --- a/packages/excalidraw/types.ts +++ b/packages/excalidraw/types.ts @@ -26,7 +26,6 @@ import type { ChartType, FontFamilyValues, FileId, - ExcalidrawImageElement, Theme, StrokeRoundness, ExcalidrawEmbeddableElement, @@ -192,7 +191,6 @@ type _CommonCanvasAppState = { offsetLeft: AppState["offsetLeft"]; offsetTop: AppState["offsetTop"]; theme: AppState["theme"]; - pendingImageElementId: AppState["pendingImageElementId"]; }; export type StaticCanvasAppState = Readonly< @@ -417,8 +415,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[]; @@ -814,6 +810,9 @@ export interface ExcalidrawImperativeAPI { getSceneElementsIncludingDeleted: InstanceType< typeof App >["getSceneElementsIncludingDeleted"]; + getSceneElementsMapIncludingDeleted: InstanceType< + typeof App + >["getSceneElementsMapIncludingDeleted"]; history: { clear: InstanceType["resetHistory"]; }; diff --git a/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx b/packages/excalidraw/wysiwyg/textWysiwyg.test.tsx index e7cd975092..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"); @@ -682,7 +680,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, @@ -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, @@ -1500,9 +1498,7 @@ describe("textWysiwyg", () => { locked: false, opacity: 100, roughness: 1, - roundness: { - type: 3, - }, + roundness: null, strokeColor: "#1e1e1e", strokeStyle: "solid", strokeWidth: 2, @@ -1534,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, @@ -1557,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); @@ -1632,7 +1628,7 @@ describe("textWysiwyg", () => { arrow.y + arrow.height / 2, ); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); @@ -1657,7 +1653,7 @@ describe("textWysiwyg", () => { rectangle.y + rectangle.height / 2, ); - const editor = await getTextEditor(textEditorSelector, true); + const editor = await getTextEditor(); updateTextEditor(editor, "Hello World!"); 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 359caee09d..fa11abd460 100644 --- a/packages/math/src/curve.ts +++ b/packages/math/src/curve.ts @@ -1,8 +1,6 @@ -import 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 { LegendreGaussN24CValues, LegendreGaussN24TValues } from "./constants"; import type { Curve, GlobalPoint, LineSegment, LocalPoint } from "./types"; @@ -104,20 +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 bounds = curveBounds(c); - if ( - rectangleIntersectLineSegment( - rectangle( - pointFrom(bounds[0], bounds[1]), - pointFrom(bounds[2], bounds[3]), - ), - l, - ).length === 0 - ) { - return []; - } - const line = (s: number) => pointFrom( l[0][0] + s * (l[1][0] - l[0][0]), @@ -295,11 +279,227 @@ 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, +) { + 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; +} + +/** + * 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); } 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/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/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/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 diff --git a/packages/utils/tests/__snapshots__/export.test.ts.snap b/packages/utils/tests/__snapshots__/export.test.ts.snap index 2c22874a9b..209ef87579 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", @@ -81,7 +81,6 @@ exports[`exportToSvg > with default arguments 1`] = ` }, "penDetected": false, "penMode": false, - "pendingImageElementId": null, "previousSelectedElementIds": {}, "resizingElement": null, "scrollX": 0, 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); - }); - }); -});