From bb985eba3ae6cd2944522111c0cfd928b62b92bf Mon Sep 17 00:00:00 2001 From: Alex Kim <45559664+alex-kim-dev@users.noreply.github.com> Date: Wed, 2 Aug 2023 15:04:21 +0500 Subject: [PATCH 01/23] fix: resizing arrow labels (#6789) * fix arrow labels resizing - min arrow labels width based on font size - labels width and padding in % of container's width - resize labels simply multiplying by scale * remove no longer needed getContainerDims * fix arrow labels font size not updated on change font size action * fix bound arrows not updated right after resize * fix test * fix 3+ point arrow label resizing with shift * fix bound text not scaling when resizing with shift & n or s handle * fix arrow labels width not updating when moving a 2-point arrow point with shift --------- Co-authored-by: Aakansha Doshi --- src/components/App.tsx | 6 +- src/constants.ts | 3 + src/element/linearElementEditor.ts | 8 +-- src/element/resizeElements.ts | 99 +++++++++++++------------- src/element/textElement.ts | 45 ++++++------ src/element/textWysiwyg.tsx | 8 +-- src/tests/linearElementEditor.test.tsx | 8 +-- 7 files changed, 89 insertions(+), 88 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 4c202441d6..28d8401bd0 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -298,7 +298,6 @@ import { getApproxMinLineWidth, getBoundTextElement, getContainerCenter, - getContainerDims, getContainerElement, getDefaultLineHeight, getLineHeightInPx, @@ -3548,9 +3547,8 @@ class App extends React.Component { lineHeight, ); const minHeight = getApproxMinLineHeight(fontSize, lineHeight); - const containerDims = getContainerDims(container); - const newHeight = Math.max(containerDims.height, minHeight); - const newWidth = Math.max(containerDims.width, minWidth); + const newHeight = Math.max(container.height, minHeight); + const newWidth = Math.max(container.width, minWidth); mutateElement(container, { height: newHeight, width: newWidth }); sceneX = container.x + newWidth / 2; sceneY = container.y + newHeight / 2; diff --git a/src/constants.ts b/src/constants.ts index 4c6b67e2c0..6ef98af17f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -117,6 +117,7 @@ export const FRAME_STYLE = { export const WINDOWS_EMOJI_FALLBACK_FONT = "Segoe UI Emoji"; +export const MIN_FONT_SIZE = 1; export const DEFAULT_FONT_SIZE = 20; export const DEFAULT_FONT_FAMILY: FontFamilyValues = FONT_FAMILY.Virgil; export const DEFAULT_TEXT_ALIGN = "left"; @@ -239,6 +240,8 @@ export const VERSIONS = { } as const; export const BOUND_TEXT_PADDING = 5; +export const ARROW_LABEL_WIDTH_FRACTION = 0.7; +export const ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO = 11; export const VERTICAL_ALIGN = { TOP: "top", diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index b10493e0dc..3426b3e1a8 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -264,11 +264,11 @@ export class LinearElementEditor { }; }), ); + } - const boundTextElement = getBoundTextElement(element); - if (boundTextElement) { - handleBindTextResize(element, false); - } + const boundTextElement = getBoundTextElement(element); + if (boundTextElement) { + handleBindTextResize(element, false); } // suggest bindings for first and last point if selected diff --git a/src/element/resizeElements.ts b/src/element/resizeElements.ts index b9fd63287b..136c70fb8c 100644 --- a/src/element/resizeElements.ts +++ b/src/element/resizeElements.ts @@ -1,4 +1,4 @@ -import { SHIFT_LOCKING_ANGLE } from "../constants"; +import { MIN_FONT_SIZE, SHIFT_LOCKING_ANGLE } from "../constants"; import { rescalePoints } from "../points"; import { @@ -204,8 +204,6 @@ const rescalePointsInElement = ( } : {}; -const MIN_FONT_SIZE = 1; - const measureFontSizeFromWidth = ( element: NonDeleted, nextWidth: number, @@ -589,24 +587,42 @@ export const resizeSingleElement = ( }); } + if ( + isArrowElement(element) && + boundTextElement && + shouldMaintainAspectRatio + ) { + const fontSize = + (resizedElement.width / element.width) * boundTextElement.fontSize; + if (fontSize < MIN_FONT_SIZE) { + return; + } + boundTextFont.fontSize = fontSize; + } + if ( resizedElement.width !== 0 && resizedElement.height !== 0 && Number.isFinite(resizedElement.x) && Number.isFinite(resizedElement.y) ) { + mutateElement(element, resizedElement); + updateBoundElements(element, { newSize: { width: resizedElement.width, height: resizedElement.height }, }); - mutateElement(element, resizedElement); if (boundTextElement && boundTextFont != null) { mutateElement(boundTextElement, { fontSize: boundTextFont.fontSize, baseline: boundTextFont.baseline, }); } - handleBindTextResize(element, transformHandleDirection); + handleBindTextResize( + element, + transformHandleDirection, + shouldMaintainAspectRatio, + ); } }; @@ -722,12 +738,8 @@ export const resizeMultipleElements = ( fontSize?: ExcalidrawTextElement["fontSize"]; baseline?: ExcalidrawTextElement["baseline"]; scale?: ExcalidrawImageElement["scale"]; + boundTextFontSize?: ExcalidrawTextElement["fontSize"]; }; - boundText: { - element: ExcalidrawTextElementWithContainer; - fontSize: ExcalidrawTextElement["fontSize"]; - baseline: ExcalidrawTextElement["baseline"]; - } | null; }[] = []; for (const { orig, latest } of targetElements) { @@ -798,50 +810,39 @@ export const resizeMultipleElements = ( } } - let boundText: typeof elementsAndUpdates[0]["boundText"] = null; - - const boundTextElement = getBoundTextElement(latest); - - if (boundTextElement || isTextElement(orig)) { - const updatedElement = { - ...latest, - width, - height, - }; - const metrics = measureFontSizeFromWidth( - boundTextElement ?? (orig as ExcalidrawTextElement), - boundTextElement - ? getBoundTextMaxWidth(updatedElement) - : updatedElement.width, - boundTextElement - ? getBoundTextMaxHeight(updatedElement, boundTextElement) - : updatedElement.height, - ); - + if (isTextElement(orig)) { + const metrics = measureFontSizeFromWidth(orig, width, height); if (!metrics) { return; } - - if (isTextElement(orig)) { - update.fontSize = metrics.size; - update.baseline = metrics.baseline; - } - - if (boundTextElement) { - boundText = { - element: boundTextElement, - fontSize: metrics.size, - baseline: metrics.baseline, - }; - } + update.fontSize = metrics.size; + update.baseline = metrics.baseline; } - elementsAndUpdates.push({ element: latest, update, boundText }); + const boundTextElement = pointerDownState.originalElements.get( + getBoundTextElementId(orig) ?? "", + ) as ExcalidrawTextElementWithContainer | undefined; + + if (boundTextElement) { + const newFontSize = boundTextElement.fontSize * scale; + if (newFontSize < MIN_FONT_SIZE) { + return; + } + update.boundTextFontSize = newFontSize; + } + + elementsAndUpdates.push({ + element: latest, + update, + }); } const elementsToUpdate = elementsAndUpdates.map(({ element }) => element); - for (const { element, update, boundText } of elementsAndUpdates) { + for (const { + element, + update: { boundTextFontSize, ...update }, + } of elementsAndUpdates) { const { width, height, angle } = update; mutateElement(element, update, false); @@ -851,17 +852,17 @@ export const resizeMultipleElements = ( newSize: { width, height }, }); - if (boundText) { - const { element: boundTextElement, ...boundTextUpdates } = boundText; + const boundTextElement = getBoundTextElement(element); + if (boundTextElement && boundTextFontSize) { mutateElement( boundTextElement, { - ...boundTextUpdates, + fontSize: boundTextFontSize, angle: isLinearElement(element) ? undefined : angle, }, false, ); - handleBindTextResize(element, transformHandleType); + handleBindTextResize(element, transformHandleType, true); } } diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 76de2c886a..61813d2b4a 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -10,6 +10,8 @@ import { } from "./types"; import { mutateElement } from "./mutateElement"; import { + ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO, + ARROW_LABEL_WIDTH_FRACTION, BOUND_TEXT_PADDING, DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE, @@ -65,7 +67,7 @@ export const redrawTextBoundingBox = ( boundTextUpdates.text = textElement.text; if (container) { - maxWidth = getBoundTextMaxWidth(container); + maxWidth = getBoundTextMaxWidth(container, textElement); boundTextUpdates.text = wrapText( textElement.originalText, getFontString(textElement), @@ -83,13 +85,12 @@ export const redrawTextBoundingBox = ( boundTextUpdates.baseline = metrics.baseline; if (container) { - const containerDims = getContainerDims(container); const maxContainerHeight = getBoundTextMaxHeight( container, textElement as ExcalidrawTextElementWithContainer, ); - let nextHeight = containerDims.height; + let nextHeight = container.height; if (metrics.height > maxContainerHeight) { nextHeight = computeContainerDimensionForBoundText( metrics.height, @@ -155,6 +156,7 @@ export const bindTextToShapeAfterDuplication = ( export const handleBindTextResize = ( container: NonDeletedExcalidrawElement, transformHandleType: MaybeTransformHandleType, + shouldMaintainAspectRatio = false, ) => { const boundTextElementId = getBoundTextElementId(container); if (!boundTextElementId) { @@ -175,15 +177,17 @@ export const handleBindTextResize = ( let text = textElement.text; let nextHeight = textElement.height; let nextWidth = textElement.width; - const containerDims = getContainerDims(container); const maxWidth = getBoundTextMaxWidth(container); const maxHeight = getBoundTextMaxHeight( container, textElement as ExcalidrawTextElementWithContainer, ); - let containerHeight = containerDims.height; + let containerHeight = container.height; let nextBaseLine = textElement.baseline; - if (transformHandleType !== "n" && transformHandleType !== "s") { + if ( + shouldMaintainAspectRatio || + (transformHandleType !== "n" && transformHandleType !== "s") + ) { if (text) { text = wrapText( textElement.originalText, @@ -207,7 +211,7 @@ export const handleBindTextResize = ( container.type, ); - const diff = containerHeight - containerDims.height; + const diff = containerHeight - container.height; // fix the y coord when resizing from ne/nw/n const updatedY = !isArrowElement(container) && @@ -687,16 +691,6 @@ export const getContainerElement = ( return null; }; -export const getContainerDims = (element: ExcalidrawElement) => { - const MIN_WIDTH = 300; - if (isArrowElement(element)) { - const width = Math.max(element.width, MIN_WIDTH); - const height = element.height; - return { width, height }; - } - return { width: element.width, height: element.height }; -}; - export const getContainerCenter = ( container: ExcalidrawElement, appState: AppState, @@ -887,12 +881,19 @@ export const computeContainerDimensionForBoundText = ( return dimension + padding; }; -export const getBoundTextMaxWidth = (container: ExcalidrawElement) => { - const width = getContainerDims(container).width; +export const getBoundTextMaxWidth = ( + container: ExcalidrawElement, + boundTextElement: ExcalidrawTextElement | null = getBoundTextElement( + container, + ), +) => { + const { width } = container; if (isArrowElement(container)) { - return width - BOUND_TEXT_PADDING * 8 * 2; + const minWidth = + (boundTextElement?.fontSize ?? DEFAULT_FONT_SIZE) * + ARROW_LABEL_FONT_SIZE_TO_MIN_WIDTH_RATIO; + return Math.max(ARROW_LABEL_WIDTH_FRACTION * width, minWidth); } - if (container.type === "ellipse") { // The width of the largest rectangle inscribed inside an ellipse is // Math.round((ellipse.width / 2) * Math.sqrt(2)) which is derived from @@ -911,7 +912,7 @@ export const getBoundTextMaxHeight = ( container: ExcalidrawElement, boundTextElement: ExcalidrawTextElementWithContainer, ) => { - const height = getContainerDims(container).height; + const { height } = container; if (isArrowElement(container)) { const containerHeight = height - BOUND_TEXT_PADDING * 8 * 2; if (containerHeight <= 0) { diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index 9105ba700a..a5bf701434 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -23,7 +23,6 @@ import { AppState } from "../types"; import { mutateElement } from "./mutateElement"; import { getBoundTextElementId, - getContainerDims, getContainerElement, getTextElementAngle, getTextWidth, @@ -177,20 +176,19 @@ export const textWysiwyg = ({ updatedTextElement, editable, ); - const containerDims = getContainerDims(container); let originalContainerData; if (propertiesUpdated) { originalContainerData = updateOriginalContainerCache( container.id, - containerDims.height, + container.height, ); } else { originalContainerData = originalContainerCache[container.id]; if (!originalContainerData) { originalContainerData = updateOriginalContainerCache( container.id, - containerDims.height, + container.height, ); } } @@ -214,7 +212,7 @@ export const textWysiwyg = ({ // autoshrink container height until original container height // is reached when text is removed !isArrowElement(container) && - containerDims.height > originalContainerData.height && + container.height > originalContainerData.height && textElementHeight < maxHeight ) { const targetContainerHeight = computeContainerDimensionForBoundText( diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index 6bcefb95ff..bcca0b1e1c 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -1128,7 +1128,7 @@ describe("Test Linear Elements", () => { height: 500, }); const arrow = UI.createElement("arrow", { - x: 210, + x: -10, y: 250, width: 400, height: 1, @@ -1152,8 +1152,8 @@ describe("Test Linear Elements", () => { expect( wrapText(textElement.originalText, font, getBoundTextMaxWidth(arrow)), ).toMatchInlineSnapshot(` - "Online whiteboard collaboration - made easy" + "Online whiteboard + collaboration made easy" `); const handleBindTextResizeSpy = vi.spyOn( textElementUtils, @@ -1165,7 +1165,7 @@ describe("Test Linear Elements", () => { mouse.moveTo(200, 0); mouse.upAt(200, 0); - expect(arrow.width).toBe(170); + expect(arrow.width).toBe(200); expect(rect.x).toBe(200); expect(rect.y).toBe(0); expect(handleBindTextResizeSpy).toHaveBeenCalledWith( From 083bcf802c01cdeae139ad5fd473187d0fd10f86 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 4 Aug 2023 15:16:55 +0200 Subject: [PATCH 02/23] fix: remove `embeddable` from generic elements (#6853) --- src/components/App.tsx | 24 ++++++++++++++++++------ src/data/restore.ts | 2 +- src/element/newElement.ts | 2 +- src/element/types.ts | 10 +++++----- src/tests/fixtures/elementFixture.ts | 1 + src/tests/helpers/api.ts | 16 +++++++++++++--- 6 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index 28d8401bd0..e0847bf89e 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5395,7 +5395,7 @@ class App extends React.Component { width: embedLink.aspectRatio.w, height: embedLink.aspectRatio.h, link, - validated: undefined, + validated: null, }); this.scene.replaceAllElements([ @@ -5583,7 +5583,7 @@ class App extends React.Component { } private createGenericElementOnPointerDown = ( - elementType: ExcalidrawGenericElement["type"], + elementType: ExcalidrawGenericElement["type"] | "embeddable", pointerDownState: PointerDownState, ): void => { const [gridX, gridY] = getGridPoint( @@ -5597,8 +5597,7 @@ class App extends React.Component { y: gridY, }); - const element = newElement({ - type: elementType, + const baseElementAttributes = { x: gridX, y: gridY, strokeColor: this.state.currentItemStrokeColor, @@ -5611,8 +5610,21 @@ class App extends React.Component { roundness: this.getCurrentItemRoundness(elementType), locked: false, frameId: topLayerFrame ? topLayerFrame.id : null, - ...(elementType === "embeddable" ? { validated: false } : {}), - }); + } as const; + + let element; + if (elementType === "embeddable") { + element = newEmbeddableElement({ + type: "embeddable", + validated: null, + ...baseElementAttributes, + }); + } else { + element = newElement({ + type: elementType, + ...baseElementAttributes, + }); + } if (element.type === "selection") { this.setState({ diff --git a/src/data/restore.ts b/src/data/restore.ts index 08fbe0930c..63f1e33c0c 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -286,7 +286,7 @@ const restoreElement = ( return restoreElementWithProperties(element, {}); case "embeddable": return restoreElementWithProperties(element, { - validated: undefined, + validated: null, }); case "frame": return restoreElementWithProperties(element, { diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 012725bc47..cb5657f1db 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -134,7 +134,7 @@ export const newElement = ( export const newEmbeddableElement = ( opts: { type: "embeddable"; - validated: boolean | undefined; + validated: ExcalidrawEmbeddableElement["validated"]; } & ElementConstructorOpts, ): NonDeleted => { return { diff --git a/src/element/types.ts b/src/element/types.ts index 7d799f234c..e8d71cae5e 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -86,15 +86,15 @@ export type ExcalidrawEllipseElement = _ExcalidrawElementBase & { export type ExcalidrawEmbeddableElement = _ExcalidrawElementBase & Readonly<{ + type: "embeddable"; /** * indicates whether the embeddable src (url) has been validated for rendering. - * nullish value indicates that the validation is pending. We reset the + * null value indicates that the validation is pending. We reset the * value on each restore (or url change) so that we can guarantee * the validation came from a trusted source (the editor). Also because we * may not have access to host-app supplied url validator during restore. */ - validated?: boolean; - type: "embeddable"; + validated: boolean | null; }>; export type ExcalidrawImageElement = _ExcalidrawElementBase & @@ -123,7 +123,6 @@ export type ExcalidrawFrameElement = _ExcalidrawElementBase & { export type ExcalidrawGenericElement = | ExcalidrawSelectionElement | ExcalidrawRectangleElement - | ExcalidrawEmbeddableElement | ExcalidrawDiamondElement | ExcalidrawEllipseElement; @@ -138,7 +137,8 @@ export type ExcalidrawElement = | ExcalidrawLinearElement | ExcalidrawFreeDrawElement | ExcalidrawImageElement - | ExcalidrawFrameElement; + | ExcalidrawFrameElement + | ExcalidrawEmbeddableElement; export type NonDeleted = TElement & { isDeleted: boolean; diff --git a/src/tests/fixtures/elementFixture.ts b/src/tests/fixtures/elementFixture.ts index ddd7b8b9de..7f1231a834 100644 --- a/src/tests/fixtures/elementFixture.ts +++ b/src/tests/fixtures/elementFixture.ts @@ -34,6 +34,7 @@ export const rectangleFixture: ExcalidrawElement = { export const embeddableFixture: ExcalidrawElement = { ...elementBase, type: "embeddable", + validated: null, }; export const ellipseFixture: ExcalidrawElement = { ...elementBase, diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index 7f63203d81..824a22956e 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -15,7 +15,11 @@ import fs from "fs"; import util from "util"; import path from "path"; import { getMimeType } from "../../data/blob"; -import { newFreeDrawElement, newImageElement } from "../../element/newElement"; +import { + newEmbeddableElement, + newFreeDrawElement, + newImageElement, +} from "../../element/newElement"; import { Point } from "../../types"; import { getSelectedElements } from "../../scene/selection"; import { isLinearElementType } from "../../element/typeChecks"; @@ -178,14 +182,20 @@ export class API { case "rectangle": case "diamond": case "ellipse": - case "embeddable": element = newElement({ - type: type as "rectangle" | "diamond" | "ellipse" | "embeddable", + type: type as "rectangle" | "diamond" | "ellipse", width, height, ...base, }); break; + case "embeddable": + element = newEmbeddableElement({ + type: "embeddable", + ...base, + validated: null, + }); + break; case "text": const fontSize = rest.fontSize ?? appState.currentItemFontSize; const fontFamily = rest.fontFamily ?? appState.currentItemFontFamily; From e7983bc493ee1cb4604b9665443de56e7a73d029 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 5 Aug 2023 11:49:57 +0200 Subject: [PATCH 03/23] fix: webpack config exclude statement to system agnostic (#6857) --- src/packages/excalidraw/webpack.dev.config.js | 5 ++--- src/packages/excalidraw/webpack.prod.config.js | 9 ++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/packages/excalidraw/webpack.dev.config.js b/src/packages/excalidraw/webpack.dev.config.js index 2813073885..332d802819 100644 --- a/src/packages/excalidraw/webpack.dev.config.js +++ b/src/packages/excalidraw/webpack.dev.config.js @@ -2,8 +2,8 @@ const path = require("path"); const webpack = require("webpack"); const autoprefixer = require("autoprefixer"); const { parseEnvVariables } = require("./env"); - const outputDir = process.env.EXAMPLE === "true" ? "example/public" : "dist"; + module.exports = { mode: "development", devtool: false, @@ -17,7 +17,6 @@ module.exports = { filename: "[name].js", chunkFilename: "excalidraw-assets-dev/[name]-[contenthash].js", assetModuleFilename: "excalidraw-assets-dev/[name][ext]", - publicPath: "", }, resolve: { @@ -45,7 +44,7 @@ module.exports = { { test: /\.(ts|tsx|js|jsx|mjs)$/, exclude: - /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/, + /node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/, use: [ { loader: "import-meta-loader", diff --git a/src/packages/excalidraw/webpack.prod.config.js b/src/packages/excalidraw/webpack.prod.config.js index c9f8d56a5d..8f8c72eef3 100644 --- a/src/packages/excalidraw/webpack.prod.config.js +++ b/src/packages/excalidraw/webpack.prod.config.js @@ -1,10 +1,10 @@ const path = require("path"); +const webpack = require("webpack"); +const autoprefixer = require("autoprefixer"); +const { parseEnvVariables } = require("./env"); const TerserPlugin = require("terser-webpack-plugin"); const BundleAnalyzerPlugin = require("webpack-bundle-analyzer").BundleAnalyzerPlugin; -const autoprefixer = require("autoprefixer"); -const webpack = require("webpack"); -const { parseEnvVariables } = require("./env"); module.exports = { mode: "production", @@ -47,8 +47,7 @@ module.exports = { { test: /\.(ts|tsx|js|jsx|mjs)$/, exclude: - /node_modules\/(?!(browser-fs-access|canvas-roundrect-polyfill))/, - + /node_modules[\\/](?!(browser-fs-access|canvas-roundrect-polyfill))/, use: [ { loader: "import-meta-loader", From ded0222e8db0d9b4ef985e77889232cefb200965 Mon Sep 17 00:00:00 2001 From: Jezreel Maldonado <49414833+Denovocto@users.noreply.github.com> Date: Sat, 5 Aug 2023 05:51:50 -0400 Subject: [PATCH 04/23] docs: add note for tests that have requisites (#6856) Co-authored-by: David Luzar --- dev-docs/docs/introduction/contributing.mdx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dev-docs/docs/introduction/contributing.mdx b/dev-docs/docs/introduction/contributing.mdx index 821355e0ed..169aa53550 100644 --- a/dev-docs/docs/introduction/contributing.mdx +++ b/dev-docs/docs/introduction/contributing.mdx @@ -69,6 +69,10 @@ It's also a good idea to consider if your change should include additional tests Finally - always manually test your changes using the convenient staging environment deployed for each pull request. As much as local development attempts to replicate production, there can still be subtle differences in behavior. For larger features consider testing your change in multiple browsers as well. +:::note +Some checks, such as the `lint` and `test`, require approval from the maintainers to run. +They will appear as `Expected — Waiting for status to be reported` in the PR checks when they are waiting for approval. +::: ## Translating From 3ea07076ada5cc9a441919de2313c82de7ec006c Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 9 Aug 2023 16:41:15 +0530 Subject: [PATCH 05/23] feat: support creating containers, linear elements, text containers, labelled arrows and arrow bindings programatically (#6546) * feat: support creating text containers programatically * fix * fix * fix * fix * update api to use label * fix api and support individual shapes and text element * update test case in package example * support creating arrows and line * support labelled arrows * add in package example * fix alignment * better types * fix * keep element as is unless we support prog api * fix tests * fix lint * ignore * support arrow bindings via start and end in api * fix lint * fix coords * support id as well for elements * preserve bindings if present and fix testcases * preserve bindings for labelled arrows * support ids, clean up code and move the api related stuff to transform.ts * allow multiple arrows to bind to single element * fix singular elements * fix single text element, unique id and tests * fix lint * fix * support binding arrow to text element * fix creation of regular text * use same stroke color as parent for text containers and height 0 for linear element by default * fix types * fix * remove more ts ignore * remove ts ignore * remove * Add coverage script * Add tests * fix tests * make type optional when id present * remove type when id provided in tests * Add more tests * tweak * let host call convertToExcalidrawElements when using programmatic API * remove convertToExcalidrawElements call from restore * lint * update snaps * Add new type excalidraw-api/clipboard for programmatic api * cleanup * rename tweak * tweak * make image attributes optional and better ts check * support image via programmatic API * fix lint * more types * make fileId mandatory for image and export convertToExcalidrawElements * fix * small tweaks * update snaps * fix * use Object.assign instead of mutateElement * lint * preserve z-index by pushing all elements first and then add bindings * instantiate instead of closure for storing elements * use element API to create regular text, diamond, ellipse and rectangle * fix snaps * udpdate api * ts fixes * make `convertToExcalidrawElements` more typesafe * update snaps * refactor the approach so that order of elements doesn't matter * Revert "update snaps" This reverts commit 621dfadccfea975a1f77223f506dce9d260f91fd. * review fixes * rename ExcalidrawProgrammaticElement -> ExcalidrawELementSkeleton * Add tests * give preference to first element when duplicate ids found * use console.error --------- Co-authored-by: dwelle --- src/clipboard.ts | 5 + src/components/App.tsx | 14 +- src/constants.ts | 1 + src/data/__snapshots__/transform.test.ts.snap | 2032 +++++++++++++++++ src/data/restore.ts | 21 +- src/data/transform.test.ts | 706 ++++++ src/data/transform.ts | 561 +++++ src/element/binding.ts | 2 +- src/element/newElement.ts | 14 +- src/element/textElement.ts | 16 +- src/packages/excalidraw/example/App.tsx | 36 +- .../{initialData.js => initialData.tsx} | 467 +--- src/packages/excalidraw/index.tsx | 1 + src/tests/data/restore.test.ts | 5 +- src/utils.ts | 13 + 15 files changed, 3440 insertions(+), 454 deletions(-) create mode 100644 src/data/__snapshots__/transform.test.ts.snap create mode 100644 src/data/transform.test.ts create mode 100644 src/data/transform.ts rename src/packages/excalidraw/example/{initialData.js => initialData.tsx} (71%) diff --git a/src/clipboard.ts b/src/clipboard.ts index c13829b6a2..c0deb93af2 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -24,6 +24,7 @@ export interface ClipboardData { files?: BinaryFiles; text?: string; errorMessage?: string; + programmaticAPI?: boolean; } let CLIPBOARD = ""; @@ -48,6 +49,7 @@ const clipboardContainsElements = ( [ EXPORT_DATA_TYPES.excalidraw, EXPORT_DATA_TYPES.excalidrawClipboard, + EXPORT_DATA_TYPES.excalidrawClipboardWithAPI, ].includes(contents?.type) && Array.isArray(contents.elements) ) { @@ -191,6 +193,8 @@ export const parseClipboard = async ( try { const systemClipboardData = JSON.parse(systemClipboard); + const programmaticAPI = + systemClipboardData.type === EXPORT_DATA_TYPES.excalidrawClipboardWithAPI; if (clipboardContainsElements(systemClipboardData)) { return { elements: systemClipboardData.elements, @@ -198,6 +202,7 @@ export const parseClipboard = async ( text: isPlainPaste ? JSON.stringify(systemClipboardData.elements, null, 2) : undefined, + programmaticAPI, }; } } catch (e) {} diff --git a/src/components/App.tsx b/src/components/App.tsx index e0847bf89e..901b983645 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -346,6 +346,10 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog"; import { actionWrapTextInContainer } from "../actions/actionBoundText"; import BraveMeasureTextError from "./BraveMeasureTextError"; import { activeEyeDropperAtom } from "./EyeDropper"; +import { + ExcalidrawElementSkeleton, + convertToExcalidrawElements, +} from "../data/transform"; import { ValueOf } from "../utility-types"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; @@ -2231,7 +2235,6 @@ class App extends React.Component { let file = event?.clipboardData?.files[0]; const data = await parseClipboard(event, isPlainPaste); - if (!file && data.text && !isPlainPaste) { const string = data.text.trim(); if (string.startsWith("")) { @@ -2286,9 +2289,16 @@ class App extends React.Component { }, }); } else if (data.elements) { + const elements = ( + data.programmaticAPI + ? convertToExcalidrawElements( + data.elements as ExcalidrawElementSkeleton[], + ) + : data.elements + ) as readonly ExcalidrawElement[]; // TODO remove formatting from elements if isPlainPaste this.addElementsFromPasteOrLibrary({ - elements: data.elements, + elements, files: data.files || null, position: "cursor", retainSeed: isPlainPaste, diff --git a/src/constants.ts b/src/constants.ts index 6ef98af17f..46ae25b401 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -164,6 +164,7 @@ export const EXPORT_DATA_TYPES = { excalidraw: "excalidraw", excalidrawClipboard: "excalidraw/clipboard", excalidrawLibrary: "excalidrawlib", + excalidrawClipboardWithAPI: "excalidraw-api/clipboard", } as const; export const EXPORT_SOURCE = diff --git a/src/data/__snapshots__/transform.test.ts.snap b/src/data/__snapshots__/transform.test.ts.snap new file mode 100644 index 0000000000..24503a9484 --- /dev/null +++ b/src/data/__snapshots__/transform.test.ts.snap @@ -0,0 +1,2032 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "#d8f5a2", + "boundElements": [ + { + "id": "id40", + "type": "arrow", + }, + { + "id": "id41", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 300, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#66a80f", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 630, + "y": 316, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id41", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#9c36b5", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 140, + "x": 96, + "y": 400, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "ellipse-1", + "focus": -0.008153707962747813, + "gap": 1, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 35, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 395, + 35, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id42", + "focus": -0.08139534883720931, + "gap": 1, + }, + "strokeColor": "#1864ab", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 395, + "x": 247, + "y": 420, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": { + "elementId": "ellipse-1", + "focus": 0.10666666666666667, + "gap": 3.834326468444573, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 400, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "diamond-1", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#e67700", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 400, + "x": 227, + "y": 450, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing shapes when start / end provided with ids 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id40", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 300, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": -53, + "y": 270, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id43", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HEYYYYY", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HEYYYYY", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 70, + "x": 185, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id43", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "Whats up ?", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "Whats up ?", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 100, + "x": 560, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id44", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "text-2", + "focus": 0, + "gap": 5, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "text-1", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to existing text elements when start / end provided with ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id43", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 340, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id33", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id35", + "focus": 0, + "gap": 1, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id34", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id32", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 340, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id32", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 155, + "y": 189, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to shapes when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id32", + "type": "arrow", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 100, + "x": 555, + "y": 189, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id37", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": { + "elementId": "id39", + "focus": 0, + "gap": 1, + }, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": { + "elementId": "id38", + "focus": 0, + "gap": 1, + }, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 300, + "x": 255, + "y": 239, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id36", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!!", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 340, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id36", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HEYYYYY", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HEYYYYY", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 70, + "x": 185, + "y": 226.5, +} +`; + +exports[`Test Transform > Test arrow bindings > should bind arrows to text when start / end provided without ids 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": [ + { + "id": "id36", + "type": "arrow", + }, + ], + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "WHATS UP ?", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "WHATS UP ?", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 100, + "x": 555, + "y": 226.5, +} +`; + +exports[`Test Transform > should not allow duplicate ids 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 200, + "id": "rect-1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 300, + "y": 100, +} +`; + +exports[`Test Transform > should transform linear elements 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 20, +} +`; + +exports[`Test Transform > should transform linear elements 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": "triangle", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": "dot", + "startBinding": null, + "strokeColor": "#1971c2", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 450, + "y": 20, +} +`; + +exports[`Test Transform > should transform linear elements 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "line", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 60, +} +`; + +exports[`Test Transform > should transform linear elements 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "endArrowhead": null, + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#2f9e44", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "line", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 450, + "y": 60, +} +`; + +exports[`Test Transform > should transform regular shapes 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform regular shapes 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 100, + "y": 250, +} +`; + +exports[`Test Transform > should transform regular shapes 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "diamond", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 100, + "x": 100, + "y": 400, +} +`; + +exports[`Test Transform > should transform regular shapes 4`] = ` +{ + "angle": 0, + "backgroundColor": "#c0eb75", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 200, + "x": 300, + "y": 100, +} +`; + +exports[`Test Transform > should transform regular shapes 5`] = ` +{ + "angle": 0, + "backgroundColor": "#ffc9c9", + "boundElements": null, + "fillStyle": "solid", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "ellipse", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 200, + "x": 300, + "y": 250, +} +`; + +exports[`Test Transform > should transform regular shapes 6`] = ` +{ + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 100, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1971c2", + "strokeStyle": "dashed", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 200, + "x": 300, + "y": 400, +} +`; + +exports[`Test Transform > should transform text element 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "HELLO WORLD!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "HELLO WORLD!", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 1, + "versionNonce": Any, + "verticalAlign": "top", + "width": 120, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform text element 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": null, + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED HELLO WORLD!", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#5f3dc4", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED HELLO WORLD!", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 1, + "versionNonce": Any, + "verticalAlign": "top", + "width": 190, + "x": 100, + "y": 150, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id28", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id29", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 0, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "arrow", + "updated": 1, + "version": 1, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 200, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id30", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 130, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1098ad", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 300, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 4`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id31", + "type": "text", + }, + ], + "endArrowhead": "arrow", + "endBinding": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 130, + "id": Any, + "isDeleted": false, + "lastCommittedPoint": null, + "link": null, + "locked": false, + "opacity": 100, + "points": [ + [ + 0, + 0, + ], + [ + 300, + 0, + ], + ], + "roughness": 1, + "roundness": null, + "seed": Any, + "startArrowhead": null, + "startBinding": null, + "strokeColor": "#1098ad", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "arrow", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 400, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id24", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "LABELED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "LABELED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 185, + "y": 87.5, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 6`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id25", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED LABELED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED LABELED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 200, + "x": 150, + "y": 187.5, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 7`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id26", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ANOTHER STYLED LABELLED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1098ad", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "ANOTHER STYLED +LABELLED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 150, + "x": 175, + "y": 275, +} +`; + +exports[`Test Transform > should transform to labelled arrows when label provided for arrows 8`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id27", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ANOTHER STYLED LABELLED ARROW", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "ANOTHER STYLED +LABELLED ARROW", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 150, + "x": 175, + "y": 375, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 1`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id18", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 35, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": Any, + "width": 250, + "x": 100, + "y": 100, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 2`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id19", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 85, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 200, + "x": 500, + "y": 100, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 3`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id20", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 170, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 280, + "x": 100, + "y": 150, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 4`] = ` +{ + "angle": 0, + "backgroundColor": "#fff3bf", + "boundElements": [ + { + "id": "id21", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 120, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "diamond", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 300, + "x": 100, + "y": 400, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 5`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "boundElements": [ + { + "id": "id22", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 85, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 200, + "x": 500, + "y": 300, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 6`] = ` +{ + "angle": 0, + "backgroundColor": "#ffec99", + "boundElements": [ + { + "id": "id23", + "type": "text", + }, + ], + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 120, + "id": Any, + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#f08c00", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "ellipse", + "updated": 1, + "version": 2, + "versionNonce": Any, + "width": 200, + "x": 500, + "y": 500, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 7`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id12", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 25, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "RECTANGLE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "RECTANGLE TEXT CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 240, + "x": 105, + "y": 105, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 8`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id13", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "ELLIPSE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "ELLIPSE TEXT +CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 534.7893218813452, + "y": 117.44796179957173, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 9`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id14", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 75, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "DIAMOND +TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "DIAMOND +TEXT +CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 90, + "x": 195, + "y": 197.5, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 10`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id15", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 50, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED DIAMOND TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#099268", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED DIAMOND +TEXT CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 140, + "x": 180, + "y": 435, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 11`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id16", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 75, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "TOP LEFT ALIGNED +RECTANGLE TEXT +CONTAINER", + "textAlign": "left", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "top", + "width": 170, + "x": 505, + "y": 305, +} +`; + +exports[`Test Transform > should transform to text containers when label provided 12`] = ` +{ + "angle": 0, + "backgroundColor": "transparent", + "baseline": 0, + "boundElements": null, + "containerId": "id17", + "fillStyle": "hachure", + "fontFamily": 1, + "fontSize": 20, + "frameId": null, + "groupIds": [], + "height": 75, + "id": Any, + "isDeleted": false, + "lineHeight": 1.25, + "link": null, + "locked": false, + "opacity": 100, + "originalText": "STYLED ELLIPSE TEXT CONTAINER", + "roughness": 1, + "roundness": null, + "seed": Any, + "strokeColor": "#c2255c", + "strokeStyle": "solid", + "strokeWidth": 1, + "text": "STYLED +ELLIPSE TEXT +CONTAINER", + "textAlign": "center", + "type": "text", + "updated": 1, + "version": 2, + "versionNonce": Any, + "verticalAlign": "middle", + "width": 130, + "x": 534.7893218813452, + "y": 522.5735931288071, +} +`; diff --git a/src/data/restore.ts b/src/data/restore.ts index 63f1e33c0c..9316cfe49c 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -29,6 +29,7 @@ import { FONT_FAMILY, ROUNDNESS, DEFAULT_SIDEBAR, + DEFAULT_ELEMENT_PROPS, } from "../constants"; import { getDefaultAppState } from "../appState"; import { LinearElementEditor } from "../element/linearElementEditor"; @@ -41,7 +42,6 @@ import { getDefaultLineHeight, measureBaseline, } from "../element/textElement"; -import { COLOR_PALETTE } from "../colors"; import { normalizeLink } from "./url"; type RestoredAppState = Omit< @@ -122,16 +122,18 @@ const restoreElementWithProperties = < versionNonce: element.versionNonce ?? 0, isDeleted: element.isDeleted ?? false, id: element.id || randomId(), - fillStyle: element.fillStyle || "hachure", - strokeWidth: element.strokeWidth || 1, - strokeStyle: element.strokeStyle ?? "solid", - roughness: element.roughness ?? 1, - opacity: element.opacity == null ? 100 : element.opacity, + fillStyle: element.fillStyle || DEFAULT_ELEMENT_PROPS.fillStyle, + strokeWidth: element.strokeWidth || DEFAULT_ELEMENT_PROPS.strokeWidth, + strokeStyle: element.strokeStyle ?? DEFAULT_ELEMENT_PROPS.strokeStyle, + roughness: element.roughness ?? DEFAULT_ELEMENT_PROPS.roughness, + opacity: + element.opacity == null ? DEFAULT_ELEMENT_PROPS.opacity : element.opacity, angle: element.angle || 0, x: extra.x ?? element.x ?? 0, y: extra.y ?? element.y ?? 0, - strokeColor: element.strokeColor || COLOR_PALETTE.black, - backgroundColor: element.backgroundColor || COLOR_PALETTE.transparent, + strokeColor: element.strokeColor || DEFAULT_ELEMENT_PROPS.strokeColor, + backgroundColor: + element.backgroundColor || DEFAULT_ELEMENT_PROPS.backgroundColor, width: element.width || 0, height: element.height || 0, seed: element.seed ?? 1, @@ -246,7 +248,6 @@ const restoreElement = ( startArrowhead = null, endArrowhead = element.type === "arrow" ? "arrow" : null, } = element; - let x = element.x; let y = element.y; let points = // migrate old arrow model to new one @@ -410,7 +411,6 @@ export const restoreElements = ( ): ExcalidrawElement[] => { // used to detect duplicate top-level element ids const existingIds = new Set(); - const localElementsMap = localElements ? arrayToMap(localElements) : null; const restoredElements = (elements || []).reduce((elements, element) => { // filtering out selection, which is legacy, no longer kept in elements, @@ -429,6 +429,7 @@ export const restoreElements = ( migratedElement = { ...migratedElement, id: randomId() }; } existingIds.add(migratedElement.id); + elements.push(migratedElement); } } diff --git a/src/data/transform.test.ts b/src/data/transform.test.ts new file mode 100644 index 0000000000..03d65beaa2 --- /dev/null +++ b/src/data/transform.test.ts @@ -0,0 +1,706 @@ +import { vi } from "vitest"; +import { + ExcalidrawElementSkeleton, + convertToExcalidrawElements, +} from "./transform"; +import { ExcalidrawArrowElement } from "../element/types"; + +describe("Test Transform", () => { + it("should transform regular shapes", () => { + const elements = [ + { + type: "rectangle", + x: 100, + y: 100, + }, + { + type: "ellipse", + x: 100, + y: 250, + }, + { + type: "diamond", + x: 100, + y: 400, + }, + { + type: "rectangle", + x: 300, + y: 100, + width: 200, + height: 100, + backgroundColor: "#c0eb75", + strokeWidth: 2, + }, + { + type: "ellipse", + x: 300, + y: 250, + width: 200, + height: 100, + backgroundColor: "#ffc9c9", + strokeStyle: "dotted", + fillStyle: "solid", + strokeWidth: 2, + }, + { + type: "diamond", + x: 300, + y: 400, + width: 200, + height: 100, + backgroundColor: "#a5d8ff", + strokeColor: "#1971c2", + strokeStyle: "dashed", + fillStyle: "cross-hatch", + strokeWidth: 2, + }, + ]; + + convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ).forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform text element", () => { + const elements = [ + { + type: "text", + x: 100, + y: 100, + text: "HELLO WORLD!", + }, + { + type: "text", + x: 100, + y: 150, + text: "STYLED HELLO WORLD!", + fontSize: 20, + strokeColor: "#5f3dc4", + }, + ]; + convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ).forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform linear elements", () => { + const elements = [ + { + type: "arrow", + x: 100, + y: 20, + }, + { + type: "arrow", + x: 450, + y: 20, + startArrowhead: "dot", + endArrowhead: "triangle", + strokeColor: "#1971c2", + strokeWidth: 2, + }, + { + type: "line", + x: 100, + y: 60, + }, + { + type: "line", + x: 450, + y: 60, + strokeColor: "#2f9e44", + strokeWidth: 2, + strokeStyle: "dotted", + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform to text containers when label provided", () => { + const elements = [ + { + type: "rectangle", + x: 100, + y: 100, + label: { + text: "RECTANGLE TEXT CONTAINER", + }, + }, + { + type: "ellipse", + x: 500, + y: 100, + width: 200, + label: { + text: "ELLIPSE TEXT CONTAINER", + }, + }, + { + type: "diamond", + x: 100, + y: 150, + width: 280, + label: { + text: "DIAMOND\nTEXT CONTAINER", + }, + }, + { + type: "diamond", + x: 100, + y: 400, + width: 300, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "STYLED DIAMOND TEXT CONTAINER", + strokeColor: "#099268", + fontSize: 20, + }, + }, + { + type: "rectangle", + x: 500, + y: 300, + width: 200, + strokeColor: "#c2255c", + label: { + text: "TOP LEFT ALIGNED RECTANGLE TEXT CONTAINER", + textAlign: "left", + verticalAlign: "top", + fontSize: 20, + }, + }, + { + type: "ellipse", + x: 500, + y: 500, + strokeColor: "#f08c00", + backgroundColor: "#ffec99", + width: 200, + label: { + text: "STYLED ELLIPSE TEXT CONTAINER", + strokeColor: "#c2255c", + }, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(12); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should transform to labelled arrows when label provided for arrows", () => { + const elements = [ + { + type: "arrow", + x: 100, + y: 100, + label: { + text: "LABELED ARROW", + }, + }, + { + type: "arrow", + x: 100, + y: 200, + label: { + text: "STYLED LABELED ARROW", + strokeColor: "#099268", + fontSize: 20, + }, + }, + { + type: "arrow", + x: 100, + y: 300, + strokeColor: "#1098ad", + strokeWidth: 2, + label: { + text: "ANOTHER STYLED LABELLED ARROW", + }, + }, + { + type: "arrow", + x: 100, + y: 400, + strokeColor: "#1098ad", + strokeWidth: 2, + label: { + text: "ANOTHER STYLED LABELLED ARROW", + strokeColor: "#099268", + }, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(8); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + describe("Test arrow bindings", () => { + it("should bind arrows to shapes when start / end provided without ids", () => { + const elements = [ + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + type: "rectangle", + }, + end: { + type: "ellipse", + }, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + const [arrow, text, rectangle, ellipse] = excaldrawElements; + expect(arrow).toMatchObject({ + type: "arrow", + x: 255, + y: 239, + boundElements: [{ id: text.id, type: "text" }], + startBinding: { + elementId: rectangle.id, + focus: 0, + gap: 1, + }, + endBinding: { + elementId: ellipse.id, + focus: 0, + }, + }); + + expect(text).toMatchObject({ + x: 340, + y: 226.5, + type: "text", + text: "HELLO WORLD!!", + containerId: arrow.id, + }); + + expect(rectangle).toMatchObject({ + x: 155, + y: 189, + type: "rectangle", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + expect(ellipse).toMatchObject({ + x: 555, + y: 189, + type: "ellipse", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to text when start / end provided without ids", () => { + const elements = [ + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + type: "text", + text: "HEYYYYY", + }, + end: { + type: "text", + text: "WHATS UP ?", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + + const [arrow, text1, text2, text3] = excaldrawElements; + + expect(arrow).toMatchObject({ + type: "arrow", + x: 255, + y: 239, + boundElements: [{ id: text1.id, type: "text" }], + startBinding: { + elementId: text2.id, + focus: 0, + gap: 1, + }, + endBinding: { + elementId: text3.id, + focus: 0, + }, + }); + + expect(text1).toMatchObject({ + x: 340, + y: 226.5, + type: "text", + text: "HELLO WORLD!!", + containerId: arrow.id, + }); + + expect(text2).toMatchObject({ + x: 185, + y: 226.5, + type: "text", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + expect(text3).toMatchObject({ + x: 555, + y: 226.5, + type: "text", + boundElements: [ + { + id: arrow.id, + type: "arrow", + }, + ], + }); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to existing shapes when start / end provided with ids", () => { + const elements = [ + { + type: "ellipse", + id: "ellipse-1", + strokeColor: "#66a80f", + x: 630, + y: 316, + width: 300, + height: 300, + backgroundColor: "#d8f5a2", + }, + { + type: "diamond", + id: "diamond-1", + strokeColor: "#9c36b5", + width: 140, + x: 96, + y: 400, + }, + { + type: "arrow", + x: 247, + y: 420, + width: 395, + height: 35, + strokeColor: "#1864ab", + start: { + type: "rectangle", + width: 300, + height: 300, + }, + end: { + id: "ellipse-1", + }, + }, + { + type: "arrow", + x: 227, + y: 450, + width: 400, + strokeColor: "#e67700", + start: { + id: "diamond-1", + }, + end: { + id: "ellipse-1", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(5); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to existing text elements when start / end provided with ids", () => { + const elements = [ + { + x: 100, + y: 239, + type: "text", + text: "HEYYYYY", + id: "text-1", + strokeColor: "#c2255c", + }, + { + type: "text", + id: "text-2", + x: 560, + y: 239, + text: "Whats up ?", + }, + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + id: "text-1", + }, + end: { + id: "text-2", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + + excaldrawElements.forEach((ele) => { + expect(ele).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + id: expect.any(String), + }); + }); + }); + + it("should bind arrows to existing elements if ids are correct", () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementationOnce(() => void 0); + const elements = [ + { + x: 100, + y: 239, + type: "text", + text: "HEYYYYY", + id: "text-1", + strokeColor: "#c2255c", + }, + { + type: "rectangle", + x: 560, + y: 139, + id: "rect-1", + width: 100, + height: 200, + backgroundColor: "#bac8ff", + }, + { + type: "arrow", + x: 255, + y: 239, + label: { + text: "HELLO WORLD!!", + }, + start: { + id: "text-13", + }, + end: { + id: "rect-11", + }, + }, + ]; + + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(4); + const [, , arrow] = excaldrawElements; + expect(arrow).toMatchObject({ + type: "arrow", + x: 255, + y: 239, + boundElements: [ + { + id: "id46", + type: "text", + }, + ], + startBinding: null, + endBinding: null, + }); + expect(consoleErrorSpy).toHaveBeenCalledTimes(2); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 1, + "No element for start binding with id text-13 found", + ); + expect(consoleErrorSpy).toHaveBeenNthCalledWith( + 2, + "No element for end binding with id rect-11 found", + ); + }); + + it("should bind when ids referenced before the element data", () => { + const elements = [ + { + type: "arrow", + x: 255, + y: 239, + end: { + id: "rect-1", + }, + }, + { + type: "rectangle", + x: 560, + y: 139, + id: "rect-1", + width: 100, + height: 200, + backgroundColor: "#bac8ff", + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + expect(excaldrawElements.length).toBe(2); + const [arrow, rect] = excaldrawElements; + expect((arrow as ExcalidrawArrowElement).endBinding).toStrictEqual({ + elementId: "rect-1", + focus: 0, + gap: 5, + }); + expect(rect.boundElements).toStrictEqual([ + { + id: "id47", + type: "arrow", + }, + ]); + }); + }); + + it("should not allow duplicate ids", () => { + const consoleErrorSpy = vi + .spyOn(console, "error") + .mockImplementationOnce(() => void 0); + const elements = [ + { + type: "rectangle", + x: 300, + y: 100, + id: "rect-1", + width: 100, + height: 200, + }, + + { + type: "rectangle", + x: 100, + y: 200, + id: "rect-1", + width: 100, + height: 200, + }, + ]; + const excaldrawElements = convertToExcalidrawElements( + elements as ExcalidrawElementSkeleton[], + ); + + expect(excaldrawElements.length).toBe(1); + expect(excaldrawElements[0]).toMatchSnapshot({ + seed: expect.any(Number), + versionNonce: expect.any(Number), + }); + expect(consoleErrorSpy).toHaveBeenCalledWith( + "Duplicate id found for rect-1", + ); + }); +}); diff --git a/src/data/transform.ts b/src/data/transform.ts new file mode 100644 index 0000000000..bf54119515 --- /dev/null +++ b/src/data/transform.ts @@ -0,0 +1,561 @@ +import { + DEFAULT_FONT_FAMILY, + DEFAULT_FONT_SIZE, + TEXT_ALIGN, + VERTICAL_ALIGN, +} from "../constants"; +import { + newElement, + newLinearElement, + redrawTextBoundingBox, +} from "../element"; +import { bindLinearElement } from "../element/binding"; +import { + ElementConstructorOpts, + newImageElement, + newTextElement, +} from "../element/newElement"; +import { + getDefaultLineHeight, + measureText, + normalizeText, +} from "../element/textElement"; +import { + ExcalidrawArrowElement, + ExcalidrawBindableElement, + ExcalidrawElement, + ExcalidrawEmbeddableElement, + ExcalidrawFrameElement, + ExcalidrawFreeDrawElement, + ExcalidrawGenericElement, + ExcalidrawImageElement, + ExcalidrawLinearElement, + ExcalidrawSelectionElement, + ExcalidrawTextElement, + FileId, + FontFamilyValues, + TextAlign, + VerticalAlign, +} from "../element/types"; +import { MarkOptional } from "../utility-types"; +import { assertNever, getFontString } from "../utils"; + +export type ValidLinearElement = { + type: "arrow" | "line"; + x: number; + y: number; + label?: { + text: string; + fontSize?: number; + fontFamily?: FontFamilyValues; + textAlign?: TextAlign; + verticalAlign?: VerticalAlign; + } & MarkOptional; + end?: + | ( + | ( + | { + type: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + id?: ExcalidrawGenericElement["id"]; + } + | { + id: ExcalidrawGenericElement["id"]; + type?: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + } + ) + | (( + | { + type: "text"; + text: string; + } + | { + type?: "text"; + id: ExcalidrawTextElement["id"]; + text: string; + } + ) & + Partial) + ) & + MarkOptional; + start?: + | ( + | ( + | { + type: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + id?: ExcalidrawGenericElement["id"]; + } + | { + id: ExcalidrawGenericElement["id"]; + type?: Exclude< + ExcalidrawBindableElement["type"], + "image" | "text" | "frame" | "embeddable" + >; + } + ) + | (( + | { + type: "text"; + text: string; + } + | { + type?: "text"; + id: ExcalidrawTextElement["id"]; + text: string; + } + ) & + Partial) + ) & + MarkOptional; +} & Partial; + +export type ValidContainer = + | { + type: Exclude; + id?: ExcalidrawGenericElement["id"]; + label?: { + text: string; + fontSize?: number; + fontFamily?: FontFamilyValues; + textAlign?: TextAlign; + verticalAlign?: VerticalAlign; + } & MarkOptional; + } & ElementConstructorOpts; + +export type ExcalidrawElementSkeleton = + | Extract< + Exclude, + | ExcalidrawEmbeddableElement + | ExcalidrawFreeDrawElement + | ExcalidrawFrameElement + > + | ({ + type: Extract; + x: number; + y: number; + } & Partial) + | ValidContainer + | ValidLinearElement + | ({ + type: "text"; + text: string; + x: number; + y: number; + id?: ExcalidrawTextElement["id"]; + } & Partial) + | ({ + type: Extract; + x: number; + y: number; + fileId: FileId; + } & Partial); + +const DEFAULT_LINEAR_ELEMENT_PROPS = { + width: 300, + height: 0, +}; + +const DEFAULT_DIMENSION = 100; + +const bindTextToContainer = ( + container: ExcalidrawElement, + textProps: { text: string } & MarkOptional, +) => { + const textElement: ExcalidrawTextElement = newTextElement({ + x: 0, + y: 0, + textAlign: TEXT_ALIGN.CENTER, + verticalAlign: VERTICAL_ALIGN.MIDDLE, + ...textProps, + containerId: container.id, + strokeColor: textProps.strokeColor || container.strokeColor, + }); + + Object.assign(container, { + boundElements: (container.boundElements || []).concat({ + type: "text", + id: textElement.id, + }), + }); + + redrawTextBoundingBox(textElement, container); + return [container, textElement] as const; +}; + +const bindLinearElementToElement = ( + linearElement: ExcalidrawArrowElement, + start: ValidLinearElement["start"], + end: ValidLinearElement["end"], + elementStore: ElementStore, +): { + linearElement: ExcalidrawLinearElement; + startBoundElement?: ExcalidrawElement; + endBoundElement?: ExcalidrawElement; +} => { + let startBoundElement; + let endBoundElement; + + Object.assign(linearElement, { + startBinding: linearElement?.startBinding || null, + endBinding: linearElement.endBinding || null, + }); + + if (start) { + const width = start?.width ?? DEFAULT_DIMENSION; + const height = start?.height ?? DEFAULT_DIMENSION; + + let existingElement; + if (start.id) { + existingElement = elementStore.getElement(start.id); + if (!existingElement) { + console.error(`No element for start binding with id ${start.id} found`); + } + } + + const startX = start.x || linearElement.x - width; + const startY = start.y || linearElement.y - height / 2; + const startType = existingElement ? existingElement.type : start.type; + + if (startType) { + if (startType === "text") { + let text = ""; + if (existingElement && existingElement.type === "text") { + text = existingElement.text; + } else if (start.type === "text") { + text = start.text; + } + if (!text) { + console.error( + `No text found for start binding text element for ${linearElement.id}`, + ); + } + startBoundElement = newTextElement({ + x: startX, + y: startY, + type: "text", + ...existingElement, + ...start, + text, + }); + // to position the text correctly when coordinates not provided + Object.assign(startBoundElement, { + x: start.x || linearElement.x - startBoundElement.width, + y: start.y || linearElement.y - startBoundElement.height / 2, + }); + } else { + switch (startType) { + case "rectangle": + case "ellipse": + case "diamond": { + startBoundElement = newElement({ + x: startX, + y: startY, + width, + height, + ...existingElement, + ...start, + type: startType, + }); + break; + } + default: { + assertNever( + linearElement as never, + `Unhandled element start type "${start.type}"`, + true, + ); + } + } + } + + bindLinearElement( + linearElement, + startBoundElement as ExcalidrawBindableElement, + "start", + ); + } + } + if (end) { + const height = end?.height ?? DEFAULT_DIMENSION; + const width = end?.width ?? DEFAULT_DIMENSION; + + let existingElement; + if (end.id) { + existingElement = elementStore.getElement(end.id); + if (!existingElement) { + console.error(`No element for end binding with id ${end.id} found`); + } + } + const endX = end.x || linearElement.x + linearElement.width; + const endY = end.y || linearElement.y - height / 2; + const endType = existingElement ? existingElement.type : end.type; + + if (endType) { + if (endType === "text") { + let text = ""; + if (existingElement && existingElement.type === "text") { + text = existingElement.text; + } else if (end.type === "text") { + text = end.text; + } + + if (!text) { + console.error( + `No text found for end binding text element for ${linearElement.id}`, + ); + } + endBoundElement = newTextElement({ + x: endX, + y: endY, + type: "text", + ...existingElement, + ...end, + text, + }); + // to position the text correctly when coordinates not provided + Object.assign(endBoundElement, { + y: end.y || linearElement.y - endBoundElement.height / 2, + }); + } else { + switch (endType) { + case "rectangle": + case "ellipse": + case "diamond": { + endBoundElement = newElement({ + x: endX, + y: endY, + width, + height, + ...existingElement, + ...end, + type: endType, + }); + break; + } + default: { + assertNever( + linearElement as never, + `Unhandled element end type "${endType}"`, + true, + ); + } + } + } + + bindLinearElement( + linearElement, + endBoundElement as ExcalidrawBindableElement, + "end", + ); + } + } + return { + linearElement, + startBoundElement, + endBoundElement, + }; +}; + +class ElementStore { + excalidrawElements = new Map(); + + add = (ele?: ExcalidrawElement) => { + if (!ele) { + return; + } + + this.excalidrawElements.set(ele.id, ele); + }; + getElements = () => { + return Array.from(this.excalidrawElements.values()); + }; + + getElement = (id: string) => { + return this.excalidrawElements.get(id); + }; +} + +export const convertToExcalidrawElements = ( + elements: ExcalidrawElementSkeleton[] | null, +) => { + if (!elements) { + return []; + } + + const elementStore = new ElementStore(); + const elementsWithIds = new Map(); + + // Create individual elements + for (const element of elements) { + let excalidrawElement: ExcalidrawElement; + switch (element.type) { + case "rectangle": + case "ellipse": + case "diamond": { + const width = + element?.label?.text && element.width === undefined + ? 0 + : element?.width || DEFAULT_DIMENSION; + const height = + element?.label?.text && element.height === undefined + ? 0 + : element?.height || DEFAULT_DIMENSION; + excalidrawElement = newElement({ + ...element, + width, + height, + }); + + break; + } + case "line": { + const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width; + const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height; + excalidrawElement = newLinearElement({ + width, + height, + points: [ + [0, 0], + [width, height], + ], + ...element, + }); + + break; + } + case "arrow": { + const width = element.width || DEFAULT_LINEAR_ELEMENT_PROPS.width; + const height = element.height || DEFAULT_LINEAR_ELEMENT_PROPS.height; + excalidrawElement = newLinearElement({ + width, + height, + endArrowhead: "arrow", + points: [ + [0, 0], + [width, height], + ], + ...element, + }); + break; + } + case "text": { + const fontFamily = element?.fontFamily || DEFAULT_FONT_FAMILY; + const fontSize = element?.fontSize || DEFAULT_FONT_SIZE; + const lineHeight = + element?.lineHeight || getDefaultLineHeight(fontFamily); + const text = element.text ?? ""; + const normalizedText = normalizeText(text); + const metrics = measureText( + normalizedText, + getFontString({ fontFamily, fontSize }), + lineHeight, + ); + + excalidrawElement = newTextElement({ + width: metrics.width, + height: metrics.height, + fontFamily, + fontSize, + ...element, + }); + break; + } + case "image": { + excalidrawElement = newImageElement({ + width: element?.width || DEFAULT_DIMENSION, + height: element?.height || DEFAULT_DIMENSION, + ...element, + }); + + break; + } + case "freedraw": + case "frame": + case "embeddable": { + excalidrawElement = element; + break; + } + + default: { + excalidrawElement = element; + assertNever( + element, + `Unhandled element type "${(element as any).type}"`, + true, + ); + } + } + const existingElement = elementStore.getElement(excalidrawElement.id); + if (existingElement) { + console.error(`Duplicate id found for ${excalidrawElement.id}`); + } else { + elementStore.add(excalidrawElement); + elementsWithIds.set(excalidrawElement.id, element); + } + } + + // Add labels and arrow bindings + for (const [id, element] of elementsWithIds) { + const excalidrawElement = elementStore.getElement(id)!; + + switch (element.type) { + case "rectangle": + case "ellipse": + case "diamond": + case "arrow": { + if (element.label?.text) { + let [container, text] = bindTextToContainer( + excalidrawElement, + element?.label, + ); + elementStore.add(container); + elementStore.add(text); + + if (container.type === "arrow") { + const originalStart = + element.type === "arrow" ? element?.start : undefined; + const originalEnd = + element.type === "arrow" ? element?.end : undefined; + const { linearElement, startBoundElement, endBoundElement } = + bindLinearElementToElement( + container as ExcalidrawArrowElement, + originalStart, + originalEnd, + elementStore, + ); + container = linearElement; + elementStore.add(linearElement); + elementStore.add(startBoundElement); + elementStore.add(endBoundElement); + } + } else { + switch (element.type) { + case "arrow": { + const { linearElement, startBoundElement, endBoundElement } = + bindLinearElementToElement( + excalidrawElement as ExcalidrawArrowElement, + element.start, + element.end, + elementStore, + ); + elementStore.add(linearElement); + elementStore.add(startBoundElement); + elementStore.add(endBoundElement); + break; + } + } + } + break; + } + } + } + return elementStore.getElements(); +}; diff --git a/src/element/binding.ts b/src/element/binding.ts index b175f14e76..bc9977596d 100644 --- a/src/element/binding.ts +++ b/src/element/binding.ts @@ -190,7 +190,7 @@ export const maybeBindLinearElement = ( } }; -const bindLinearElement = ( +export const bindLinearElement = ( linearElement: NonDeleted, hoveredElement: ExcalidrawBindableElement, startOrEnd: "start" | "end", diff --git a/src/element/newElement.ts b/src/element/newElement.ts index cb5657f1db..bbc98030cf 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -46,7 +46,7 @@ import { } from "../constants"; import { MarkOptional, Merge, Mutable } from "../utility-types"; -type ElementConstructorOpts = MarkOptional< +export type ElementConstructorOpts = MarkOptional< Omit, | "width" | "height" @@ -187,7 +187,7 @@ export const newTextElement = ( fontFamily?: FontFamilyValues; textAlign?: TextAlign; verticalAlign?: VerticalAlign; - containerId?: ExcalidrawTextContainer["id"]; + containerId?: ExcalidrawTextContainer["id"] | null; lineHeight?: ExcalidrawTextElement["lineHeight"]; strokeWidth?: ExcalidrawTextElement["strokeWidth"]; } & ElementConstructorOpts, @@ -361,8 +361,8 @@ export const newFreeDrawElement = ( export const newLinearElement = ( opts: { type: ExcalidrawLinearElement["type"]; - startArrowhead: Arrowhead | null; - endArrowhead: Arrowhead | null; + startArrowhead?: Arrowhead | null; + endArrowhead?: Arrowhead | null; points?: ExcalidrawLinearElement["points"]; } & ElementConstructorOpts, ): NonDeleted => { @@ -372,8 +372,8 @@ export const newLinearElement = ( lastCommittedPoint: null, startBinding: null, endBinding: null, - startArrowhead: opts.startArrowhead, - endArrowhead: opts.endArrowhead, + startArrowhead: opts.startArrowhead || null, + endArrowhead: opts.endArrowhead || null, }; }; @@ -477,7 +477,7 @@ export const deepCopyElement = ( * utility wrapper to generate new id. In test env it reuses the old + postfix * for test assertions. */ -const regenerateId = ( +export const regenerateId = ( /** supply null if no previous id exists */ previousId: string | null, ) => { diff --git a/src/element/textElement.ts b/src/element/textElement.ts index 61813d2b4a..ce80d0eb22 100644 --- a/src/element/textElement.ts +++ b/src/element/textElement.ts @@ -89,16 +89,23 @@ export const redrawTextBoundingBox = ( container, textElement as ExcalidrawTextElementWithContainer, ); + const maxContainerWidth = getBoundTextMaxWidth(container); - let nextHeight = container.height; if (metrics.height > maxContainerHeight) { - nextHeight = computeContainerDimensionForBoundText( + const nextHeight = computeContainerDimensionForBoundText( metrics.height, container.type, ); mutateElement(container, { height: nextHeight }); updateOriginalContainerCache(container.id, nextHeight); } + if (metrics.width > maxContainerWidth) { + const nextWidth = computeContainerDimensionForBoundText( + metrics.width, + container.type, + ); + mutateElement(container, { width: nextWidth }); + } const updatedTextElement = { ...textElement, ...boundTextUpdates, @@ -859,8 +866,9 @@ const VALID_CONTAINER_TYPES = new Set([ "arrow", ]); -export const isValidTextContainer = (element: ExcalidrawElement) => - VALID_CONTAINER_TYPES.has(element.type); +export const isValidTextContainer = (element: { + type: ExcalidrawElement["type"]; +}) => VALID_CONTAINER_TYPES.has(element.type); export const computeContainerDimensionForBoundText = ( dimension: number, diff --git a/src/packages/excalidraw/example/App.tsx b/src/packages/excalidraw/example/App.tsx index 1a5c6b9edc..1574442713 100644 --- a/src/packages/excalidraw/example/App.tsx +++ b/src/packages/excalidraw/example/App.tsx @@ -75,6 +75,7 @@ const { WelcomeScreen, MainMenu, LiveCollaborationTrigger, + convertToExcalidrawElements, } = window.ExcalidrawLib; const COMMENT_ICON_DIMENSION = 32; @@ -140,7 +141,10 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { ]; //@ts-ignore - initialStatePromiseRef.current.promise.resolve(initialData); + initialStatePromiseRef.current.promise.resolve({ + ...initialData, + elements: convertToExcalidrawElements(initialData.elements), + }); excalidrawAPI.addFiles(imagesArray); }; }; @@ -184,38 +188,40 @@ export default function App({ appTitle, useCustom, customArgs }: AppProps) { const updateScene = () => { const sceneData = { elements: restoreElements( - [ + convertToExcalidrawElements([ { type: "rectangle", - version: 141, - versionNonce: 361174001, - isDeleted: false, - id: "oDVXy8D6rom3H1-LLH2-f", + id: "rect-1", fillStyle: "hachure", strokeWidth: 1, strokeStyle: "solid", roughness: 1, - opacity: 100, angle: 0, x: 100.50390625, y: 93.67578125, strokeColor: "#c92a2a", - backgroundColor: "transparent", width: 186.47265625, height: 141.9765625, seed: 1968410350, - groupIds: [], - frameId: null, - boundElements: null, - locked: false, - link: null, - updated: 1, roundness: { type: ROUNDNESS.ADAPTIVE_RADIUS, value: 32, }, }, - ], + { + type: "arrow", + x: 300, + y: 150, + start: { id: "rect-1" }, + end: { type: "ellipse" }, + }, + { + type: "text", + x: 300, + y: 100, + text: "HELLO WORLD!", + }, + ]), null, ), appState: { diff --git a/src/packages/excalidraw/example/initialData.js b/src/packages/excalidraw/example/initialData.tsx similarity index 71% rename from src/packages/excalidraw/example/initialData.js rename to src/packages/excalidraw/example/initialData.tsx index 37b0755d35..92c7205d0a 100644 --- a/src/packages/excalidraw/example/initialData.js +++ b/src/packages/excalidraw/example/initialData.tsx @@ -1,107 +1,58 @@ +import { ExcalidrawElementSkeleton } from "../../../data/transform"; +import { FileId } from "../../../element/types"; + +const elements: ExcalidrawElementSkeleton[] = [ + { + type: "rectangle", + x: 10, + y: 10, + strokeWidth: 2, + }, + { + type: "diamond", + x: 120, + y: 20, + backgroundColor: "#fff3bf", + strokeWidth: 2, + label: { + text: "HELLO EXCALIDRAW", + strokeColor: "#099268", + fontSize: 30, + }, + }, + { + type: "arrow", + x: 100, + y: 200, + label: { text: "HELLO WORLD!!" }, + start: { type: "rectangle" }, + end: { type: "ellipse" }, + }, + { + type: "image", + x: 606.1042326312408, + y: 153.57729779411773, + width: 230, + height: 230, + fileId: "rocket" as FileId, + }, +]; export default { - elements: [ - { - type: "rectangle", - version: 141, - versionNonce: 361174001, - isDeleted: false, - id: "oDVXy8D6rom3H1-LLH2-f", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, - x: 100.50390625, - y: 93.67578125, - strokeColor: "#000000", - backgroundColor: "transparent", - width: 186.47265625, - height: 141.9765625, - seed: 1968410350, - groupIds: [], - frameId: null, - }, - { - id: "-xMIs_0jIFqvpx-R9UnaG", - type: "ellipse", - x: 300.5703125, - y: 190.69140625, - width: 198.21875, - height: 129.51171875, - angle: 0, - strokeColor: "#000000", - backgroundColor: "transparent", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - groupIds: [], - frameId: null, - seed: 957947807, - version: 47, - versionNonce: 1128618623, - isDeleted: false, - }, - { - fileId: "rocket", - type: "image", - x: 606.1042326312408, - y: 153.57729779411773, - width: 231.30325348751828, - height: 231.64340533088227, - angle: 0, - strokeColor: "transparent", - backgroundColor: "transparent", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - groupIds: [], - frameId: null, - strokeSharpness: "round", - seed: 707269846, - version: 143, - versionNonce: 2028982666, - isDeleted: false, - boundElements: null, - updated: 1644914782403, - link: null, - status: "pending", - scale: [1, 1], - }, - ], + elements, appState: { viewBackgroundColor: "#AFEEEE", currentItemFontFamily: 1 }, scrollToContent: true, libraryItems: [ [ { type: "line", - version: 1699, - versionNonce: 1813275999, - isDeleted: false, - id: "1OMHrnYMU3LJ3w3IaXU_R", - fillStyle: "hachure", - strokeWidth: 1, - strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, + x: 209.72304760646858, y: 338.83587294718825, strokeColor: "#881fa3", backgroundColor: "#be4bdb", width: 116.42036295658873, height: 103.65107323746608, - seed: 1445523839, - groupIds: [], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [-92.28090097254909, 7.105427357601002e-15], [-154.72281841151394, 19.199290805487394], @@ -111,37 +62,21 @@ export default { [-39.037226329125524, 21.285677238400705], [-92.28090097254909, 7.105427357601002e-15], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, ], [ { type: "line", - version: 3901, - versionNonce: 540959103, - isDeleted: false, - id: "b-rwW8s76ztV_uTu1SHq1", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, x: -249.48446738689245, y: 374.851387389359, strokeColor: "#0a11d3", backgroundColor: "#228be6", width: 88.21658171083376, height: 113.8575037534261, - seed: 1513238033, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, - strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [-0.22814350714115691, -43.414939319563715], [0.06274947619197979, 42.63794490105306], @@ -163,16 +98,9 @@ export default { [-0.2758413461535838, -43.46664538034193], [-0.22814350714115691, -43.414939319563715], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 1635, - versionNonce: 1383184881, - isDeleted: false, - id: "3CMZYj34FwjhgPB7jUC3f", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -187,11 +115,8 @@ export default { height: 9.797916664247975, seed: 683951089, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.1538602707609424], [2.326538897826852, 1.751753055375216], @@ -202,16 +127,9 @@ export default { [85.2899738827162, 1.3342483900732343], [88.30808627974527, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 1722, - versionNonce: 303290783, - isDeleted: false, - id: "DX3fUhBWtlJwYyrBDhebG", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -226,11 +144,8 @@ export default { height: 9.797916664247975, seed: 1817746897, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.1538602707609424], [2.326538897826852, 1.751753055375216], @@ -241,16 +156,9 @@ export default { [85.2899738827162, 1.3342483900732343], [88.30808627974527, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 4738, - versionNonce: 753357777, - isDeleted: false, - id: "a-Snvp2FgqDYqSLylF44S", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -265,16 +173,11 @@ export default { height: 17.72670397681366, seed: 1409727409, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], }, { type: "ellipse", - version: 109, - versionNonce: 1992641983, - isDeleted: false, - id: "7-6c-JFuB2yGoNQRgb2WM", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -289,16 +192,10 @@ export default { height: 13.941904362416096, seed: 1073094033, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 158, - versionNonce: 1028567473, - isDeleted: false, - id: "150XitJtlKDhTPRCyzv56", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -313,16 +210,10 @@ export default { height: 13.941904362416096, seed: 526271345, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 212, - versionNonce: 158547423, - isDeleted: false, - id: "cmwAR3NBl1VqvSorrQN2W", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -337,18 +228,12 @@ export default { height: 13.941904362416096, seed: 243707217, groupIds: ["N2YAi9nU-wlRb0rDaDZoe"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, ], [ { type: "diamond", - version: 659, - versionNonce: 1294871039, - isDeleted: false, - id: "aDDArXRjZugwyEawdhCeZ", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -363,16 +248,10 @@ export default { height: 36.77344700318558, seed: 511870335, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "diamond", - version: 700, - versionNonce: 60864881, - isDeleted: false, - id: "Hzx8zkeyDs3YicO2Tdv6G", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -387,16 +266,10 @@ export default { height: 36.77344700318558, seed: 1283079231, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "diamond", - version: 780, - versionNonce: 251040287, - isDeleted: false, - id: "PNzYhT295VNCT5EXmqvmw", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -411,16 +284,10 @@ export default { height: 36.77344700318558, seed: 996251633, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "diamond", - version: 822, - versionNonce: 1862951761, - isDeleted: false, - id: "jiMMAhQF3__7bF-obgXc0", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -435,18 +302,12 @@ export default { height: 36.77344700318558, seed: 1764842481, groupIds: ["M6ByXuSmtHCr3RtPPKJQh"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, ], [ { type: "line", - version: 4766, - versionNonce: 2003030321, - isDeleted: false, - id: "BXfdLRoPYZ9MIumzzoA9-", fillStyle: "hachure", strokeWidth: 1, strokeStyle: "solid", @@ -461,11 +322,8 @@ export default { height: 154.56722543646003, seed: 1424381745, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [-0.24755378372925183, -40.169554027464216], [-0.07503751055611152, 76.6515171914404], @@ -487,9 +345,6 @@ export default { [-0.2758413461535838, -40.23974757720194], [-0.24755378372925183, -40.169554027464216], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", @@ -511,11 +366,8 @@ export default { height: 12.698053371678215, seed: 726657713, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.0205717204386002], [1.3361877396713384, 3.0410845646550486], @@ -526,16 +378,9 @@ export default { [48.98410145879092, 2.500000505196364], [50.7174766392476, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 2538, - versionNonce: 1913946897, - isDeleted: false, - id: "VIuxhGjvYUBniitomEkKm", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -550,11 +395,8 @@ export default { height: 10.178760037658167, seed: 1977326481, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, -2.136356936862347], [1.332367676378171, 1.9210669226078037], @@ -565,16 +407,9 @@ export default { [48.84405948536458, 1.4873339211608216], [50.57247907260371, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 5503, - versionNonce: 1236644479, - isDeleted: false, - id: "1acGiqpJjntE3sr1JVnBP", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -589,7 +424,6 @@ export default { height: 22.797152568995934, seed: 1774660383, groupIds: ["HSrtfEf-CssQTf160Fb6R"], - frameId: null, strokeSharpness: "sharp", boundElementIds: ["bxuMGTzXLn7H-uBCptINx"], }, @@ -597,10 +431,6 @@ export default { [ { type: "rectangle", - version: 4270, - versionNonce: 309922463, - isDeleted: false, - id: "SqGRpNqls7OV1QB2Eq-0m", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", @@ -615,7 +445,6 @@ export default { height: 107.25081879410921, seed: 371096063, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "sharp", boundElementIds: [ "CFu0B4Mw_1wC1Hbgx8Fs0", @@ -625,10 +454,6 @@ export default { }, { type: "rectangle", - version: 4319, - versionNonce: 896119505, - isDeleted: false, - id: "fayss6b_GPh6LK1x4iX-q", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", @@ -643,7 +468,6 @@ export default { height: 107.25081879410921, seed: 685932433, groupIds: ["0RJwA-yKP5dqk5oMiSeot", "9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "sharp", boundElementIds: [ "CFu0B4Mw_1wC1Hbgx8Fs0", @@ -653,10 +477,6 @@ export default { }, { type: "rectangle", - version: 4417, - versionNonce: 1968987839, - isDeleted: false, - id: "HgAnv2rwYhUpLiJiZAXv-", fillStyle: "solid", strokeWidth: 2, strokeStyle: "solid", @@ -671,7 +491,6 @@ export default { height: 107.25081879410921, seed: 58634943, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "sharp", boundElementIds: [ "CFu0B4Mw_1wC1Hbgx8Fs0", @@ -681,10 +500,6 @@ export default { }, { type: "draw", - version: 3541, - versionNonce: 1680683185, - isDeleted: false, - id: "12aO-Bs9HdALZN_-tuQTr", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -699,24 +514,16 @@ export default { height: 3.249953844290203, seed: 1673003743, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0.6014697828497827], [40.42449133807562, 0.7588628355182573], [46.57983585730082, -2.491091008771946], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3567, - versionNonce: 620768991, - isDeleted: false, - id: "Ck_Y0EVPh_fsY0qoRnGiD", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -731,24 +538,16 @@ export default { height: 2.8032978840147194, seed: 1821527807, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [16.832548902953302, -2.8032978840147194], [45.567415680676426, -0.3275477042019195], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3592, - versionNonce: 1300624017, - isDeleted: false, - id: "a_7IZapEuD918VW1P8Ss_", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -763,25 +562,17 @@ export default { height: 4.280657518731036, seed: 1485707039, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [26.41225578429045, -0.2552319773002338], [37.62000339651456, 2.3153712935189787], [48.33668263438425, -1.9652862252120569], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3629, - versionNonce: 737475327, - isDeleted: false, - id: "8io6FVNdFOLsQ266W8Lni", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -796,9 +587,8 @@ export default { height: 2.9096445412231735, seed: 1042012991, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [10.166093050596771, -1.166642430373031], @@ -806,16 +596,9 @@ export default { [46.26079588567538, 0.6125567455206506], [54.40694982784246, -2.297087795702523], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3594, - versionNonce: 1982560369, - isDeleted: false, - id: "LJI5kY6tg7UFAjPV3fKL-", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -830,24 +613,16 @@ export default { height: 2.4757501798128, seed: 295443295, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [18.193786115221407, -0.5912874140789839], [46.92865289294453, 1.884462765733816], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "draw", - version: 3609, - versionNonce: 1857766175, - isDeleted: false, - id: "zCrZOHW-q8YWKLw6ltKxX", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -862,27 +637,19 @@ export default { height: 2.4757501798128, seed: 1734301567, groupIds: ["9ppmKFUbA4iKjt8FaDFox"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], + points: [ [0, 0], [8.093938105125233, 1.4279702913643746], [18.193786115221407, -0.5912874140789839], [46.92865289294453, 1.884462765733816], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, ], [ { type: "rectangle", - version: 676, - versionNonce: 1841530687, - isDeleted: false, - id: "XOD3vRhtoLWoxC9wF9Sk8", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -897,16 +664,10 @@ export default { height: 76.53703389977764, seed: 106569279, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "line", - version: 462, - versionNonce: 1737150513, - isDeleted: false, - id: "WBkTga1PjKzYK-tcGjnjZ", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -921,25 +682,15 @@ export default { height: 0, seed: 73916127, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, 0], [128.84193229844433, 0], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 282, - versionNonce: 1198409567, - isDeleted: false, - id: "FHX0ZsIzUUfYPJqrZ8Lso", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -954,16 +705,10 @@ export default { height: 5.001953125, seed: 387857791, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 327, - versionNonce: 1661182481, - isDeleted: false, - id: "ugVRR0f_uDOjrllO10yAs", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -978,16 +723,10 @@ export default { height: 5.001953125, seed: 1486370207, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 385, - versionNonce: 2047607679, - isDeleted: false, - id: "SBzNA0Sn-ou4QGxotj0SB", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1002,16 +741,10 @@ export default { height: 5.001953125, seed: 610150847, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 664, - versionNonce: 2135373809, - isDeleted: false, - id: "VKcfbELTVlyJ90m0bGsj0", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1026,35 +759,21 @@ export default { height: 42.72020253937572, seed: 144280593, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "draw", - version: 1281, - versionNonce: 1708997535, - isDeleted: false, - id: "zWrJVrKnkF5K8iXNxi9Aa", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", - roughness: 0, - opacity: 100, - angle: 0, x: -530.327851842306, y: 378.9357912947449, strokeColor: "#087f5b", backgroundColor: "#40c057", width: 28.226201983883442, height: 24.44112284281997, - seed: 29167967, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [4.907524351775825, 2.043055712211473], [3.0769604829149455, 1.6284171290602836], @@ -1079,16 +798,9 @@ export default { [4.669824267311791, 1.1200945145694894], [4.907524351775825, 2.043055712211473], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 701, - versionNonce: 1583157713, - isDeleted: false, - id: "LX6kTl9A8K36ld2MEV4tI", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1103,25 +815,15 @@ export default { height: 0, seed: 1443027377, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, 0], [42.095115772272244, 0], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "line", - version: 2908, - versionNonce: 252866495, - isDeleted: false, - id: "SHmV_QtcwxIE-peI_QOX1", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1134,13 +836,8 @@ export default { backgroundColor: "#99bcff", width: 29.31860660384862, height: 5.711199931375845, - seed: 244310513, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, points: [ [0, -2.341683327443203], [0.7724193963150375, -0.06510358900749044], @@ -1151,16 +848,9 @@ export default { [28.316582284417855, -0.3084668090492442], [29.31860660384862, -2.6041666666666288], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, { type: "ellipse", - version: 725, - versionNonce: 1969008561, - isDeleted: false, - id: "PKRg6SqIetkWIgRqBAnDY", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1175,22 +865,14 @@ export default { height: 44.82230388130942, seed: 683572113, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "line", - version: 3113, - versionNonce: 533471199, - isDeleted: false, - id: "HrelUAgvfxi_4v8MyL_iT", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", - roughness: 0, opacity: 90, - angle: 0, x: -544.828148539078, y: 402.0199316371545, strokeColor: "#000000", @@ -1199,11 +881,8 @@ export default { height: 5.896061363392446, seed: 318798801, groupIds: ["TC0RSM64Cxmu17MlE12-o"], - frameId: null, strokeSharpness: "round", - boundElementIds: [], - startBinding: null, - endBinding: null, + points: [ [0, 0], [4.103544916365185, -4.322122351104391], @@ -1213,18 +892,11 @@ export default { [28.316582284417855, -2.0990281379671547], [29.31860660384862, 0.2709794602754383], ], - lastCommittedPoint: null, - startArrowhead: null, - endArrowhead: null, }, ], [ { type: "rectangle", - version: 685, - versionNonce: 706399231, - isDeleted: false, - id: "dba8s5bDYEnF20oGn2a8b", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1239,16 +911,10 @@ export default { height: 108.30428902193904, seed: 1914896753, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "rectangle", - version: 835, - versionNonce: 851916657, - isDeleted: false, - id: "3HxCT4mFZF-jJ6m9pyOCt", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1263,16 +929,10 @@ export default { height: 82.83278895375764, seed: 1306468145, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "ellipse", - version: 881, - versionNonce: 704574495, - isDeleted: false, - id: "xX9mcMHy_0Bn-D0UAMyCc", fillStyle: "solid", strokeWidth: 1, strokeStyle: "solid", @@ -1287,16 +947,10 @@ export default { height: 11.427824006438863, seed: 93422161, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "rectangle", - version: 528, - versionNonce: 816914769, - isDeleted: false, - id: "h60d2h6UPYkopTlW_XEs4", fillStyle: "cross-hatch", strokeWidth: 1, strokeStyle: "solid", @@ -1311,22 +965,13 @@ export default { height: 19.889460471185775, seed: 11646495, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, { type: "rectangle", - version: 570, - versionNonce: 1198069823, - isDeleted: false, - id: "bZbx28BjXM33JV1UezMcH", fillStyle: "cross-hatch", strokeWidth: 1, strokeStyle: "solid", - roughness: 1, - opacity: 100, - angle: 0, x: -698.7169501405845, y: 384.7822247024333, strokeColor: "#000000", @@ -1335,9 +980,7 @@ export default { height: 19.889460471185775, seed: 291717649, groupIds: ["GMZ-NW9lG7c1AtfBInZ0n"], - frameId: null, strokeSharpness: "sharp", - boundElementIds: [], }, ], ], diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index c417cdfb33..901785f1cb 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -253,3 +253,4 @@ export { LiveCollaborationTrigger }; export { DefaultSidebar } from "../../components/DefaultSidebar"; export { normalizeLink } from "../../data/url"; +export { convertToExcalidrawElements } from "../../data/transform"; diff --git a/src/tests/data/restore.test.ts b/src/tests/data/restore.test.ts index e8ac49eded..0019b0e8bc 100644 --- a/src/tests/data/restore.test.ts +++ b/src/tests/data/restore.test.ts @@ -140,9 +140,8 @@ describe("restoreElements", () => { expect(restoredArrow).toMatchSnapshot({ seed: expect.any(Number) }); }); - it("when arrow element has defined endArrowHead", () => { + it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is null', () => { const arrowElement = API.createElement({ type: "arrow" }); - const restoredElements = restore.restoreElements([arrowElement], null); const restoredArrow = restoredElements[0] as ExcalidrawLinearElement; @@ -150,7 +149,7 @@ describe("restoreElements", () => { expect(arrowElement.endArrowhead).toBe(restoredArrow.endArrowhead); }); - it("when arrow element has undefined endArrowHead", () => { + it('should set arrow element endArrowHead as "arrow" when arrow element endArrowHead is undefined', () => { const arrowElement = API.createElement({ type: "arrow" }); Object.defineProperty(arrowElement, "endArrowhead", { get: vi.fn(() => undefined), diff --git a/src/utils.ts b/src/utils.ts index e14413eaf6..407fecd2e1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -914,3 +914,16 @@ export const isOnlyExportingSingleFrame = ( ) ); }; + +export const assertNever = ( + value: never, + message: string, + softAssert?: boolean, +): never => { + if (softAssert) { + console.error(message); + return value; + } + + throw new Error(message); +}; From a376bd949537f93b1f0cae2ca696cfe44065b226 Mon Sep 17 00:00:00 2001 From: Marcel Mraz Date: Sat, 12 Aug 2023 22:56:59 +0200 Subject: [PATCH 06/23] feat: partition main canvas vertically (#6759) Co-authored-by: Marcel Mraz Co-authored-by: dwelle --- package.json | 2 +- src/actions/actionCanvas.tsx | 2 +- src/actions/actionDuplicateSelection.tsx | 38 +- src/actions/actionFinalize.tsx | 9 +- src/actions/actionFrame.ts | 2 +- src/actions/actionGroup.tsx | 17 +- src/actions/actionSelectAll.ts | 34 +- src/components/Actions.tsx | 6 +- src/components/App.test.tsx | 4 +- src/components/App.tsx | 732 ++++--- src/components/EyeDropper.tsx | 4 +- src/components/JSONExportDialog.tsx | 4 +- src/components/LayerUI.tsx | 24 +- src/components/MobileMenu.tsx | 8 +- src/components/canvases/InteractiveCanvas.tsx | 222 +++ src/components/canvases/StaticCanvas.tsx | 113 ++ src/components/canvases/index.tsx | 4 + src/css/styles.scss | 14 +- src/data/blob.ts | 6 +- src/element/Hyperlink.tsx | 12 +- src/element/binding.ts | 1 + src/element/bounds.ts | 8 +- src/element/collision.ts | 8 +- src/element/linearElementEditor.ts | 17 +- src/element/mutateElement.ts | 4 +- src/element/sizeHelpers.ts | 40 +- src/element/textWysiwyg.test.tsx | 10 +- src/element/textWysiwyg.tsx | 2 +- src/element/transformHandles.ts | 6 +- src/excalidraw-app/data/tabSync.ts | 22 +- src/excalidraw-app/index.tsx | 2 +- src/frame.ts | 20 +- src/groups.ts | 246 ++- src/math.ts | 4 +- src/renderer/renderElement.ts | 234 +-- src/renderer/renderScene.ts | 1341 ++++++------- src/scene/Fonts.ts | 4 +- src/scene/Renderer.ts | 131 ++ src/scene/Scene.ts | 8 + src/scene/ShapeCache.ts | 61 + src/scene/export.ts | 23 +- src/scene/scroll.ts | 9 +- src/scene/scrollbars.ts | 22 +- src/scene/selection.ts | 4 +- src/scene/types.ts | 76 +- .../__snapshots__/contextmenu.test.tsx.snap | 1228 ++++++------ .../__snapshots__/dragCreate.test.tsx.snap | 20 +- .../linearElementEditor.test.tsx.snap | 2 +- src/tests/__snapshots__/move.test.tsx.snap | 24 +- .../multiPointCreate.test.tsx.snap | 8 +- .../regressionTests.test.tsx.snap | 1674 +++++++++++------ .../__snapshots__/selection.test.tsx.snap | 20 +- src/tests/contextmenu.test.tsx | 44 +- src/tests/dragCreate.test.tsx | 58 +- src/tests/helpers/api.ts | 4 +- src/tests/helpers/ui.ts | 27 +- src/tests/linearElementEditor.test.tsx | 95 +- src/tests/move.test.tsx | 38 +- src/tests/multiPointCreate.test.tsx | 32 +- src/tests/packages/excalidraw.test.tsx | 12 +- src/tests/regressionTests.test.tsx | 28 +- src/tests/resize.test.tsx | 5 +- src/tests/selection.test.tsx | 49 +- src/tests/test-utils.ts | 27 +- src/tests/viewMode.test.tsx | 20 +- src/tests/zindex.test.tsx | 2 +- src/types.ts | 53 +- src/utils.ts | 103 +- yarn.lock | 185 +- 69 files changed, 4348 insertions(+), 2970 deletions(-) create mode 100644 src/components/canvases/InteractiveCanvas.tsx create mode 100644 src/components/canvases/StaticCanvas.tsx create mode 100644 src/components/canvases/index.tsx create mode 100644 src/scene/Renderer.ts create mode 100644 src/scene/ShapeCache.ts diff --git a/package.json b/package.json index 8929905695..a2a66b5c1a 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,7 @@ "vite-plugin-ejs": "1.6.4", "vite-plugin-pwa": "0.16.4", "vite-plugin-svgr": "2.4.0", - "vitest": "0.32.2", + "vitest": "0.34.1", "vitest-canvas-mock": "0.3.2" }, "engines": { diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index d5ecfcfefe..6531203ee6 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -423,7 +423,7 @@ export const actionToggleHandTool = register({ type: "hand", lastActiveToolBeforeEraser: appState.activeTool, }); - setCursor(app.canvas, CURSOR_TYPE.GRAB); + setCursor(app.interactiveCanvas, CURSOR_TYPE.GRAB); } return { diff --git a/src/actions/actionDuplicateSelection.tsx b/src/actions/actionDuplicateSelection.tsx index 81dea88f1d..a21260d5bb 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/src/actions/actionDuplicateSelection.tsx @@ -259,23 +259,25 @@ const duplicateElements = ( return { elements: finalElements, - appState: selectGroupsForSelectedElements( - { - ...appState, - selectedGroupIds: {}, - selectedElementIds: nextElementsToSelect.reduce( - (acc: Record, element) => { - if (!isBoundToContainer(element)) { - acc[element.id] = true; - } - return acc; - }, - {}, - ), - }, - getNonDeletedElements(finalElements), - appState, - null, - ), + appState: { + ...appState, + ...selectGroupsForSelectedElements( + { + editingGroupId: appState.editingGroupId, + selectedElementIds: nextElementsToSelect.reduce( + (acc: Record, element) => { + if (!isBoundToContainer(element)) { + acc[element.id] = true; + } + return acc; + }, + {}, + ), + }, + getNonDeletedElements(finalElements), + appState, + null, + ), + }, }; }; diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 44f14d70b0..c25e2ef4d0 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -19,7 +19,12 @@ import { AppState } from "../types"; export const actionFinalize = register({ name: "finalize", trackEvent: false, - perform: (elements, appState, _, { canvas, focusContainer, scene }) => { + perform: ( + elements, + appState, + _, + { interactiveCanvas, focusContainer, scene }, + ) => { if (appState.editingLinearElement) { const { elementId, startBindingElement, endBindingElement } = appState.editingLinearElement; @@ -132,7 +137,7 @@ export const actionFinalize = register({ appState.activeTool.type !== "freedraw") || !multiPointElement ) { - resetCursor(canvas); + resetCursor(interactiveCanvas); } let activeTool: AppState["activeTool"]; diff --git a/src/actions/actionFrame.ts b/src/actions/actionFrame.ts index 7cf6ea4e45..339545f877 100644 --- a/src/actions/actionFrame.ts +++ b/src/actions/actionFrame.ts @@ -108,7 +108,7 @@ export const actionSetFrameAsActiveTool = register({ type: "frame", }); - setCursorForShape(app.canvas, { + setCursorForShape(app.interactiveCanvas, { ...appState, activeTool: nextActiveTool, }); diff --git a/src/actions/actionGroup.tsx b/src/actions/actionGroup.tsx index 82293d7d15..8ee84ac7bf 100644 --- a/src/actions/actionGroup.tsx +++ b/src/actions/actionGroup.tsx @@ -149,11 +149,14 @@ export const actionGroup = register({ ]; return { - appState: selectGroup( - newGroupId, - { ...appState, selectedGroupIds: {} }, - getNonDeletedElements(nextElements), - ), + appState: { + ...appState, + ...selectGroup( + newGroupId, + { ...appState, selectedGroupIds: {} }, + getNonDeletedElements(nextElements), + ), + }, elements: nextElements, commitToHistory: true, }; @@ -212,7 +215,7 @@ export const actionUngroup = register({ }); const updateAppState = selectGroupsForSelectedElements( - { ...appState, selectedGroupIds: {} }, + appState, getNonDeletedElements(nextElements), appState, null, @@ -243,7 +246,7 @@ export const actionUngroup = register({ ); return { - appState: updateAppState, + appState: { ...appState, ...updateAppState }, elements: nextElements, commitToHistory: true, }; diff --git a/src/actions/actionSelectAll.ts b/src/actions/actionSelectAll.ts index eeadf2dc93..49f5072ce3 100644 --- a/src/actions/actionSelectAll.ts +++ b/src/actions/actionSelectAll.ts @@ -28,22 +28,24 @@ export const actionSelectAll = register({ }, {}); return { - appState: selectGroupsForSelectedElements( - { - ...appState, - selectedLinearElement: - // single linear element selected - Object.keys(selectedElementIds).length === 1 && - isLinearElement(elements[0]) - ? new LinearElementEditor(elements[0], app.scene) - : null, - editingGroupId: null, - selectedElementIds, - }, - getNonDeletedElements(elements), - appState, - app, - ), + appState: { + ...appState, + ...selectGroupsForSelectedElements( + { + editingGroupId: null, + selectedElementIds, + }, + getNonDeletedElements(elements), + appState, + app, + ), + selectedLinearElement: + // single linear element selected + Object.keys(selectedElementIds).length === 1 && + isLinearElement(elements[0]) + ? new LinearElementEditor(elements[0], app.scene) + : null, + }, commitToHistory: true, }; }, diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 86381572f4..e9483eac14 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -213,13 +213,13 @@ export const SelectedShapeActions = ({ }; export const ShapesSwitcher = ({ - canvas, + interactiveCanvas, activeTool, setAppState, onImageAction, appState, }: { - canvas: HTMLCanvasElement | null; + interactiveCanvas: HTMLCanvasElement | null; activeTool: UIAppState["activeTool"]; setAppState: React.Component["setState"]; onImageAction: (data: { pointerType: PointerType | null }) => void; @@ -270,7 +270,7 @@ export const ShapesSwitcher = ({ multiElement: null, selectedElementIds: {}, }); - setCursorForShape(canvas, { + setCursorForShape(interactiveCanvas, { ...appState, activeTool: nextActiveTool, }); diff --git a/src/components/App.test.tsx b/src/components/App.test.tsx index 32f62f7ada..19f987308d 100644 --- a/src/components/App.test.tsx +++ b/src/components/App.test.tsx @@ -6,14 +6,14 @@ import { render, queryByTestId } from "../tests/test-utils"; import ExcalidrawApp from "../excalidraw-app"; import { vi } from "vitest"; -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); describe("Test ", () => { beforeEach(async () => { // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); localStorage.clear(); - renderScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); diff --git a/src/components/App.tsx b/src/components/App.tsx index 901b983645..2a39bc6b2f 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -189,15 +189,13 @@ import { isArrowKey, KEYS, } from "../keys"; - +import { isElementInViewport } from "../element/sizeHelpers"; import { distance2d, getCornerRadius, getGridPoint, isPathALoop, } from "../math"; -import { isVisibleElement, renderScene } from "../renderer/renderScene"; -import { invalidateShapeForElement } from "../renderer/renderElement"; import { calculateScrollCenter, getElementsAtPosition, @@ -209,7 +207,7 @@ import { isSomeElementSelected, } from "../scene"; import Scene from "../scene/Scene"; -import { RenderConfig, ScrollBars } from "../scene/types"; +import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types"; import { getStateForZoom } from "../scene/zoom"; import { findShapeByKey, SHAPES } from "../shapes"; import { @@ -352,6 +350,9 @@ import { } from "../data/transform"; import { ValueOf } from "../utility-types"; import { isSidebarDockedAtom } from "./Sidebar/Sidebar"; +import { StaticCanvas, InteractiveCanvas } from "./canvases"; +import { Renderer } from "../scene/Renderer"; +import { ShapeCache } from "../scene/ShapeCache"; const AppContext = React.createContext(null!); const AppPropsContext = React.createContext(null!); @@ -429,10 +430,6 @@ const YOUTUBE_VIDEO_STATES = new Map< ValueOf >(); -// remove this hack when we can sync render & resizeObserver (state update) -// to rAF. See #5439 -let THROTTLE_NEXT_RENDER = true; - let IS_PLAIN_PASTE = false; let IS_PLAIN_PASTE_TIMER = 0; let PLAIN_PASTE_TOAST_SHOWN = false; @@ -446,8 +443,9 @@ const gesture: Gesture = { }; class App extends React.Component { - canvas: AppClassProperties["canvas"] = null; - rc: RoughCanvas | null = null; + canvas: AppClassProperties["canvas"]; + interactiveCanvas: AppClassProperties["interactiveCanvas"] = null; + rc: RoughCanvas; unmounted: boolean = false; actionManager: ActionManager; device: Device = deviceContextInitialValue; @@ -461,6 +459,7 @@ class App extends React.Component { }; public scene: Scene; + public renderer: Renderer; private fonts: Fonts; private resizeObserver: ResizeObserver | undefined; private nearestScrollableContainer: HTMLElement | Document | undefined; @@ -505,8 +504,16 @@ class App extends React.Component { width: window.innerWidth, height: window.innerHeight, }; + this.id = nanoid(); + this.library = new Library(this); + this.scene = new Scene(); + + this.canvas = document.createElement("canvas"); + this.rc = rough.canvas(this.canvas); + this.renderer = new Renderer(this.scene); + if (excalidrawRef) { const readyPromise = ("current" in excalidrawRef && excalidrawRef.current?.readyPromise) || @@ -549,7 +556,6 @@ class App extends React.Component { id: this.id, }; - this.scene = new Scene(); this.fonts = new Fonts({ scene: this.scene, onSceneUpdated: this.onSceneUpdated, @@ -567,65 +573,6 @@ class App extends React.Component { this.actionManager.registerAction(createRedoAction(this.history)); } - private renderCanvas() { - const canvasScale = window.devicePixelRatio; - const { - width: canvasDOMWidth, - height: canvasDOMHeight, - viewModeEnabled, - } = this.state; - const canvasWidth = canvasDOMWidth * canvasScale; - const canvasHeight = canvasDOMHeight * canvasScale; - if (viewModeEnabled) { - return ( - ) => - this.handleCanvasContextMenu(event) - } - onPointerMove={this.handleCanvasPointerMove} - onPointerUp={this.handleCanvasPointerUp} - onPointerCancel={this.removePointer} - onTouchMove={this.handleTouchMove} - onPointerDown={this.handleCanvasPointerDown} - > - {t("labels.drawingCanvas")} - - ); - } - return ( - ) => - this.handleCanvasContextMenu(event) - } - onPointerDown={this.handleCanvasPointerDown} - onDoubleClick={this.handleCanvasDoubleClick} - onPointerMove={this.handleCanvasPointerMove} - onPointerUp={this.handleCanvasPointerUp} - onPointerCancel={this.removePointer} - onTouchMove={this.handleTouchMove} - > - {t("labels.drawingCanvas")} - - ); - } - private onWindowMessage(event: MessageEvent) { if ( event.origin !== "https://player.vimeo.com" && @@ -817,7 +764,7 @@ class App extends React.Component { ); mutateElement(element, { validated }, false); - invalidateShapeForElement(element); + ShapeCache.delete(element); } } return false; @@ -855,7 +802,7 @@ class App extends React.Component { this.state, ); const embedLink = getEmbedLink(toValidURL(el.link || "")); - const isVisible = isVisibleElement( + const isVisible = isElementInViewport( el, normalizedWidth, normalizedHeight, @@ -1017,8 +964,7 @@ class App extends React.Component { return this.scene.getNonDeletedFrames().map((f, index) => { if ( - !this.canvas || - !isVisibleElement( + !isElementInViewport( f, this.canvas.width / window.devicePixelRatio, this.canvas.height / window.devicePixelRatio, @@ -1141,9 +1087,7 @@ class App extends React.Component { }} onPointerDown={(event) => this.handleCanvasPointerDown(event)} onWheel={(event) => this.handleWheel(event)} - onContextMenu={(event: React.PointerEvent) => { - this.handleCanvasContextMenu(event); - }} + onContextMenu={this.handleCanvasContextMenu} onDoubleClick={() => { this.setState({ editingFrame: f.id, @@ -1157,9 +1101,24 @@ class App extends React.Component { }; public render() { - const selectedElement = this.scene.getSelectedElements(this.state); + const selectedElements = this.scene.getSelectedElements(this.state); const { renderTopRightUI, renderCustomStats } = this.props; + const versionNonce = this.scene.getVersionNonce(); + const { canvasElements, visibleElements } = + this.renderer.getRenderableElements({ + versionNonce, + zoom: this.state.zoom, + offsetLeft: this.state.offsetLeft, + offsetTop: this.state.offsetTop, + scrollX: this.state.scrollX, + scrollY: this.state.scrollY, + height: this.state.height, + width: this.state.width, + editingElement: this.state.editingElement, + pendingImageElementId: this.state.pendingImageElementId, + }); + return (
{ > {
- {selectedElement.length === 1 && + {selectedElements.length === 1 && !this.state.contextMenu && this.state.showHyperlinkPopup && ( { actionManager={this.actionManager} /> )} -
{this.renderCanvas()}
+ + {this.renderFrameNames()} {this.renderEmbeddables()} @@ -1552,17 +1551,13 @@ class App extends React.Component { if (initialData?.scrollToContent) { scene.appState = { ...scene.appState, - ...calculateScrollCenter( - scene.elements, - { - ...scene.appState, - width: this.state.width, - height: this.state.height, - offsetTop: this.state.offsetTop, - offsetLeft: this.state.offsetLeft, - }, - null, - ), + ...calculateScrollCenter(scene.elements, { + ...scene.appState, + width: this.state.width, + height: this.state.height, + offsetTop: this.state.offsetTop, + offsetLeft: this.state.offsetLeft, + }), }; } // FontFaceSet loadingdone event we listen on may not always fire @@ -1644,7 +1639,6 @@ class App extends React.Component { if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) { this.resizeObserver = new ResizeObserver(() => { - THROTTLE_NEXT_RENDER = false; // recompute device dimensions state // --------------------------------------------------------------------- this.refreshDeviceState(this.excalidrawContainerRef.current!); @@ -1700,6 +1694,9 @@ class App extends React.Component { } public componentWillUnmount() { + this.renderer.destroy(); + this.scene = new Scene(); + this.renderer = new Renderer(this.scene); this.files = {}; this.imageCache.clear(); this.resizeObserver?.disconnect(); @@ -1707,15 +1704,17 @@ class App extends React.Component { this.removeEventListeners(); this.scene.destroy(); this.library.destroy(); + ShapeCache.destroy(); clearTimeout(touchTimeout); isSomeElementSelected.clearCache(); + selectGroupsForSelectedElements.clearCache(); touchTimeout = 0; } private onResize = withBatchedUpdates(() => { this.scene .getElementsIncludingDeleted() - .forEach((element) => invalidateShapeForElement(element)); + .forEach((element) => ShapeCache.delete(element)); this.setState({}); }); @@ -1880,7 +1879,7 @@ class App extends React.Component { this.state.activeTool.type === "eraser" && prevState.theme !== this.state.theme ) { - setEraserCursor(this.canvas, this.state.theme); + setEraserCursor(this.interactiveCanvas, this.state.theme); } // Hide hyperlink popup if shown when element type is not selection if ( @@ -1976,7 +1975,6 @@ class App extends React.Component { ), ); } - this.renderScene(); this.history.record(this.state, this.scene.getElementsIncludingDeleted()); // Do not notify consumers if we're still loading the scene. Among other @@ -1992,114 +1990,24 @@ class App extends React.Component { } } - private renderScene = () => { - const cursorButton: { - [id: string]: string | undefined; - } = {}; - const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] = - {}; - const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] = - {}; - const pointerUsernames: { [id: string]: string } = {}; - const pointerUserStates: { [id: string]: string } = {}; - this.state.collaborators.forEach((user, socketId) => { - if (user.selectedElementIds) { - for (const id of Object.keys(user.selectedElementIds)) { - if (!(id in remoteSelectedElementIds)) { - remoteSelectedElementIds[id] = []; - } - remoteSelectedElementIds[id].push(socketId); - } - } - if (!user.pointer) { - return; - } - if (user.username) { - pointerUsernames[socketId] = user.username; - } - if (user.userState) { - pointerUserStates[socketId] = user.userState; - } - pointerViewportCoords[socketId] = sceneCoordsToViewportCoords( - { - sceneX: user.pointer.x, - sceneY: user.pointer.y, - }, - this.state, - ); - cursorButton[socketId] = user.button; - }); - - const renderingElements = this.scene - .getNonDeletedElements() - .filter((element) => { - if (isImageElement(element)) { - if ( - // not placed on canvas yet (but in elements array) - this.state.pendingImageElementId === element.id - ) { - return false; - } - } - // don't render text element that's being currently edited (it's - // rendered on remote only) - return ( - !this.state.editingElement || - this.state.editingElement.type !== "text" || - element.id !== this.state.editingElement.id - ); - }); - - const selectionColor = getComputedStyle( - document.querySelector(".excalidraw")!, - ).getPropertyValue("--color-selection"); - - renderScene( - { - elements: renderingElements, - appState: this.state, - scale: window.devicePixelRatio, - rc: this.rc!, - canvas: this.canvas!, - renderConfig: { - selectionColor, - scrollX: this.state.scrollX, - scrollY: this.state.scrollY, - viewBackgroundColor: this.state.viewBackgroundColor, - zoom: this.state.zoom, - remotePointerViewportCoords: pointerViewportCoords, - remotePointerButton: cursorButton, - remoteSelectedElementIds, - remotePointerUsernames: pointerUsernames, - remotePointerUserStates: pointerUserStates, - shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom, - theme: this.state.theme, - imageCache: this.imageCache, - isExporting: false, - renderScrollbars: false, - }, - callback: ({ atLeastOneVisibleElement, scrollBars }) => { - if (scrollBars) { - currentScrollBars = scrollBars; - } - const scrolledOutside = - // hide when editing text - isTextElement(this.state.editingElement) - ? false - : !atLeastOneVisibleElement && renderingElements.length > 0; - if (this.state.scrolledOutside !== scrolledOutside) { - this.setState({ scrolledOutside }); - } - - this.scheduleImageRefresh(); - }, - }, - THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true, - ); - - if (!THROTTLE_NEXT_RENDER) { - THROTTLE_NEXT_RENDER = true; + private renderInteractiveSceneCallback = ({ + atLeastOneVisibleElement, + scrollBars, + elements, + }: RenderInteractiveSceneCallback) => { + if (scrollBars) { + currentScrollBars = scrollBars; } + const scrolledOutside = + // hide when editing text + isTextElement(this.state.editingElement) + ? false + : !atLeastOneVisibleElement && elements.length > 0; + if (this.state.scrolledOutside !== scrolledOutside) { + this.setState({ scrolledOutside }); + } + + this.scheduleImageRefresh(); }; private onScroll = debounce(() => { @@ -2150,7 +2058,7 @@ class App extends React.Component { didTapTwice = false; } - private onTapStart = (event: TouchEvent) => { + private onTouchStart = (event: TouchEvent) => { // fix for Apple Pencil Scribble // On Android, preventing the event would disable contextMenu on tap-hold if (!isAndroid) { @@ -2169,7 +2077,7 @@ class App extends React.Component { // insert text only if we tapped twice with a single finger // event.touches.length === 1 will also prevent inserting text when user's zooming if (didTapTwice && event.touches.length === 1) { - const [touch] = event.touches; + const touch = event.touches[0]; // @ts-ignore this.handleCanvasDoubleClick({ clientX: touch.clientX, @@ -2190,7 +2098,7 @@ class App extends React.Component { } }; - private onTapEnd = (event: TouchEvent) => { + private onTouchEnd = (event: TouchEvent) => { this.resetContextMenuTimer(); if (event.touches.length > 0) { this.setState({ @@ -2399,35 +2307,37 @@ class App extends React.Component { excludeElementsInFramesFromSelection(newElements); this.setState( - selectGroupsForSelectedElements( - { - ...this.state, - // keep sidebar (presumably the library) open if it's docked and - // can fit. - // - // Note, we should close the sidebar only if we're dropping items - // from library, not when pasting from clipboard. Alas. - openSidebar: - this.state.openSidebar && - this.device.canDeviceFitSidebar && - jotaiStore.get(isSidebarDockedAtom) - ? this.state.openSidebar - : null, - selectedElementIds: nextElementsToSelect.reduce( - (acc: Record, element) => { - if (!isBoundToContainer(element)) { - acc[element.id] = true; - } - return acc; - }, - {}, - ), - selectedGroupIds: {}, - }, - this.scene.getNonDeletedElements(), - this.state, - this, - ), + { + ...this.state, + // keep sidebar (presumably the library) open if it's docked and + // can fit. + // + // Note, we should close the sidebar only if we're dropping items + // from library, not when pasting from clipboard. Alas. + openSidebar: + this.state.openSidebar && + this.device.canDeviceFitSidebar && + jotaiStore.get(isSidebarDockedAtom) + ? this.state.openSidebar + : null, + ...selectGroupsForSelectedElements( + { + editingGroupId: null, + selectedElementIds: nextElementsToSelect.reduce( + (acc: Record, element) => { + if (!isBoundToContainer(element)) { + acc[element.id] = true; + } + return acc; + }, + {}, + ), + }, + this.scene.getNonDeletedElements(), + this.state, + this, + ), + }, () => { if (opts.files) { this.addNewImagesToImageCache(); @@ -2686,11 +2596,7 @@ class App extends React.Component { scrollY = appState.scrollY; } else { // compute only the viewport location, without any zoom adjustment - const scroll = calculateScrollCenter( - targetElements, - this.state, - this.canvas, - ); + const scroll = calculateScrollCenter(targetElements, this.state); scrollX = scroll.scrollX; scrollY = scroll.scrollY; } @@ -2796,7 +2702,7 @@ class App extends React.Component { filesMap.has(element.fileId) ) { this.imageCache.delete(element.fileId); - invalidateShapeForElement(element); + ShapeCache.delete(element); } }); this.scene.informMutation(); @@ -3079,7 +2985,7 @@ class App extends React.Component { } if (event.key === KEYS.SPACE && gesture.pointers.size === 0) { isHoldingSpace = true; - setCursor(this.canvas, CURSOR_TYPE.GRAB); + setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); event.preventDefault(); } @@ -3143,11 +3049,11 @@ class App extends React.Component { private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => { if (event.key === KEYS.SPACE) { if (this.state.viewModeEnabled) { - setCursor(this.canvas, CURSOR_TYPE.GRAB); + setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); } else if (this.state.activeTool.type === "selection") { - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); } else { - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); this.setState({ selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, @@ -3183,9 +3089,9 @@ class App extends React.Component { ) => { const nextActiveTool = updateActiveTool(this.state, tool); if (nextActiveTool.type === "hand") { - setCursor(this.canvas, CURSOR_TYPE.GRAB); + setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); } else if (!isHoldingSpace) { - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); } if (isToolIcon(document.activeElement)) { this.focusContainer(); @@ -3210,11 +3116,11 @@ class App extends React.Component { }; private setCursor = (cursor: string) => { - setCursor(this.canvas, cursor); + setCursor(this.interactiveCanvas, cursor); }; private resetCursor = () => { - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); }; /** * returns whether user is making a gesture with >= 2 fingers (points) @@ -3374,7 +3280,7 @@ class App extends React.Component { editingElement: null, }); if (this.state.activeTool.locked) { - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); } this.focusContainer(); @@ -3674,7 +3580,7 @@ class App extends React.Component { } } - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords( event, @@ -3691,24 +3597,23 @@ class App extends React.Component { getSelectedGroupIdForElement(hitElement, this.state.selectedGroupIds); if (selectedGroupId) { - this.setState((prevState) => - selectGroupsForSelectedElements( + this.setState((prevState) => ({ + ...prevState, + ...selectGroupsForSelectedElements( { - ...prevState, editingGroupId: selectedGroupId, selectedElementIds: { [hitElement!.id]: true }, - selectedGroupIds: {}, }, this.scene.getNonDeletedElements(), prevState, this, ), - ); + })); return; } } - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) { const hitElement = this.getElementAtPosition(sceneX, sceneY); @@ -3934,9 +3839,9 @@ class App extends React.Component { const isOverScrollBar = isPointerOverScrollBars.isOverEither; if (!this.state.draggingElement && !this.state.multiElement) { if (isOverScrollBar) { - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); } else { - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); } } @@ -3999,7 +3904,7 @@ class App extends React.Component { const { points, lastCommittedPoint } = multiElement; const lastPoint = points[points.length - 1]; - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); if (lastPoint === lastCommittedPoint) { // if we haven't yet created a temp point and we're beyond commit-zone @@ -4016,7 +3921,7 @@ class App extends React.Component { points: [...points, [scenePointerX - rx, scenePointerY - ry]], }); } else { - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); // in this branch, we're inside the commit zone, and no uncommitted // point exists. Thus do nothing (don't add/remove points). } @@ -4030,7 +3935,7 @@ class App extends React.Component { lastCommittedPoint[1], ) < LINE_CONFIRM_THRESHOLD ) { - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); mutateElement(multiElement, { points: points.slice(0, -1), }); @@ -4060,7 +3965,7 @@ class App extends React.Component { } if (isPathALoop(points, this.state.zoom.value)) { - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } // update last uncommitted point mutateElement(multiElement, { @@ -4108,7 +4013,7 @@ class App extends React.Component { elementWithTransformHandleType.transformHandleType ) { setCursor( - this.canvas, + this.interactiveCanvas, getCursorForResizingElement(elementWithTransformHandleType), ); return; @@ -4123,7 +4028,7 @@ class App extends React.Component { ); if (transformHandleType) { setCursor( - this.canvas, + this.interactiveCanvas, getCursorForResizingElement({ transformHandleType, }), @@ -4147,7 +4052,7 @@ class App extends React.Component { this.hitLinkElement && !this.state.selectedElementIds[this.hitLinkElement.id] ) { - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); showHyperlinkTooltip(this.hitLinkElement, this.state); } else { hideHyperlinkToolip(); @@ -4161,13 +4066,13 @@ class App extends React.Component { this.setState({ showHyperlinkPopup: "info" }); } else if (this.state.activeTool.type === "text") { setCursor( - this.canvas, + this.interactiveCanvas, isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR, ); } else if (this.state.viewModeEnabled) { - setCursor(this.canvas, CURSOR_TYPE.GRAB); + setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); } else if (isOverScrollBar) { - setCursor(this.canvas, CURSOR_TYPE.AUTO); + setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } else if (this.state.selectedLinearElement) { this.handleHoverSelectedLinearElement( this.state.selectedLinearElement, @@ -4196,19 +4101,19 @@ class App extends React.Component { scenePointerY, ) ) { - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); this.setState({ activeEmbeddable: { element: hitElement, state: "hover" }, }); } else { - setCursor(this.canvas, CURSOR_TYPE.MOVE); + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); if (this.state.activeEmbeddable?.state === "hover") { this.setState({ activeEmbeddable: null }); } } } } else { - setCursor(this.canvas, CURSOR_TYPE.AUTO); + setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } } }; @@ -4346,9 +4251,9 @@ class App extends React.Component { ); if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) { - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } else { - setCursor(this.canvas, CURSOR_TYPE.MOVE); + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); } } else if ( shouldShowBoundingBox([element], this.state) && @@ -4360,7 +4265,7 @@ class App extends React.Component { scenePointerY, ) ) { - setCursor(this.canvas, CURSOR_TYPE.MOVE); + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); } else if ( boundTextElement && hitTest( @@ -4371,7 +4276,7 @@ class App extends React.Component { scenePointerY, ) ) { - setCursor(this.canvas, CURSOR_TYPE.MOVE); + setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE); } if ( @@ -4399,7 +4304,7 @@ class App extends React.Component { }); } } else { - setCursor(this.canvas, CURSOR_TYPE.AUTO); + setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } } private handleCanvasPointerDown = ( @@ -4553,7 +4458,7 @@ class App extends React.Component { ); } else if (this.state.activeTool.type === "image") { // reset image preview on pointerdown - setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR); + setCursor(this.interactiveCanvas, CURSOR_TYPE.CROSSHAIR); // retrieve the latest element as the state may be stale const pendingImageElement = @@ -4583,7 +4488,7 @@ class App extends React.Component { pointerDownState, ); } else if (this.state.activeTool.type === "custom") { - setCursor(this.canvas, CURSOR_TYPE.AUTO); + setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO); } else if (this.state.activeTool.type === "frame") { this.createFrameElementOnPointerDown(pointerDownState); } else if ( @@ -4751,7 +4656,7 @@ class App extends React.Component { let nextPastePrevented = false; const isLinux = /Linux/.test(window.navigator.platform); - setCursor(this.canvas, CURSOR_TYPE.GRABBING); + setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING); let { clientX: lastX, clientY: lastY } = event; const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => { const deltaX = lastX - event.clientX; @@ -4804,9 +4709,9 @@ class App extends React.Component { isPanning = false; if (!isHoldingSpace) { if (this.state.viewModeEnabled) { - setCursor(this.canvas, CURSOR_TYPE.GRAB); + setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB); } else { - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); } } this.setState({ @@ -4929,7 +4834,7 @@ class App extends React.Component { const onPointerUp = withBatchedUpdates(() => { isDraggingScrollBar = false; - setCursorForShape(this.canvas, this.state); + setCursorForShape(this.interactiveCanvas, this.state); lastPointerUp = null; this.setState({ cursorButton: "up", @@ -5203,19 +5108,21 @@ class App extends React.Component { } } - return selectGroupsForSelectedElements( - { - ...prevState, - selectedElementIds: nextSelectedElementIds, - showHyperlinkPopup: - hitElement.link || isEmbeddableElement(hitElement) - ? "info" - : false, - }, - this.scene.getNonDeletedElements(), - prevState, - this, - ); + return { + ...selectGroupsForSelectedElements( + { + editingGroupId: prevState.editingGroupId, + selectedElementIds: nextSelectedElementIds, + }, + this.scene.getNonDeletedElements(), + prevState, + this, + ), + showHyperlinkPopup: + hitElement.link || isEmbeddableElement(hitElement) + ? "info" + : false, + }; }); pointerDownState.hit.wasAddedToSelection = true; } @@ -5290,7 +5197,7 @@ class App extends React.Component { container, }); - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); if (!this.state.activeTool.locked) { this.setState({ activeTool: updateActiveTool(this.state, { type: "selection" }), @@ -5501,7 +5408,7 @@ class App extends React.Component { mutateElement(multiElement, { lastCommittedPoint: multiElement.points[multiElement.points.length - 1], }); - setCursor(this.canvas, CURSOR_TYPE.POINTER); + setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); } else { const [gridX, gridY] = getGridPoint( pointerDownState.origin.x, @@ -6147,34 +6054,36 @@ class App extends React.Component { } } - return selectGroupsForSelectedElements( - { - ...prevState, - ...(!shouldReuseSelection && { - selectedGroupIds: {}, - editingGroupId: null, - }), - selectedElementIds: nextSelectedElementIds, - showHyperlinkPopup: - elementsWithinSelection.length === 1 && - (elementsWithinSelection[0].link || - isEmbeddableElement(elementsWithinSelection[0])) - ? "info" - : false, - // select linear element only when we haven't box-selected anything else - selectedLinearElement: - elementsWithinSelection.length === 1 && - isLinearElement(elementsWithinSelection[0]) - ? new LinearElementEditor( - elementsWithinSelection[0], - this.scene, - ) - : null, - }, - this.scene.getNonDeletedElements(), - prevState, - this, - ); + prevState = !shouldReuseSelection + ? { ...prevState, selectedGroupIds: {}, editingGroupId: null } + : prevState; + + return { + ...selectGroupsForSelectedElements( + { + editingGroupId: prevState.editingGroupId, + selectedElementIds: nextSelectedElementIds, + }, + this.scene.getNonDeletedElements(), + prevState, + this, + ), + // select linear element only when we haven't box-selected anything else + selectedLinearElement: + elementsWithinSelection.length === 1 && + isLinearElement(elementsWithinSelection[0]) + ? new LinearElementEditor( + elementsWithinSelection[0], + this.scene, + ) + : null, + showHyperlinkPopup: + elementsWithinSelection.length === 1 && + (elementsWithinSelection[0].link || + isEmbeddableElement(elementsWithinSelection[0])) + ? "info" + : false, + }; }); } } @@ -6428,7 +6337,7 @@ class App extends React.Component { } this.setState({ suggestedBindings: [], startBoundElement: null }); if (!activeTool.locked) { - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); this.setState((prevState) => ({ draggingElement: null, activeTool: updateActiveTool(this.state, { @@ -6775,24 +6684,26 @@ class App extends React.Component { { selectedElementIds: newSelectedElementIds }, ); - return selectGroupsForSelectedElements( - { - ...prevState, - selectedElementIds: newSelectedElementIds, - // set selectedLinearElement only if thats the only element selected - selectedLinearElement: - newSelectedElements.length === 1 && - isLinearElement(newSelectedElements[0]) - ? new LinearElementEditor( - newSelectedElements[0], - this.scene, - ) - : prevState.selectedLinearElement, - }, - this.scene.getNonDeletedElements(), - prevState, - this, - ); + return { + ...selectGroupsForSelectedElements( + { + editingGroupId: prevState.editingGroupId, + selectedElementIds: newSelectedElementIds, + }, + this.scene.getNonDeletedElements(), + prevState, + this, + ), + // set selectedLinearElement only if thats the only element selected + selectedLinearElement: + newSelectedElements.length === 1 && + isLinearElement(newSelectedElements[0]) + ? new LinearElementEditor( + newSelectedElements[0], + this.scene, + ) + : prevState.selectedLinearElement, + }; }); } } else if ( @@ -6820,19 +6731,21 @@ class App extends React.Component { delete nextSelectedElementIds[element.id]; }); - return selectGroupsForSelectedElements( - { - ...prevState, - selectedElementIds: nextSelectedElementIds, - showHyperlinkPopup: - hitElement.link || isEmbeddableElement(hitElement) - ? "info" - : false, - }, - this.scene.getNonDeletedElements(), - prevState, - this, - ); + return { + ...selectGroupsForSelectedElements( + { + editingGroupId: prevState.editingGroupId, + selectedElementIds: nextSelectedElementIds, + }, + this.scene.getNonDeletedElements(), + prevState, + this, + ), + showHyperlinkPopup: + hitElement.link || isEmbeddableElement(hitElement) + ? "info" + : false, + }; }); } else { // add element to selection while keeping prev elements selected @@ -6850,20 +6763,20 @@ class App extends React.Component { this.setState((prevState) => ({ ...selectGroupsForSelectedElements( { - ...prevState, + editingGroupId: prevState.editingGroupId, selectedElementIds: { [hitElement.id]: true }, - selectedLinearElement: - isLinearElement(hitElement) && - // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. - // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized - prevState.selectedLinearElement?.elementId !== hitElement.id - ? new LinearElementEditor(hitElement, this.scene) - : prevState.selectedLinearElement, }, this.scene.getNonDeletedElements(), prevState, this, ), + selectedLinearElement: + isLinearElement(hitElement) && + // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. + // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized + prevState.selectedLinearElement?.elementId !== hitElement.id + ? new LinearElementEditor(hitElement, this.scene) + : prevState.selectedLinearElement, })); } } @@ -6931,7 +6844,7 @@ class App extends React.Component { } if (!activeTool.locked && activeTool.type !== "freedraw") { - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); this.setState({ draggingElement: null, suggestedBindings: [], @@ -7040,7 +6953,7 @@ class App extends React.Component { } const mimeType = imageFile.type; - setCursor(this.canvas, "wait"); + setCursor(this.interactiveCanvas, "wait"); if (mimeType === MIME_TYPES.svg) { try { @@ -7140,7 +7053,7 @@ class App extends React.Component { reject(new Error(t("errors.imageInsertError"))); } finally { if (!showCursorImagePreview) { - resetCursor(this.canvas); + resetCursor(this.interactiveCanvas); } } }, @@ -7209,7 +7122,7 @@ class App extends React.Component { } if (this.state.pendingImageElementId) { - setCursor(this.canvas, `url(${previewDataURL}) 4 4, auto`); + setCursor(this.interactiveCanvas, `url(${previewDataURL}) 4 4, auto`); } }; @@ -7350,7 +7263,7 @@ class App extends React.Component { if (updatedFiles.size || erroredFiles.size) { for (const element of elements) { if (updatedFiles.has(element.fileId)) { - invalidateShapeForElement(element); + ShapeCache.delete(element); } } } @@ -7491,21 +7404,35 @@ class App extends React.Component { }); } - private handleCanvasRef = (canvas: HTMLCanvasElement) => { + private handleInteractiveCanvasRef = (canvas: HTMLCanvasElement | null) => { // canvas is null when unmounting if (canvas !== null) { - this.canvas = canvas; - this.rc = rough.canvas(this.canvas); + this.interactiveCanvas = canvas; - this.canvas.addEventListener(EVENT.WHEEL, this.handleWheel, { - passive: false, - }); - this.canvas.addEventListener(EVENT.TOUCH_START, this.onTapStart); - this.canvas.addEventListener(EVENT.TOUCH_END, this.onTapEnd); + // ----------------------------------------------------------------------- + // NOTE wheel, touchstart, touchend events must be registered outside + // of react because react binds them them passively (so we can't prevent + // default on them) + this.interactiveCanvas.addEventListener(EVENT.WHEEL, this.handleWheel); + this.interactiveCanvas.addEventListener( + EVENT.TOUCH_START, + this.onTouchStart, + ); + this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTouchEnd); + // ----------------------------------------------------------------------- } else { - this.canvas?.removeEventListener(EVENT.WHEEL, this.handleWheel); - this.canvas?.removeEventListener(EVENT.TOUCH_START, this.onTapStart); - this.canvas?.removeEventListener(EVENT.TOUCH_END, this.onTapEnd); + this.interactiveCanvas?.removeEventListener( + EVENT.WHEEL, + this.handleWheel, + ); + this.interactiveCanvas?.removeEventListener( + EVENT.TOUCH_START, + this.onTouchStart, + ); + this.interactiveCanvas?.removeEventListener( + EVENT.TOUCH_END, + this.onTouchEnd, + ); } }; @@ -7651,13 +7578,15 @@ class App extends React.Component { }; private handleCanvasContextMenu = ( - event: React.PointerEvent, + event: React.MouseEvent, ) => { event.preventDefault(); if ( - (event.nativeEvent.pointerType === "touch" || - (event.nativeEvent.pointerType === "pen" && + (("pointerType" in event.nativeEvent && + event.nativeEvent.pointerType === "touch") || + ("pointerType" in event.nativeEvent && + event.nativeEvent.pointerType === "pen" && // always allow if user uses a pen secondary button event.button !== POINTER_BUTTON.SECONDARY)) && this.state.activeTool.type !== "selection" @@ -7691,18 +7620,21 @@ class App extends React.Component { this.setState( { ...(element && !this.state.selectedElementIds[element.id] - ? selectGroupsForSelectedElements( - { - ...this.state, - selectedElementIds: { [element.id]: true }, - selectedLinearElement: isLinearElement(element) - ? new LinearElementEditor(element, this.scene) - : null, - }, - this.scene.getNonDeletedElements(), - this.state, - this, - ) + ? { + ...this.state, + ...selectGroupsForSelectedElements( + { + editingGroupId: this.state.editingGroupId, + selectedElementIds: { [element.id]: true }, + }, + this.scene.getNonDeletedElements(), + this.state, + this, + ), + selectedLinearElement: isLinearElement(element) + ? new LinearElementEditor(element, this.scene) + : null, + } : this.state), showHyperlinkPopup: false, }, @@ -7992,7 +7924,9 @@ class App extends React.Component { }; private handleWheel = withBatchedUpdates( - (event: WheelEvent | React.WheelEvent) => { + ( + event: WheelEvent | React.WheelEvent, + ) => { event.preventDefault(); if (isPanning) { return; diff --git a/src/components/EyeDropper.tsx b/src/components/EyeDropper.tsx index 8e3e21ece3..5278dfab60 100644 --- a/src/components/EyeDropper.tsx +++ b/src/components/EyeDropper.tsx @@ -8,9 +8,9 @@ import { mutateElement } from "../element/mutateElement"; import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer"; import { useOutsideClick } from "../hooks/useOutsideClick"; import { KEYS } from "../keys"; -import { invalidateShapeForElement } from "../renderer/renderElement"; import { getSelectedElements } from "../scene"; import Scene from "../scene/Scene"; +import { ShapeCache } from "../scene/ShapeCache"; import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App"; import "./EyeDropper.scss"; @@ -98,7 +98,7 @@ export const EyeDropper: React.FC<{ }, false, ); - invalidateShapeForElement(element); + ShapeCache.delete(element); } Scene.getScene( metaStuffRef.current.selectedElements[0], diff --git a/src/components/JSONExportDialog.tsx b/src/components/JSONExportDialog.tsx index f40ebe4532..e4d51da43f 100644 --- a/src/components/JSONExportDialog.tsx +++ b/src/components/JSONExportDialog.tsx @@ -34,7 +34,7 @@ const JSONExportModal = ({ actionManager: ActionManager; onCloseRequest: () => void; exportOpts: ExportOpts; - canvas: HTMLCanvasElement | null; + canvas: HTMLCanvasElement; }) => { const { onExportToBackend } = exportOpts; return ( @@ -100,7 +100,7 @@ export const JSONExportDialog = ({ files: BinaryFiles; actionManager: ActionManager; exportOpts: ExportOpts; - canvas: HTMLCanvasElement | null; + canvas: HTMLCanvasElement; setAppState: React.Component["setState"]; }) => { const handleClose = React.useCallback(() => { diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index 100259a815..26be77aefa 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -57,7 +57,8 @@ interface LayerUIProps { actionManager: ActionManager; appState: UIAppState; files: BinaryFiles; - canvas: HTMLCanvasElement | null; + canvas: HTMLCanvasElement; + interactiveCanvas: HTMLCanvasElement | null; setAppState: React.Component["setState"]; elements: readonly NonDeletedExcalidrawElement[]; onLockToggle: () => void; @@ -117,6 +118,7 @@ const LayerUI = ({ setAppState, elements, canvas, + interactiveCanvas, onLockToggle, onHandToolToggle, onPenModeToggle, @@ -272,7 +274,7 @@ const LayerUI = ({ { @@ -413,7 +415,7 @@ const LayerUI = ({ onLockToggle={onLockToggle} onHandToolToggle={onHandToolToggle} onPenModeToggle={onPenModeToggle} - canvas={canvas} + interactiveCanvas={interactiveCanvas} onImageAction={onImageAction} renderTopRightUI={renderTopRightUI} renderCustomStats={renderCustomStats} @@ -464,7 +466,7 @@ const LayerUI = ({ className="scroll-back-to-content" onClick={() => { setAppState((appState) => ({ - ...calculateScrollCenter(elements, appState, canvas), + ...calculateScrollCenter(elements, appState), })); }} > @@ -507,8 +509,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => { return false; } - const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps; - const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps; + const { + canvas: _pC, + interactiveCanvas: _pIC, + appState: prevAppState, + ...prev + } = prevProps; + const { + canvas: _nC, + interactiveCanvas: _nIC, + appState: nextAppState, + ...next + } = nextProps; return ( isShallowEqual( diff --git a/src/components/MobileMenu.tsx b/src/components/MobileMenu.tsx index 30f431cbfe..f08b999691 100644 --- a/src/components/MobileMenu.tsx +++ b/src/components/MobileMenu.tsx @@ -36,7 +36,7 @@ type MobileMenuProps = { onLockToggle: () => void; onHandToolToggle: () => void; onPenModeToggle: () => void; - canvas: HTMLCanvasElement | null; + interactiveCanvas: HTMLCanvasElement | null; onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void; renderTopRightUI?: ( @@ -58,7 +58,7 @@ export const MobileMenu = ({ onLockToggle, onHandToolToggle, onPenModeToggle, - canvas, + interactiveCanvas, onImageAction, renderTopRightUI, renderCustomStats, @@ -85,7 +85,7 @@ export const MobileMenu = ({ { @@ -202,7 +202,7 @@ export const MobileMenu = ({ className="scroll-back-to-content" onClick={() => { setAppState((appState) => ({ - ...calculateScrollCenter(elements, appState, canvas), + ...calculateScrollCenter(elements, appState), })); }} > diff --git a/src/components/canvases/InteractiveCanvas.tsx b/src/components/canvases/InteractiveCanvas.tsx new file mode 100644 index 0000000000..11bbec24b7 --- /dev/null +++ b/src/components/canvases/InteractiveCanvas.tsx @@ -0,0 +1,222 @@ +import React, { useEffect, useRef } from "react"; +import { renderInteractiveScene } from "../../renderer/renderScene"; +import { + isRenderThrottlingEnabled, + isShallowEqual, + sceneCoordsToViewportCoords, +} from "../../utils"; +import { CURSOR_TYPE } from "../../constants"; +import { t } from "../../i18n"; +import type { DOMAttributes } from "react"; +import type { AppState, InteractiveCanvasAppState } from "../../types"; +import type { + InteractiveCanvasRenderConfig, + RenderInteractiveSceneCallback, +} from "../../scene/types"; +import type { NonDeletedExcalidrawElement } from "../../element/types"; + +type InteractiveCanvasProps = { + canvas: HTMLCanvasElement | null; + elements: readonly NonDeletedExcalidrawElement[]; + visibleElements: readonly NonDeletedExcalidrawElement[]; + selectedElements: readonly NonDeletedExcalidrawElement[]; + versionNonce: number | undefined; + selectionNonce: number | undefined; + scale: number; + appState: InteractiveCanvasAppState; + renderInteractiveSceneCallback: ( + data: RenderInteractiveSceneCallback, + ) => void; + handleCanvasRef: (canvas: HTMLCanvasElement | null) => void; + onContextMenu: Exclude< + DOMAttributes["onContextMenu"], + undefined + >; + onPointerMove: Exclude< + DOMAttributes["onPointerMove"], + undefined + >; + onPointerUp: Exclude< + DOMAttributes["onPointerUp"], + undefined + >; + onPointerCancel: Exclude< + DOMAttributes["onPointerCancel"], + undefined + >; + onTouchMove: Exclude< + DOMAttributes["onTouchMove"], + undefined + >; + onPointerDown: Exclude< + DOMAttributes["onPointerDown"], + undefined + >; + onDoubleClick: Exclude< + DOMAttributes["onDoubleClick"], + undefined + >; +}; + +const InteractiveCanvas = (props: InteractiveCanvasProps) => { + const isComponentMounted = useRef(false); + + useEffect(() => { + if (!isComponentMounted.current) { + isComponentMounted.current = true; + return; + } + + const cursorButton: { + [id: string]: string | undefined; + } = {}; + const pointerViewportCoords: InteractiveCanvasRenderConfig["remotePointerViewportCoords"] = + {}; + const remoteSelectedElementIds: InteractiveCanvasRenderConfig["remoteSelectedElementIds"] = + {}; + const pointerUsernames: { [id: string]: string } = {}; + const pointerUserStates: { [id: string]: string } = {}; + + props.appState.collaborators.forEach((user, socketId) => { + if (user.selectedElementIds) { + for (const id of Object.keys(user.selectedElementIds)) { + if (!(id in remoteSelectedElementIds)) { + remoteSelectedElementIds[id] = []; + } + remoteSelectedElementIds[id].push(socketId); + } + } + if (!user.pointer) { + return; + } + if (user.username) { + pointerUsernames[socketId] = user.username; + } + if (user.userState) { + pointerUserStates[socketId] = user.userState; + } + pointerViewportCoords[socketId] = sceneCoordsToViewportCoords( + { + sceneX: user.pointer.x, + sceneY: user.pointer.y, + }, + props.appState, + ); + cursorButton[socketId] = user.button; + }); + + const selectionColor = getComputedStyle( + document.querySelector(".excalidraw")!, + ).getPropertyValue("--color-selection"); + + renderInteractiveScene( + { + canvas: props.canvas, + elements: props.elements, + visibleElements: props.visibleElements, + selectedElements: props.selectedElements, + scale: window.devicePixelRatio, + appState: props.appState, + renderConfig: { + remotePointerViewportCoords: pointerViewportCoords, + remotePointerButton: cursorButton, + remoteSelectedElementIds, + remotePointerUsernames: pointerUsernames, + remotePointerUserStates: pointerUserStates, + selectionColor, + renderScrollbars: false, + }, + callback: props.renderInteractiveSceneCallback, + }, + isRenderThrottlingEnabled(), + ); + }); + + return ( + + {t("labels.drawingCanvas")} + + ); +}; + +const getRelevantAppStateProps = ( + appState: AppState, +): Omit => ({ + zoom: appState.zoom, + scrollX: appState.scrollX, + scrollY: appState.scrollY, + width: appState.width, + height: appState.height, + viewModeEnabled: appState.viewModeEnabled, + editingGroupId: appState.editingGroupId, + editingLinearElement: appState.editingLinearElement, + selectedElementIds: appState.selectedElementIds, + frameToHighlight: appState.frameToHighlight, + offsetLeft: appState.offsetLeft, + offsetTop: appState.offsetTop, + theme: appState.theme, + pendingImageElementId: appState.pendingImageElementId, + selectionElement: appState.selectionElement, + selectedGroupIds: appState.selectedGroupIds, + selectedLinearElement: appState.selectedLinearElement, + multiElement: appState.multiElement, + isBindingEnabled: appState.isBindingEnabled, + suggestedBindings: appState.suggestedBindings, + isRotating: appState.isRotating, + elementsToHighlight: appState.elementsToHighlight, + openSidebar: appState.openSidebar, + showHyperlinkPopup: appState.showHyperlinkPopup, + collaborators: appState.collaborators, // Necessary for collab. sessions + activeEmbeddable: appState.activeEmbeddable, +}); + +const areEqual = ( + prevProps: InteractiveCanvasProps, + nextProps: InteractiveCanvasProps, +) => { + // This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation + if ( + prevProps.selectionNonce !== nextProps.selectionNonce || + prevProps.versionNonce !== nextProps.versionNonce || + prevProps.scale !== nextProps.scale || + // we need to memoize on element arrays because they may have renewed + // even if versionNonce didn't change (e.g. we filter elements out based + // on appState) + prevProps.elements !== nextProps.elements || + prevProps.visibleElements !== nextProps.visibleElements || + prevProps.selectedElements !== nextProps.selectedElements + ) { + return false; + } + + // Comparing the interactive appState for changes in case of some edge cases + return isShallowEqual( + // asserting AppState because we're being passed the whole AppState + // but resolve to only the InteractiveCanvas-relevant props + getRelevantAppStateProps(prevProps.appState as AppState), + getRelevantAppStateProps(nextProps.appState as AppState), + ); +}; + +export default React.memo(InteractiveCanvas, areEqual); diff --git a/src/components/canvases/StaticCanvas.tsx b/src/components/canvases/StaticCanvas.tsx new file mode 100644 index 0000000000..8babdb7456 --- /dev/null +++ b/src/components/canvases/StaticCanvas.tsx @@ -0,0 +1,113 @@ +import React, { useEffect, useRef } from "react"; +import { RoughCanvas } from "roughjs/bin/canvas"; +import { renderStaticScene } from "../../renderer/renderScene"; +import { isRenderThrottlingEnabled, isShallowEqual } from "../../utils"; +import type { AppState, StaticCanvasAppState } from "../../types"; +import type { StaticCanvasRenderConfig } from "../../scene/types"; +import type { NonDeletedExcalidrawElement } from "../../element/types"; + +type StaticCanvasProps = { + canvas: HTMLCanvasElement; + rc: RoughCanvas; + elements: readonly NonDeletedExcalidrawElement[]; + visibleElements: readonly NonDeletedExcalidrawElement[]; + versionNonce: number | undefined; + selectionNonce: number | undefined; + scale: number; + appState: StaticCanvasAppState; + renderConfig: StaticCanvasRenderConfig; +}; + +const StaticCanvas = (props: StaticCanvasProps) => { + const wrapperRef = useRef(null); + const isComponentMounted = useRef(false); + + useEffect(() => { + const wrapper = wrapperRef.current; + if (!wrapper) { + return; + } + + const canvas = props.canvas; + + if (!isComponentMounted.current) { + isComponentMounted.current = true; + + wrapper.replaceChildren(canvas); + canvas.classList.add("excalidraw__canvas", "static"); + } + + canvas.style.width = `${props.appState.width}px`; + canvas.style.height = `${props.appState.height}px`; + canvas.width = props.appState.width * props.scale; + canvas.height = props.appState.height * props.scale; + + renderStaticScene( + { + canvas, + rc: props.rc, + scale: props.scale, + elements: props.elements, + visibleElements: props.visibleElements, + appState: props.appState, + renderConfig: props.renderConfig, + }, + isRenderThrottlingEnabled(), + ); + }); + + return
; +}; + +const getRelevantAppStateProps = ( + appState: AppState, +): Omit< + StaticCanvasAppState, + | "editingElement" + | "selectedElementIds" + | "editingGroupId" + | "frameToHighlight" +> => ({ + zoom: appState.zoom, + scrollX: appState.scrollX, + scrollY: appState.scrollY, + width: appState.width, + height: appState.height, + viewModeEnabled: appState.viewModeEnabled, + offsetLeft: appState.offsetLeft, + offsetTop: appState.offsetTop, + theme: appState.theme, + pendingImageElementId: appState.pendingImageElementId, + shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom, + viewBackgroundColor: appState.viewBackgroundColor, + exportScale: appState.exportScale, + selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged, + gridSize: appState.gridSize, + frameRendering: appState.frameRendering, +}); + +const areEqual = ( + prevProps: StaticCanvasProps, + nextProps: StaticCanvasProps, +) => { + if ( + prevProps.versionNonce !== nextProps.versionNonce || + prevProps.scale !== nextProps.scale || + // we need to memoize on element arrays because they may have renewed + // even if versionNonce didn't change (e.g. we filter elements out based + // on appState) + prevProps.elements !== nextProps.elements || + prevProps.visibleElements !== nextProps.visibleElements + ) { + return false; + } + + return isShallowEqual( + // asserting AppState because we're being passed the whole AppState + // but resolve to only the StaticCanvas-relevant props + getRelevantAppStateProps(prevProps.appState as AppState), + getRelevantAppStateProps(nextProps.appState as AppState), + ); +}; + +export default React.memo(StaticCanvas, areEqual); diff --git a/src/components/canvases/index.tsx b/src/components/canvases/index.tsx new file mode 100644 index 0000000000..b3956d78b8 --- /dev/null +++ b/src/components/canvases/index.tsx @@ -0,0 +1,4 @@ +import InteractiveCanvas from "./InteractiveCanvas"; +import StaticCanvas from "./StaticCanvas"; + +export { InteractiveCanvas, StaticCanvas }; diff --git a/src/css/styles.scss b/src/css/styles.scss index a9aebe7d67..f1d359d5e4 100644 --- a/src/css/styles.scss +++ b/src/css/styles.scss @@ -3,8 +3,9 @@ :root { --zIndex-canvas: 1; - --zIndex-wysiwyg: 2; - --zIndex-layerUI: 3; + --zIndex-interactiveCanvas: 2; + --zIndex-wysiwyg: 3; + --zIndex-layerUI: 4; --zIndex-modal: 1000; --zIndex-popup: 1001; @@ -69,10 +70,19 @@ z-index: var(--zIndex-canvas); + &.interactive { + z-index: var(--zIndex-interactiveCanvas); + } + // Remove the main canvas from document flow to avoid resizeObserver // feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379) } + &__canvas-wrapper, + &__canvas.static { + pointer-events: none; + } + &__canvas { position: absolute; } diff --git a/src/data/blob.ts b/src/data/blob.ts index c0aa66ee73..922400303f 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -144,11 +144,7 @@ export const loadSceneOrLibraryFromBlob = async ( fileHandle: fileHandle || blob.handle || null, ...cleanAppStateForExport(data.appState || {}), ...(localAppState - ? calculateScrollCenter( - data.elements || [], - localAppState, - null, - ) + ? calculateScrollCenter(data.elements || [], localAppState) : {}), }, files: data.files, diff --git a/src/element/Hyperlink.tsx b/src/element/Hyperlink.tsx index 9fa760f682..720f17824a 100644 --- a/src/element/Hyperlink.tsx +++ b/src/element/Hyperlink.tsx @@ -25,10 +25,7 @@ import { } from "react"; import clsx from "clsx"; import { KEYS } from "../keys"; -import { - DEFAULT_LINK_SIZE, - invalidateShapeForElement, -} from "../renderer/renderElement"; +import { DEFAULT_LINK_SIZE } from "../renderer/renderElement"; import { rotate } from "../math"; import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants"; import { Bounds } from "./bounds"; @@ -42,6 +39,7 @@ import "./Hyperlink.scss"; import { trackEvent } from "../analytics"; import { useAppProps, useExcalidrawAppState } from "../components/App"; import { isEmbeddableElement } from "./typeChecks"; +import { ShapeCache } from "../scene/ShapeCache"; const CONTAINER_WIDTH = 320; const SPACE_BOTTOM = 85; @@ -115,7 +113,7 @@ export const Hyperlink = ({ validated: false, link, }); - invalidateShapeForElement(element); + ShapeCache.delete(element); } else { const { width, height } = element; const embedLink = getEmbedLink(link); @@ -147,7 +145,7 @@ export const Hyperlink = ({ validated: true, link, }); - invalidateShapeForElement(element); + ShapeCache.delete(element); if (embeddableLinkCache.has(element.id)) { embeddableLinkCache.delete(element.id); } @@ -393,7 +391,7 @@ export const getContextMenuLabel = ( export const getLinkHandleFromCoords = ( [x1, y1, x2, y2]: Bounds, angle: number, - appState: UIAppState, + appState: Pick, ): [x: number, y: number, width: number, height: number] => { const size = DEFAULT_LINK_SIZE; const linkWidth = size / appState.zoom.value; diff --git a/src/element/binding.ts b/src/element/binding.ts index bc9977596d..3f6cf0022d 100644 --- a/src/element/binding.ts +++ b/src/element/binding.ts @@ -474,6 +474,7 @@ const maybeCalculateNewGapWhenScaling = ( return { elementId, gap: newGap, focus }; }; +// TODO: this is a bottleneck, optimise export const getEligibleElementsForBinding = ( elements: NonDeleted[], ): SuggestedBinding[] => { diff --git a/src/element/bounds.ts b/src/element/bounds.ts index a2a04439b6..c5af069745 100644 --- a/src/element/bounds.ts +++ b/src/element/bounds.ts @@ -10,10 +10,7 @@ import { distance2d, rotate, rotatePoint } from "../math"; import rough from "roughjs/bin/rough"; import { Drawable, Op } from "roughjs/bin/core"; import { Point } from "../types"; -import { - getShapeForElement, - generateRoughOptions, -} from "../renderer/renderElement"; +import { generateRoughOptions } from "../renderer/renderElement"; import { isArrowElement, isFreeDrawElement, @@ -24,6 +21,7 @@ import { rescalePoints } from "../points"; import { getBoundTextElement, getContainerElement } from "./textElement"; import { LinearElementEditor } from "./linearElementEditor"; import { Mutable } from "../utility-types"; +import { ShapeCache } from "../scene/ShapeCache"; export type RectangleBox = { x: number; @@ -621,7 +619,7 @@ const getLinearElementRotatedBounds = ( } // first element is always the curve - const cachedShape = getShapeForElement(element)?.[0]; + const cachedShape = ShapeCache.get(element)?.[0]; const shape = cachedShape ?? generateLinearElementShape(element); const ops = getCurvePathOps(shape); const transformXY = (x: number, y: number) => diff --git a/src/element/collision.ts b/src/element/collision.ts index 1878b93bb7..d04f1a0ea7 100644 --- a/src/element/collision.ts +++ b/src/element/collision.ts @@ -39,7 +39,6 @@ import { import { FrameNameBoundsCache, Point } from "../types"; import { Drawable } from "roughjs/bin/core"; import { AppState } from "../types"; -import { getShapeForElement } from "../renderer/renderElement"; import { hasBoundTextElement, isEmbeddableElement, @@ -50,6 +49,7 @@ import { isTransparent } from "../utils"; import { shouldShowBoundingBox } from "./transformHandles"; import { getBoundTextElement } from "./textElement"; import { Mutable } from "../utility-types"; +import { ShapeCache } from "../scene/ShapeCache"; const isElementDraggableFromInside = ( element: NonDeletedExcalidrawElement, @@ -489,7 +489,7 @@ const hitTestFreeDrawElement = ( B = element.points[i + 1]; } - const shape = getShapeForElement(element); + const shape = ShapeCache.get(element); // for filled freedraw shapes, support // selecting from inside @@ -502,7 +502,7 @@ const hitTestFreeDrawElement = ( const hitTestLinear = (args: HitTestArgs): boolean => { const { element, threshold } = args; - if (!getShapeForElement(element)) { + if (!ShapeCache.get(element)) { return false; } @@ -520,7 +520,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => { } const [relX, relY] = GAPoint.toTuple(point); - const shape = getShapeForElement(element as ExcalidrawLinearElement); + const shape = ShapeCache.get(element as ExcalidrawLinearElement); if (!shape) { return false; diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index 3426b3e1a8..f0dee4faad 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -25,7 +25,12 @@ import { getElementPointsCoords, getMinMaxXYFromCurvePathOps, } from "./bounds"; -import { Point, AppState, PointerCoords } from "../types"; +import { + Point, + AppState, + PointerCoords, + InteractiveCanvasAppState, +} from "../types"; import { mutateElement } from "./mutateElement"; import History from "../history"; @@ -39,9 +44,9 @@ import { tupleToCoors } from "../utils"; import { isBindingElement } from "./typeChecks"; import { shouldRotateWithDiscreteAngle } from "../keys"; import { getBoundTextElement, handleBindTextResize } from "./textElement"; -import { getShapeForElement } from "../renderer/renderElement"; import { DRAGGING_THRESHOLD } from "../constants"; import { Mutable } from "../utility-types"; +import { ShapeCache } from "../scene/ShapeCache"; const editorMidPointsCache: { version: number | null; @@ -398,7 +403,7 @@ export class LinearElementEditor { static getEditorMidPoints = ( element: NonDeleted, - appState: AppState, + appState: InteractiveCanvasAppState, ): typeof editorMidPointsCache["points"] => { const boundText = getBoundTextElement(element); @@ -422,7 +427,7 @@ export class LinearElementEditor { static updateEditorMidPointsCache = ( element: NonDeleted, - appState: AppState, + appState: InteractiveCanvasAppState, ) => { const points = LinearElementEditor.getPointsGlobalCoordinates(element); @@ -1418,7 +1423,7 @@ export class LinearElementEditor { let y1; let x2; let y2; - if (element.points.length < 2 || !getShapeForElement(element)) { + if (element.points.length < 2 || !ShapeCache.get(element)) { // XXX this is just a poor estimate and not very useful const { minX, minY, maxX, maxY } = element.points.reduce( (limits, [x, y]) => { @@ -1437,7 +1442,7 @@ export class LinearElementEditor { x2 = maxX + element.x; y2 = maxY + element.y; } else { - const shape = getShapeForElement(element)!; + const shape = ShapeCache.generateElementShape(element); // first element is always the curve const ops = getCurvePathOps(shape[0]); diff --git a/src/element/mutateElement.ts b/src/element/mutateElement.ts index 1c3d661217..0e01a080ae 100644 --- a/src/element/mutateElement.ts +++ b/src/element/mutateElement.ts @@ -1,11 +1,11 @@ import { ExcalidrawElement } from "./types"; -import { invalidateShapeForElement } from "../renderer/renderElement"; import Scene from "../scene/Scene"; import { getSizeFromPoints } from "../points"; import { randomInteger } from "../random"; import { Point } from "../types"; import { getUpdatedTimestamp } from "../utils"; import { Mutable } from "../utility-types"; +import { ShapeCache } from "../scene/ShapeCache"; type ElementUpdate = Omit< Partial, @@ -89,7 +89,7 @@ export const mutateElement = >( typeof fileId != "undefined" || typeof points !== "undefined" ) { - invalidateShapeForElement(element); + ShapeCache.delete(element); } element.version++; diff --git a/src/element/sizeHelpers.ts b/src/element/sizeHelpers.ts index b5810dbeb0..1b69ca0bc2 100644 --- a/src/element/sizeHelpers.ts +++ b/src/element/sizeHelpers.ts @@ -2,7 +2,9 @@ import { ExcalidrawElement } from "./types"; import { mutateElement } from "./mutateElement"; import { isFreeDrawElement, isLinearElement } from "./typeChecks"; import { SHIFT_LOCKING_ANGLE } from "../constants"; -import { AppState } from "../types"; +import { AppState, Zoom } from "../types"; +import { getElementBounds } from "./bounds"; +import { viewportCoordsToSceneCoords } from "../utils"; export const isInvisiblySmallElement = ( element: ExcalidrawElement, @@ -13,6 +15,42 @@ export const isInvisiblySmallElement = ( return element.width === 0 && element.height === 0; }; +export const isElementInViewport = ( + element: ExcalidrawElement, + width: number, + height: number, + viewTransformations: { + zoom: Zoom; + offsetLeft: number; + offsetTop: number; + scrollX: number; + scrollY: number; + }, +) => { + const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates + const topLeftSceneCoords = viewportCoordsToSceneCoords( + { + clientX: viewTransformations.offsetLeft, + clientY: viewTransformations.offsetTop, + }, + viewTransformations, + ); + const bottomRightSceneCoords = viewportCoordsToSceneCoords( + { + clientX: viewTransformations.offsetLeft + width, + clientY: viewTransformations.offsetTop + height, + }, + viewTransformations, + ); + + return ( + topLeftSceneCoords.x <= x2 && + topLeftSceneCoords.y <= y2 && + bottomRightSceneCoords.x >= x1 && + bottomRightSceneCoords.y >= y1 + ); +}; + /** * Makes a perfect shape or diagonal/horizontal/vertical line */ diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index c091911706..a2301b9645 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -759,7 +759,7 @@ describe("textWysiwyg", () => { expect(h.elements[1].type).toBe("text"); API.setSelectedElements([h.elements[0], h.elements[1]]); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, @@ -903,7 +903,7 @@ describe("textWysiwyg", () => { mouse.clickAt(10, 20); mouse.down(); mouse.up(); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, @@ -1154,7 +1154,7 @@ describe("textWysiwyg", () => { h.elements = [container, text]; API.setSelectedElements([container, text]); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, @@ -1168,7 +1168,7 @@ describe("textWysiwyg", () => { expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe( "Online \nwhitebo\nard \ncollabo\nration \nmade \neasy", ); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, @@ -1406,7 +1406,7 @@ describe("textWysiwyg", () => { API.setSelectedElements([textElement]); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index a5bf701434..b60fdeed2b 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -116,7 +116,7 @@ export const textWysiwyg = ({ }) => void; getViewportCoords: (x: number, y: number) => [number, number]; element: ExcalidrawTextElement; - canvas: HTMLCanvasElement | null; + canvas: HTMLCanvasElement; excalidrawContainer: HTMLDivElement | null; app: App; }) => { diff --git a/src/element/transformHandles.ts b/src/element/transformHandles.ts index cf8e5b6a2e..1d2ccdcae8 100644 --- a/src/element/transformHandles.ts +++ b/src/element/transformHandles.ts @@ -6,7 +6,7 @@ import { import { getElementAbsoluteCoords } from "./bounds"; import { rotate } from "../math"; -import { AppState, Zoom } from "../types"; +import { InteractiveCanvasAppState, Zoom } from "../types"; import { isTextElement } from "."; import { isFrameElement, isLinearElement } from "./typeChecks"; import { DEFAULT_SPACING } from "../renderer/renderScene"; @@ -276,8 +276,8 @@ export const getTransformHandles = ( }; export const shouldShowBoundingBox = ( - elements: NonDeletedExcalidrawElement[], - appState: AppState, + elements: readonly NonDeletedExcalidrawElement[], + appState: InteractiveCanvasAppState, ) => { if (appState.editingLinearElement) { return false; diff --git a/src/excalidraw-app/data/tabSync.ts b/src/excalidraw-app/data/tabSync.ts index ddb70dbb75..0617a55169 100644 --- a/src/excalidraw-app/data/tabSync.ts +++ b/src/excalidraw-app/data/tabSync.ts @@ -16,14 +16,24 @@ export const isBrowserStorageStateNewer = (type: BrowserStateTypes) => { export const updateBrowserStateVersion = (type: BrowserStateTypes) => { const timestamp = Date.now(); - localStorage.setItem(type, JSON.stringify(timestamp)); - LOCAL_STATE_VERSIONS[type] = timestamp; + try { + localStorage.setItem(type, JSON.stringify(timestamp)); + LOCAL_STATE_VERSIONS[type] = timestamp; + } catch (error) { + console.error("error while updating browser state verison", error); + } }; export const resetBrowserStateVersions = () => { - for (const key of Object.keys(LOCAL_STATE_VERSIONS) as BrowserStateTypes[]) { - const timestamp = -1; - localStorage.setItem(key, JSON.stringify(timestamp)); - LOCAL_STATE_VERSIONS[key] = timestamp; + try { + for (const key of Object.keys( + LOCAL_STATE_VERSIONS, + ) as BrowserStateTypes[]) { + const timestamp = -1; + localStorage.setItem(key, JSON.stringify(timestamp)); + LOCAL_STATE_VERSIONS[key] = timestamp; + } + } catch (error) { + console.error("error while resetting browser state verison", error); } }; diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index cd719e2765..00bcd0cb39 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -598,7 +598,7 @@ const ExcalidrawWrapper = () => { exportedElements: readonly NonDeletedExcalidrawElement[], appState: Partial, files: BinaryFiles, - canvas: HTMLCanvasElement | null, + canvas: HTMLCanvasElement, ) => { if (exportedElements.length === 0) { return window.alert(t("alerts.cannotExportEmptyCanvas")); diff --git a/src/frame.ts b/src/frame.ts index 8a39a41b3f..7f0f42d7ae 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -16,7 +16,7 @@ import { } from "./element/textElement"; import { arrayToMap, findIndex } from "./utils"; import { mutateElement } from "./element/mutateElement"; -import { AppClassProperties, AppState } from "./types"; +import { AppClassProperties, AppState, StaticCanvasAppState } from "./types"; import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { isFrameElement } from "./element"; import { moveOneRight } from "./zindex"; @@ -469,9 +469,16 @@ export const addElementsToFrame = ( } let nextElements = allElements.slice(); + // Optimisation since findIndex on "newElements" is slow + const nextElementsIndex = nextElements.reduce( + (acc: Record, element, index) => { + acc[element.id] = index; + return acc; + }, + {}, + ); const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id); - for (const element of omitGroupsContainingFrames( allElements, _elementsToAdd, @@ -485,8 +492,8 @@ export const addElementsToFrame = ( false, ); - const frameIndex = findIndex(nextElements, (e) => e.id === frame.id); - const elementIndex = findIndex(nextElements, (e) => e.id === element.id); + const frameIndex = nextElementsIndex[frame.id] ?? -1; + const elementIndex = nextElementsIndex[element.id] ?? -1; if (elementIndex < frameBoundary) { nextElements = [ @@ -648,7 +655,7 @@ export const omitGroupsContainingFrames = ( */ export const getTargetFrame = ( element: ExcalidrawElement, - appState: AppState, + appState: StaticCanvasAppState, ) => { const _element = isTextElement(element) ? getContainerElement(element) || element @@ -660,11 +667,12 @@ export const getTargetFrame = ( : getContainingFrame(_element); }; +// TODO: this a huge bottleneck for large scenes, optimise // given an element, return if the element is in some frame export const isElementInFrame = ( element: ExcalidrawElement, allElements: ExcalidrawElementsIncludingDeleted, - appState: AppState, + appState: StaticCanvasAppState, ) => { const frame = getTargetFrame(element, appState); const _element = isTextElement(element) diff --git a/src/groups.ts b/src/groups.ts index c7aa60fc24..e7b222b3c6 100644 --- a/src/groups.ts +++ b/src/groups.ts @@ -4,27 +4,40 @@ import { NonDeleted, NonDeletedExcalidrawElement, } from "./element/types"; -import { AppClassProperties, AppState } from "./types"; +import { + AppClassProperties, + AppState, + InteractiveCanvasAppState, +} from "./types"; import { getSelectedElements } from "./scene"; import { getBoundTextElement } from "./element/textElement"; import { makeNextSelectedElementIds } from "./scene/selection"; export const selectGroup = ( groupId: GroupId, - appState: AppState, + appState: InteractiveCanvasAppState, elements: readonly NonDeleted[], -): AppState => { - const elementsInGroup = elements.filter((element) => - element.groupIds.includes(groupId), +): Pick< + InteractiveCanvasAppState, + "selectedGroupIds" | "selectedElementIds" | "editingGroupId" +> => { + const elementsInGroup = elements.reduce( + (acc: Record, element) => { + if (element.groupIds.includes(groupId)) { + acc[element.id] = true; + } + return acc; + }, + {}, ); - if (elementsInGroup.length < 2) { + if (Object.keys(elementsInGroup).length < 2) { if ( appState.selectedGroupIds[groupId] || appState.editingGroupId === groupId ) { return { - ...appState, + selectedElementIds: appState.selectedElementIds, selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: false }, editingGroupId: null, }; @@ -33,104 +46,184 @@ export const selectGroup = ( } return { - ...appState, + editingGroupId: appState.editingGroupId, selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true }, selectedElementIds: { ...appState.selectedElementIds, - ...Object.fromEntries( - elementsInGroup.map((element) => [element.id, true]), - ), + ...elementsInGroup, }, }; }; +export const selectGroupsForSelectedElements = (function () { + type SelectGroupsReturnType = Pick< + InteractiveCanvasAppState, + "selectedGroupIds" | "editingGroupId" | "selectedElementIds" + >; + + let lastSelectedElements: readonly NonDeleted[] | null = + null; + let lastElements: readonly NonDeleted[] | null = null; + let lastReturnValue: SelectGroupsReturnType | null = null; + + const _selectGroups = ( + selectedElements: readonly NonDeleted[], + elements: readonly NonDeleted[], + appState: Pick, + ): SelectGroupsReturnType => { + if ( + lastReturnValue !== undefined && + elements === lastElements && + selectedElements === lastSelectedElements && + appState.editingGroupId === lastReturnValue?.editingGroupId + ) { + return lastReturnValue; + } + + const selectedGroupIds: Record = {}; + // Gather all the groups withing selected elements + for (const selectedElement of selectedElements) { + let groupIds = selectedElement.groupIds; + if (appState.editingGroupId) { + // handle the case where a group is nested within a group + const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId); + if (indexOfEditingGroup > -1) { + groupIds = groupIds.slice(0, indexOfEditingGroup); + } + } + if (groupIds.length > 0) { + const lastSelectedGroup = groupIds[groupIds.length - 1]; + selectedGroupIds[lastSelectedGroup] = true; + } + } + + // Gather all the elements within selected groups + const groupElementsIndex: Record = {}; + const selectedElementIdsInGroups = elements.reduce( + (acc: Record, element) => { + const groupId = element.groupIds.find((id) => selectedGroupIds[id]); + + if (groupId) { + acc[element.id] = true; + + // Populate the index + if (!Array.isArray(groupElementsIndex[groupId])) { + groupElementsIndex[groupId] = [element.id]; + } else { + groupElementsIndex[groupId].push(element.id); + } + } + return acc; + }, + {}, + ); + + for (const groupId of Object.keys(groupElementsIndex)) { + // If there is one element in the group, and the group is selected or it's being edited, it's not a group + if (groupElementsIndex[groupId].length < 2) { + if (selectedGroupIds[groupId]) { + selectedGroupIds[groupId] = false; + } + } + } + + lastElements = elements; + lastSelectedElements = selectedElements; + + lastReturnValue = { + editingGroupId: appState.editingGroupId, + selectedGroupIds, + selectedElementIds: { + ...appState.selectedElementIds, + ...selectedElementIdsInGroups, + }, + }; + + return lastReturnValue; + }; + + /** + * When you select an element, you often want to actually select the whole group it's in, unless + * you're currently editing that group. + */ + const selectGroupsForSelectedElements = ( + appState: Pick, + elements: readonly NonDeletedExcalidrawElement[], + prevAppState: InteractiveCanvasAppState, + /** + * supply null in cases where you don't have access to App instance and + * you don't care about optimizing selectElements retrieval + */ + app: AppClassProperties | null, + ): Pick< + InteractiveCanvasAppState, + "selectedGroupIds" | "editingGroupId" | "selectedElementIds" + > => { + const selectedElements = app + ? app.scene.getSelectedElements({ + selectedElementIds: appState.selectedElementIds, + // supplying elements explicitly in case we're passed non-state elements + elements, + }) + : getSelectedElements(elements, appState); + + if (!selectedElements.length) { + return { + selectedGroupIds: {}, + editingGroupId: null, + selectedElementIds: makeNextSelectedElementIds( + appState.selectedElementIds, + prevAppState, + ), + }; + } + + return _selectGroups(selectedElements, elements, appState); + }; + + selectGroupsForSelectedElements.clearCache = () => { + lastElements = null; + lastSelectedElements = null; + lastReturnValue = null; + }; + + return selectGroupsForSelectedElements; +})(); + /** * If the element's group is selected, don't render an individual * selection border around it. */ export const isSelectedViaGroup = ( - appState: AppState, + appState: InteractiveCanvasAppState, element: ExcalidrawElement, ) => getSelectedGroupForElement(appState, element) != null; export const getSelectedGroupForElement = ( - appState: AppState, + appState: InteractiveCanvasAppState, element: ExcalidrawElement, ) => element.groupIds .filter((groupId) => groupId !== appState.editingGroupId) .find((groupId) => appState.selectedGroupIds[groupId]); -export const getSelectedGroupIds = (appState: AppState): GroupId[] => +export const getSelectedGroupIds = ( + appState: InteractiveCanvasAppState, +): GroupId[] => Object.entries(appState.selectedGroupIds) .filter(([groupId, isSelected]) => isSelected) .map(([groupId, isSelected]) => groupId); -/** - * When you select an element, you often want to actually select the whole group it's in, unless - * you're currently editing that group. - */ -export const selectGroupsForSelectedElements = ( - appState: AppState, - elements: readonly NonDeletedExcalidrawElement[], - prevAppState: AppState, - /** - * supply null in cases where you don't have access to App instance and - * you don't care about optimizing selectElements retrieval - */ - app: AppClassProperties | null, -): AppState => { - let nextAppState: AppState = { ...appState, selectedGroupIds: {} }; - - const selectedElements = app - ? app.scene.getSelectedElements({ - selectedElementIds: appState.selectedElementIds, - // supplying elements explicitly in case we're passed non-state elements - elements, - }) - : getSelectedElements(elements, appState); - - if (!selectedElements.length) { - return { - ...nextAppState, - editingGroupId: null, - selectedElementIds: makeNextSelectedElementIds( - nextAppState.selectedElementIds, - prevAppState, - ), - }; - } - - for (const selectedElement of selectedElements) { - let groupIds = selectedElement.groupIds; - if (appState.editingGroupId) { - // handle the case where a group is nested within a group - const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId); - if (indexOfEditingGroup > -1) { - groupIds = groupIds.slice(0, indexOfEditingGroup); - } - } - if (groupIds.length > 0) { - const groupId = groupIds[groupIds.length - 1]; - nextAppState = selectGroup(groupId, nextAppState, elements); - } - } - - nextAppState.selectedElementIds = makeNextSelectedElementIds( - nextAppState.selectedElementIds, - prevAppState, - ); - - return nextAppState; -}; - // given a list of elements, return the the actual group ids that should be selected // or used to update the elements export const selectGroupsFromGivenElements = ( elements: readonly NonDeleted[], - appState: AppState, + appState: InteractiveCanvasAppState, ) => { - let nextAppState: AppState = { ...appState, selectedGroupIds: {} }; + let nextAppState: InteractiveCanvasAppState = { + ...appState, + selectedGroupIds: {}, + }; for (const element of elements) { let groupIds = element.groupIds; @@ -142,7 +235,10 @@ export const selectGroupsFromGivenElements = ( } if (groupIds.length > 0) { const groupId = groupIds[groupIds.length - 1]; - nextAppState = selectGroup(groupId, nextAppState, elements); + nextAppState = { + ...nextAppState, + ...selectGroup(groupId, nextAppState, elements), + }; } } diff --git a/src/math.ts b/src/math.ts index 01123ce81d..f549a6af16 100644 --- a/src/math.ts +++ b/src/math.ts @@ -10,9 +10,9 @@ import { ExcalidrawLinearElement, NonDeleted, } from "./element/types"; -import { getShapeForElement } from "./renderer/renderElement"; import { getCurvePathOps } from "./element/bounds"; import { Mutable } from "./utility-types"; +import { ShapeCache } from "./scene/ShapeCache"; export const rotate = ( x1: number, @@ -303,7 +303,7 @@ export const getControlPointsForBezierCurve = ( element: NonDeleted, endPoint: Point, ) => { - const shape = getShapeForElement(element as ExcalidrawLinearElement); + const shape = ShapeCache.generateElementShape(element); if (!shape) { return null; } diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index b320c81ac4..837fd62de3 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -26,7 +26,7 @@ import { Drawable, Options } from "roughjs/bin/core"; import { RoughSVG } from "roughjs/bin/svg"; import { RoughGenerator } from "roughjs/bin/generator"; -import { RenderConfig } from "../scene/types"; +import { StaticCanvasRenderConfig } from "../scene/types"; import { distance, getFontString, @@ -36,7 +36,13 @@ import { } from "../utils"; import { getCornerRadius, isPathALoop, isRightAngle } from "../math"; import rough from "roughjs/bin/rough"; -import { AppState, BinaryFiles, Zoom } from "../types"; +import { + AppState, + StaticCanvasAppState, + BinaryFiles, + Zoom, + InteractiveCanvasAppState, +} from "../types"; import { getDefaultAppState } from "../appState"; import { BOUND_TEXT_PADDING, @@ -61,6 +67,7 @@ import { } from "../element/embeddable"; import { getContainingFrame } from "../frame"; import { normalizeLink, toValidURL } from "../data/url"; +import { ShapeCache } from "../scene/ShapeCache"; // using a stronger invert (100% vs our regular 93%) and saturate // as a temp hack to make images in dark theme look closer to original @@ -72,17 +79,18 @@ const defaultAppState = getDefaultAppState(); const isPendingImageElement = ( element: ExcalidrawElement, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, ) => isInitializedImageElement(element) && !renderConfig.imageCache.has(element.fileId); const shouldResetImageFilter = ( element: ExcalidrawElement, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ) => { return ( - renderConfig.theme === "dark" && + appState.theme === "dark" && isInitializedImageElement(element) && !isPendingImageElement(element, renderConfig) && renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg @@ -99,9 +107,9 @@ const getCanvasPadding = (element: ExcalidrawElement) => export interface ExcalidrawElementWithCanvas { element: ExcalidrawElement | ExcalidrawTextElement; canvas: HTMLCanvasElement; - theme: RenderConfig["theme"]; + theme: AppState["theme"]; scale: number; - zoomValue: RenderConfig["zoom"]["value"]; + zoomValue: AppState["zoom"]["value"]; canvasOffsetX: number; canvasOffsetY: number; boundTextElementVersion: number | null; @@ -165,7 +173,8 @@ const cappedElementCanvasSize = ( const generateElementCanvas = ( element: NonDeletedExcalidrawElement, zoom: Zoom, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ): ExcalidrawElementWithCanvas => { const canvas = document.createElement("canvas"); const context = canvas.getContext("2d")!; @@ -205,17 +214,17 @@ const generateElementCanvas = ( const rc = rough.canvas(canvas); // in dark theme, revert the image color filter - if (shouldResetImageFilter(element, renderConfig)) { + if (shouldResetImageFilter(element, renderConfig, appState)) { context.filter = IMAGE_INVERT_FILTER; } - drawElementOnCanvas(element, rc, context, renderConfig); + drawElementOnCanvas(element, rc, context, renderConfig, appState); context.restore(); return { element, canvas, - theme: renderConfig.theme, + theme: appState.theme, scale, zoomValue: zoom.value, canvasOffsetX, @@ -262,11 +271,13 @@ const drawImagePlaceholder = ( size, ); }; + const drawElementOnCanvas = ( element: NonDeletedExcalidrawElement, rc: RoughCanvas, context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ) => { context.globalAlpha = ((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000; @@ -277,7 +288,7 @@ const drawElementOnCanvas = ( case "ellipse": { context.lineJoin = "round"; context.lineCap = "round"; - rc.draw(getShapeForElement(element)!); + rc.draw(ShapeCache.get(element)!); break; } case "arrow": @@ -285,7 +296,7 @@ const drawElementOnCanvas = ( context.lineJoin = "round"; context.lineCap = "round"; - getShapeForElement(element)!.forEach((shape) => { + ShapeCache.get(element)!.forEach((shape) => { rc.draw(shape); }); break; @@ -296,7 +307,7 @@ const drawElementOnCanvas = ( context.fillStyle = element.strokeColor; const path = getFreeDrawPath2D(element) as Path2D; - const fillShape = getShapeForElement(element); + const fillShape = ShapeCache.get(element); if (fillShape) { rc.draw(fillShape); @@ -321,7 +332,7 @@ const drawElementOnCanvas = ( element.height, ); } else { - drawImagePlaceholder(element, context, renderConfig.zoom.value); + drawImagePlaceholder(element, context, appState.zoom.value); } break; } @@ -378,33 +389,6 @@ const elementWithCanvasCache = new WeakMap< ExcalidrawElementWithCanvas >(); -const shapeCache = new WeakMap(); - -type ElementShape = Drawable | Drawable[] | null; - -type ElementShapes = { - freedraw: Drawable | null; - arrow: Drawable[]; - line: Drawable[]; - text: null; - image: null; -}; - -export const getShapeForElement = (element: T) => - shapeCache.get(element) as T["type"] extends keyof ElementShapes - ? ElementShapes[T["type"]] | undefined - : Drawable | null | undefined; - -export const setShapeForElement = ( - element: T, - shape: T["type"] extends keyof ElementShapes - ? ElementShapes[T["type"]] - : Drawable, -) => shapeCache.set(element, shape); - -export const invalidateShapeForElement = (element: ExcalidrawElement) => - shapeCache.delete(element); - export const generateRoughOptions = ( element: ExcalidrawElement, continuousPath = false, @@ -494,16 +478,22 @@ const modifyEmbeddableForRoughOptions = ( * @param element * @param generator */ -const generateElementShape = ( +export const generateElementShape = ( element: NonDeletedExcalidrawElement, generator: RoughGenerator, isExporting: boolean = false, -) => { - let shape = isExporting ? undefined : shapeCache.get(element); +): Drawable | Drawable[] | null => { + const cachedShape = isExporting ? undefined : ShapeCache.get(element); + + if (cachedShape) { + return cachedShape; + } // `null` indicates no rc shape applicable for this element type // (= do not generate anything) - if (shape === undefined) { + if (cachedShape === undefined) { + let shape: Drawable | Drawable[] | null = null; + elementWithCanvasCache.delete(element); switch (element.type) { @@ -539,7 +529,7 @@ const generateElementShape = ( ), ); } - setShapeForElement(element, shape); + ShapeCache.set(element, shape); break; } @@ -589,7 +579,7 @@ const generateElementShape = ( generateRoughOptions(element), ); } - setShapeForElement(element, shape); + ShapeCache.set(element, shape); break; } @@ -601,7 +591,7 @@ const generateElementShape = ( element.height, generateRoughOptions(element), ); - setShapeForElement(element, shape); + ShapeCache.set(element, shape); break; case "line": @@ -726,7 +716,7 @@ const generateElementShape = ( } } - setShapeForElement(element, shape); + ShapeCache.set(element, shape); break; } @@ -742,36 +732,39 @@ const generateElementShape = ( } else { shape = null; } - setShapeForElement(element, shape); + ShapeCache.set(element, shape); break; } case "text": case "image": { // just to ensure we don't regenerate element.canvas on rerenders - setShapeForElement(element, null); + ShapeCache.set(element, null); break; } } + return shape; } + return null; }; const generateElementWithCanvas = ( element: NonDeletedExcalidrawElement, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ) => { - const zoom: Zoom = renderConfig ? renderConfig.zoom : defaultAppState.zoom; + const zoom: Zoom = renderConfig ? appState.zoom : defaultAppState.zoom; const prevElementWithCanvas = elementWithCanvasCache.get(element); const shouldRegenerateBecauseZoom = prevElementWithCanvas && prevElementWithCanvas.zoomValue !== zoom.value && - !renderConfig?.shouldCacheIgnoreZoom; + !appState?.shouldCacheIgnoreZoom; const boundTextElementVersion = getBoundTextElement(element)?.version || null; const containingFrameOpacity = getContainingFrame(element)?.opacity || 100; if ( !prevElementWithCanvas || shouldRegenerateBecauseZoom || - prevElementWithCanvas.theme !== renderConfig.theme || + prevElementWithCanvas.theme !== appState.theme || prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion || prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity ) { @@ -779,6 +772,7 @@ const generateElementWithCanvas = ( element, zoom, renderConfig, + appState, ); elementWithCanvasCache.set(element, elementWithCanvas); @@ -790,9 +784,9 @@ const generateElementWithCanvas = ( const drawElementFromCanvas = ( elementWithCanvas: ExcalidrawElementWithCanvas, - rc: RoughCanvas, context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ) => { const element = elementWithCanvas.element; const padding = getCanvasPadding(element); @@ -807,8 +801,8 @@ const drawElementFromCanvas = ( y2 = Math.ceil(y2); } - const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio; - const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio; + const cx = ((x1 + x2) / 2 + appState.scrollX) * window.devicePixelRatio; + const cy = ((y1 + y2) / 2 + appState.scrollY) * window.devicePixelRatio; context.save(); context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio); @@ -906,9 +900,9 @@ const drawElementFromCanvas = ( context.drawImage( elementWithCanvas.canvas!, - (x1 + renderConfig.scrollX) * window.devicePixelRatio - + (x1 + appState.scrollX) * window.devicePixelRatio - (padding * elementWithCanvas.scale) / elementWithCanvas.scale, - (y1 + renderConfig.scrollY) * window.devicePixelRatio - + (y1 + appState.scrollY) * window.devicePixelRatio - (padding * elementWithCanvas.scale) / elementWithCanvas.scale, elementWithCanvas.canvas!.width / elementWithCanvas.scale, elementWithCanvas.canvas!.height / elementWithCanvas.scale, @@ -926,8 +920,8 @@ const drawElementFromCanvas = ( context.strokeStyle = "#c92a2a"; context.lineWidth = 3; context.strokeRect( - (coords.x + renderConfig.scrollX) * window.devicePixelRatio, - (coords.y + renderConfig.scrollY) * window.devicePixelRatio, + (coords.x + appState.scrollX) * window.devicePixelRatio, + (coords.y + appState.scrollY) * window.devicePixelRatio, getBoundTextMaxWidth(element) * window.devicePixelRatio, getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio, ); @@ -938,40 +932,38 @@ const drawElementFromCanvas = ( // Clear the nested element we appended to the DOM }; +export const renderSelectionElement = ( + element: NonDeletedExcalidrawElement, + context: CanvasRenderingContext2D, + appState: InteractiveCanvasAppState, +) => { + context.save(); + context.translate(element.x + appState.scrollX, element.y + appState.scrollY); + context.fillStyle = "rgba(0, 0, 200, 0.04)"; + + // render from 0.5px offset to get 1px wide line + // https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540 + // TODO can be be improved by offseting to the negative when user selects + // from right to left + const offset = 0.5 / appState.zoom.value; + + context.fillRect(offset, offset, element.width, element.height); + context.lineWidth = 1 / appState.zoom.value; + context.strokeStyle = " rgb(105, 101, 219)"; + context.strokeRect(offset, offset, element.width, element.height); + + context.restore(); +}; + export const renderElement = ( element: NonDeletedExcalidrawElement, rc: RoughCanvas, context: CanvasRenderingContext2D, - renderConfig: RenderConfig, - appState: AppState, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ) => { const generator = rc.generator; switch (element.type) { - case "selection": { - // do not render selection when exporting - if (!renderConfig.isExporting) { - context.save(); - context.translate( - element.x + renderConfig.scrollX, - element.y + renderConfig.scrollY, - ); - context.fillStyle = "rgba(0, 0, 200, 0.04)"; - - // render from 0.5px offset to get 1px wide line - // https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540 - // TODO can be be improved by offseting to the negative when user selects - // from right to left - const offset = 0.5 / renderConfig.zoom.value; - - context.fillRect(offset, offset, element.width, element.height); - context.lineWidth = 1 / renderConfig.zoom.value; - context.strokeStyle = " rgb(105, 101, 219)"; - context.strokeRect(offset, offset, element.width, element.height); - - context.restore(); - } - break; - } case "frame": { if ( !renderConfig.isExporting && @@ -980,12 +972,12 @@ export const renderElement = ( ) { context.save(); context.translate( - element.x + renderConfig.scrollX, - element.y + renderConfig.scrollY, + element.x + appState.scrollX, + element.y + appState.scrollY, ); context.fillStyle = "rgba(0, 0, 200, 0.04)"; - context.lineWidth = 2 / renderConfig.zoom.value; + context.lineWidth = 2 / appState.zoom.value; context.strokeStyle = FRAME_STYLE.strokeColor; if (FRAME_STYLE.radius && context.roundRect) { @@ -995,7 +987,7 @@ export const renderElement = ( 0, element.width, element.height, - FRAME_STYLE.radius / renderConfig.zoom.value, + FRAME_STYLE.radius / appState.zoom.value, ); context.stroke(); context.closePath(); @@ -1012,22 +1004,28 @@ export const renderElement = ( if (renderConfig.isExporting) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); - const cx = (x1 + x2) / 2 + renderConfig.scrollX; - const cy = (y1 + y2) / 2 + renderConfig.scrollY; + const cx = (x1 + x2) / 2 + appState.scrollX; + const cy = (y1 + y2) / 2 + appState.scrollY; const shiftX = (x2 - x1) / 2 - (element.x - x1); const shiftY = (y2 - y1) / 2 - (element.y - y1); context.save(); context.translate(cx, cy); context.rotate(element.angle); context.translate(-shiftX, -shiftY); - drawElementOnCanvas(element, rc, context, renderConfig); + drawElementOnCanvas(element, rc, context, renderConfig, appState); context.restore(); } else { const elementWithCanvas = generateElementWithCanvas( element, renderConfig, + appState, + ); + drawElementFromCanvas( + elementWithCanvas, + context, + renderConfig, + appState, ); - drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig); } break; @@ -1043,8 +1041,8 @@ export const renderElement = ( generateElementShape(element, generator, renderConfig.isExporting); if (renderConfig.isExporting) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); - const cx = (x1 + x2) / 2 + renderConfig.scrollX; - const cy = (y1 + y2) / 2 + renderConfig.scrollY; + const cx = (x1 + x2) / 2 + appState.scrollX; + const cy = (y1 + y2) / 2 + appState.scrollY; let shiftX = (x2 - x1) / 2 - (element.x - x1); let shiftY = (y2 - y1) / 2 - (element.y - y1); if (isTextElement(element)) { @@ -1062,7 +1060,7 @@ export const renderElement = ( context.save(); context.translate(cx, cy); - if (shouldResetImageFilter(element, renderConfig)) { + if (shouldResetImageFilter(element, renderConfig, appState)) { context.filter = "none"; } const boundTextElement = getBoundTextElement(element); @@ -1096,7 +1094,13 @@ export const renderElement = ( tempCanvasContext.translate(-shiftX, -shiftY); - drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig); + drawElementOnCanvas( + element, + tempRc, + tempCanvasContext, + renderConfig, + appState, + ); tempCanvasContext.translate(shiftX, shiftY); @@ -1133,7 +1137,7 @@ export const renderElement = ( } context.translate(-shiftX, -shiftY); - drawElementOnCanvas(element, rc, context, renderConfig); + drawElementOnCanvas(element, rc, context, renderConfig, appState); } context.restore(); @@ -1143,6 +1147,7 @@ export const renderElement = ( const elementWithCanvas = generateElementWithCanvas( element, renderConfig, + appState, ); const currentImageSmoothingStatus = context.imageSmoothingEnabled; @@ -1150,7 +1155,7 @@ export const renderElement = ( if ( // do not disable smoothing during zoom as blurry shapes look better // on low resolution (while still zooming in) than sharp ones - !renderConfig?.shouldCacheIgnoreZoom && + !appState?.shouldCacheIgnoreZoom && // angle is 0 -> always disable smoothing (!element.angle || // or check if angle is a right angle in which case we can still @@ -1167,7 +1172,12 @@ export const renderElement = ( context.imageSmoothingEnabled = false; } - drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig); + drawElementFromCanvas( + elementWithCanvas, + context, + renderConfig, + appState, + ); // reset context.imageSmoothingEnabled = currentImageSmoothingStatus; @@ -1273,7 +1283,7 @@ export const renderElementToSvg = ( generateElementShape(element, generator); const node = roughSVGDrawWithPrecision( rsvg, - getShapeForElement(element)!, + ShapeCache.get(element)!, MAX_DECIMALS_FOR_SVG_EXPORT, ); if (opacity !== 1) { @@ -1303,7 +1313,7 @@ export const renderElementToSvg = ( generateElementShape(element, generator, true); const node = roughSVGDrawWithPrecision( rsvg, - getShapeForElement(element)!, + ShapeCache.get(element)!, MAX_DECIMALS_FOR_SVG_EXPORT, ); const opacity = element.opacity / 100; @@ -1337,7 +1347,7 @@ export const renderElementToSvg = ( // render embeddable element + iframe const embeddableNode = roughSVGDrawWithPrecision( rsvg, - getShapeForElement(element)!, + ShapeCache.get(element)!, MAX_DECIMALS_FOR_SVG_EXPORT, ); embeddableNode.setAttribute("stroke-linecap", "round"); @@ -1450,7 +1460,7 @@ export const renderElementToSvg = ( } group.setAttribute("stroke-linecap", "round"); - getShapeForElement(element)!.forEach((shape) => { + ShapeCache.get(element)!.forEach((shape) => { const node = roughSVGDrawWithPrecision( rsvg, shape, @@ -1493,7 +1503,7 @@ export const renderElementToSvg = ( case "freedraw": { generateElementShape(element, generator); generateFreeDrawShape(element); - const shape = getShapeForElement(element); + const shape = ShapeCache.get(element); const node = shape ? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT) : svgRoot.ownerDocument!.createElementNS(SVG_NS, "g"); diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index f8fa79ed7a..03de49b175 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -1,8 +1,14 @@ -import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughSVG } from "roughjs/bin/svg"; import oc from "open-color"; -import { AppState, BinaryFiles, Point, Zoom } from "../types"; +import { + InteractiveCanvasAppState, + StaticCanvasAppState, + BinaryFiles, + Point, + CommonCanvasAppState, + Zoom, +} from "../types"; import { ExcalidrawElement, NonDeletedExcalidrawElement, @@ -17,20 +23,27 @@ import { OMIT_SIDES_FOR_MULTIPLE_ELEMENTS, getTransformHandlesFromCoords, getTransformHandles, - getElementBounds, getCommonBounds, } from "../element"; import { roundRect } from "./roundRect"; -import { RenderConfig } from "../scene/types"; +import { + InteractiveCanvasRenderConfig, + InteractiveSceneRenderConfig, + StaticCanvasRenderConfig, + StaticSceneRenderConfig, +} from "../scene/types"; import { getScrollBars, SCROLLBAR_COLOR, SCROLLBAR_WIDTH, } from "../scene/scrollbars"; -import { getSelectedElements } from "../scene/selection"; -import { renderElement, renderElementToSvg } from "./renderElement"; +import { + renderElement, + renderElementToSvg, + renderSelectionElement, +} from "./renderElement"; import { getClientColor } from "../clients"; import { LinearElementEditor } from "../element/linearElementEditor"; import { @@ -40,22 +53,14 @@ import { selectGroupsFromGivenElements, } from "../groups"; import { maxBindingGap } from "../element/collision"; -import { - SuggestedBinding, - SuggestedPointBinding, - isBindingEnabled, -} from "../element/binding"; +import { SuggestedBinding, SuggestedPointBinding } from "../element/binding"; import { OMIT_SIDES_FOR_FRAME, shouldShowBoundingBox, TransformHandles, TransformHandleType, } from "../element/transformHandles"; -import { - viewportCoordsToSceneCoords, - throttleRAF, - isOnlyExportingSingleFrame, -} from "../utils"; +import { throttleRAF, isOnlyExportingSingleFrame } from "../utils"; import { UserIdleState } from "../types"; import { FRAME_STYLE, THEME_FILTER } from "../constants"; import { @@ -213,7 +218,7 @@ const strokeGrid = ( const renderSingleLinearPoint = ( context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, point: Point, radius: number, isSelected: boolean, @@ -232,23 +237,22 @@ const renderSingleLinearPoint = ( context, point[0], point[1], - radius / renderConfig.zoom.value, + radius / appState.zoom.value, !isPhantomPoint, ); }; const renderLinearPointHandles = ( context: CanvasRenderingContext2D, - appState: AppState, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, element: NonDeleted, ) => { if (!appState.selectedLinearElement) { return; } context.save(); - context.translate(renderConfig.scrollX, renderConfig.scrollY); - context.lineWidth = 1 / renderConfig.zoom.value; + context.translate(appState.scrollX, appState.scrollY); + context.lineWidth = 1 / appState.zoom.value; const points = LinearElementEditor.getPointsGlobalCoordinates(element); const { POINT_HANDLE_SIZE } = LinearElementEditor; @@ -259,7 +263,7 @@ const renderLinearPointHandles = ( const isSelected = !!appState.editingLinearElement?.selectedPointsIndices?.includes(idx); - renderSingleLinearPoint(context, renderConfig, point, radius, isSelected); + renderSingleLinearPoint(context, appState, point, radius, isSelected); }); //Rendering segment mid points @@ -283,17 +287,17 @@ const renderLinearPointHandles = ( if (appState.editingLinearElement) { renderSingleLinearPoint( context, - renderConfig, + appState, segmentMidPoint, radius, false, ); - highlightPoint(segmentMidPoint, context, renderConfig); + highlightPoint(segmentMidPoint, context, appState); } else { - highlightPoint(segmentMidPoint, context, renderConfig); + highlightPoint(segmentMidPoint, context, appState); renderSingleLinearPoint( context, - renderConfig, + appState, segmentMidPoint, radius, false, @@ -302,7 +306,7 @@ const renderLinearPointHandles = ( } else if (appState.editingLinearElement || points.length === 2) { renderSingleLinearPoint( context, - renderConfig, + appState, segmentMidPoint, POINT_HANDLE_SIZE / 2, false, @@ -317,7 +321,7 @@ const renderLinearPointHandles = ( const highlightPoint = ( point: Point, context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, ) => { context.fillStyle = "rgba(105, 101, 219, 0.4)"; @@ -325,14 +329,13 @@ const highlightPoint = ( context, point[0], point[1], - LinearElementEditor.POINT_HANDLE_SIZE / renderConfig.zoom.value, + LinearElementEditor.POINT_HANDLE_SIZE / appState.zoom.value, false, ); }; const renderLinearElementPointHighlight = ( context: CanvasRenderingContext2D, - appState: AppState, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, ) => { const { elementId, hoverPointIndex } = appState.selectedLinearElement!; if ( @@ -351,21 +354,19 @@ const renderLinearElementPointHighlight = ( hoverPointIndex, ); context.save(); - context.translate(renderConfig.scrollX, renderConfig.scrollY); + context.translate(appState.scrollX, appState.scrollY); - highlightPoint(point, context, renderConfig); + highlightPoint(point, context, appState); context.restore(); }; const frameClip = ( frame: ExcalidrawFrameElement, context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + renderConfig: StaticCanvasRenderConfig, + appState: StaticCanvasAppState, ) => { - context.translate( - frame.x + renderConfig.scrollX, - frame.y + renderConfig.scrollY, - ); + context.translate(frame.x + appState.scrollX, frame.y + appState.scrollY); context.beginPath(); if (context.roundRect && !renderConfig.isExporting) { context.roundRect( @@ -373,332 +374,219 @@ const frameClip = ( 0, frame.width, frame.height, - FRAME_STYLE.radius / renderConfig.zoom.value, + FRAME_STYLE.radius / appState.zoom.value, ); } else { context.rect(0, 0, frame.width, frame.height); } context.clip(); context.translate( - -(frame.x + renderConfig.scrollX), - -(frame.y + renderConfig.scrollY), + -(frame.x + appState.scrollX), + -(frame.y + appState.scrollY), ); }; -export const _renderScene = ({ - elements, - appState, - scale, - rc, +const getNormalizedCanvasDimensions = ( + canvas: HTMLCanvasElement, + scale: number, +): [number, number] => { + // When doing calculations based on canvas width we should used normalized one + return [canvas.width / scale, canvas.height / scale]; +}; + +const bootstrapCanvas = ({ canvas, - renderConfig, + scale, + normalizedWidth, + normalizedHeight, + theme, + isExporting, + viewBackgroundColor, }: { - elements: readonly NonDeletedExcalidrawElement[]; - appState: AppState; - scale: number; - rc: RoughCanvas; canvas: HTMLCanvasElement; - renderConfig: RenderConfig; -}) => - // extra options passed to the renderer - { - if (canvas === null) { - return { atLeastOneVisibleElement: false }; + scale: number; + normalizedWidth: number; + normalizedHeight: number; + theme?: CommonCanvasAppState["theme"]; + isExporting?: StaticCanvasRenderConfig["isExporting"]; + viewBackgroundColor?: StaticCanvasAppState["viewBackgroundColor"]; +}): CanvasRenderingContext2D => { + const context = canvas.getContext("2d")!; + + context.setTransform(1, 0, 0, 1, 0, 0); + context.scale(scale, scale); + + if (isExporting && theme === "dark") { + context.filter = THEME_FILTER; + } + + // Paint background + if (typeof viewBackgroundColor === "string") { + const hasTransparence = + viewBackgroundColor === "transparent" || + viewBackgroundColor.length === 5 || // #RGBA + viewBackgroundColor.length === 9 || // #RRGGBBA + /(hsla|rgba)\(/.test(viewBackgroundColor); + if (hasTransparence) { + context.clearRect(0, 0, normalizedWidth, normalizedHeight); } - const { - renderScrollbars = false, - renderSelection = true, - renderGrid = true, - isExporting, - } = renderConfig; - - const selectionColor = renderConfig.selectionColor || oc.black; - - const context = canvas.getContext("2d")!; - - context.setTransform(1, 0, 0, 1, 0, 0); context.save(); - context.scale(scale, scale); - // When doing calculations based on canvas width we should used normalized one - const normalizedCanvasWidth = canvas.width / scale; - const normalizedCanvasHeight = canvas.height / scale; + context.fillStyle = viewBackgroundColor; + context.fillRect(0, 0, normalizedWidth, normalizedHeight); + context.restore(); + } else { + context.clearRect(0, 0, normalizedWidth, normalizedHeight); + } - if (isExporting && renderConfig.theme === "dark") { - context.filter = THEME_FILTER; - } + return context; +}; - // Paint background - if (typeof renderConfig.viewBackgroundColor === "string") { - const hasTransparence = - renderConfig.viewBackgroundColor === "transparent" || - renderConfig.viewBackgroundColor.length === 5 || // #RGBA - renderConfig.viewBackgroundColor.length === 9 || // #RRGGBBA - /(hsla|rgba)\(/.test(renderConfig.viewBackgroundColor); - if (hasTransparence) { - context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight); - } - context.save(); - context.fillStyle = renderConfig.viewBackgroundColor; - context.fillRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight); - context.restore(); - } else { - context.clearRect(0, 0, normalizedCanvasWidth, normalizedCanvasHeight); - } +const _renderInteractiveScene = ({ + canvas, + elements, + visibleElements, + selectedElements, + scale, + appState, + renderConfig, +}: InteractiveSceneRenderConfig) => { + if (canvas === null) { + return { atLeastOneVisibleElement: false, elements }; + } - // Apply zoom - context.save(); - context.scale(renderConfig.zoom.value, renderConfig.zoom.value); + const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( + canvas, + scale, + ); - // Grid - if (renderGrid && appState.gridSize) { - strokeGrid( - context, - appState.gridSize, - renderConfig.scrollX, - renderConfig.scrollY, - renderConfig.zoom, - normalizedCanvasWidth / renderConfig.zoom.value, - normalizedCanvasHeight / renderConfig.zoom.value, - ); - } + const context = bootstrapCanvas({ + canvas, + scale, + normalizedWidth, + normalizedHeight, + }); - // Paint visible elements - const visibleElements = elements.filter((element) => - isVisibleElement(element, normalizedCanvasWidth, normalizedCanvasHeight, { - zoom: renderConfig.zoom, - offsetLeft: appState.offsetLeft, - offsetTop: appState.offsetTop, - scrollX: renderConfig.scrollX, - scrollY: renderConfig.scrollY, - }), - ); + // Apply zoom + context.save(); + context.scale(appState.zoom.value, appState.zoom.value); - const groupsToBeAddedToFrame = new Set(); - - visibleElements.forEach((element) => { - if ( - element.groupIds.length > 0 && - appState.frameToHighlight && - appState.selectedElementIds[element.id] && - (elementOverlapsWithFrame(element, appState.frameToHighlight) || - element.groupIds.find((groupId) => - groupsToBeAddedToFrame.has(groupId), - )) - ) { - element.groupIds.forEach((groupId) => - groupsToBeAddedToFrame.add(groupId), - ); - } - }); - - let editingLinearElement: NonDeleted | undefined = - undefined; - - visibleElements - .filter((el) => !isEmbeddableOrFrameLabel(el)) - .forEach((element) => { - try { - // - when exporting the whole canvas, we DO NOT apply clipping - // - when we are exporting a particular frame, apply clipping - // if the containing frame is not selected, apply clipping - const frameId = element.frameId || appState.frameToHighlight?.id; - - if ( - frameId && - ((renderConfig.isExporting && - isOnlyExportingSingleFrame(elements)) || - (!renderConfig.isExporting && - appState.frameRendering.enabled && - appState.frameRendering.clip)) - ) { - context.save(); - - const frame = getTargetFrame(element, appState); - - if (frame && isElementInFrame(element, elements, appState)) { - frameClip(frame, context, renderConfig); - } - renderElement(element, rc, context, renderConfig, appState); - context.restore(); - } else { - renderElement(element, rc, context, renderConfig, appState); - } - // Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to - // ShapeCache returns empty hence making sure that we get the - // correct element from visible elements - if (appState.editingLinearElement?.elementId === element.id) { - if (element) { - editingLinearElement = - element as NonDeleted; - } - } - if (!isExporting) { - renderLinkIcon(element, context, appState); - } - } catch (error: any) { - console.error(error); - } - }); - - // render embeddables on top - visibleElements - .filter((el) => isEmbeddableOrFrameLabel(el)) - .forEach((element) => { - try { - const render = () => { - renderElement(element, rc, context, renderConfig, appState); - - if ( - isEmbeddableElement(element) && - (isExporting || !element.validated) && - element.width && - element.height - ) { - const label = createPlaceholderEmbeddableLabel(element); - renderElement(label, rc, context, renderConfig, appState); - } - if (!isExporting) { - renderLinkIcon(element, context, appState); - } - }; - // - when exporting the whole canvas, we DO NOT apply clipping - // - when we are exporting a particular frame, apply clipping - // if the containing frame is not selected, apply clipping - const frameId = element.frameId || appState.frameToHighlight?.id; - - if ( - frameId && - ((renderConfig.isExporting && - isOnlyExportingSingleFrame(elements)) || - (!renderConfig.isExporting && - appState.frameRendering.enabled && - appState.frameRendering.clip)) - ) { - context.save(); - - const frame = getTargetFrame(element, appState); - - if (frame && isElementInFrame(element, elements, appState)) { - frameClip(frame, context, renderConfig); - } - render(); - context.restore(); - } else { - render(); - } - } catch (error: any) { - console.error(error); - } - }); - - if (editingLinearElement) { - renderLinearPointHandles( - context, - appState, - renderConfig, - editingLinearElement, - ); - } - - // Paint selection element - if (appState.selectionElement) { - try { - renderElement( - appState.selectionElement, - rc, - context, - renderConfig, - appState, - ); - } catch (error: any) { - console.error(error); - } - } - - if (isBindingEnabled(appState)) { - appState.suggestedBindings - .filter((binding) => binding != null) - .forEach((suggestedBinding) => { - renderBindingHighlight(context, renderConfig, suggestedBinding!); - }); - } - - if (appState.frameToHighlight) { - renderFrameHighlight(context, renderConfig, appState.frameToHighlight); - } - - if (appState.elementsToHighlight) { - renderElementsBoxHighlight( - context, - renderConfig, - appState.elementsToHighlight, - appState, - ); - } - - const locallySelectedElements = getSelectedElements(elements, appState); - const isFrameSelected = locallySelectedElements.some((element) => - isFrameElement(element), - ); + let editingLinearElement: NonDeleted | undefined = + undefined; + visibleElements.forEach((element) => { // Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to // ShapeCache returns empty hence making sure that we get the // correct element from visible elements + if (appState.editingLinearElement?.elementId === element.id) { + if (element) { + editingLinearElement = element as NonDeleted; + } + } + }); + + if (editingLinearElement) { + renderLinearPointHandles(context, appState, editingLinearElement); + } + + // Paint selection element + if (appState.selectionElement) { + try { + renderSelectionElement(appState.selectionElement, context, appState); + } catch (error: any) { + console.error(error); + } + } + + if (appState.isBindingEnabled) { + appState.suggestedBindings + .filter((binding) => binding != null) + .forEach((suggestedBinding) => { + renderBindingHighlight(context, appState, suggestedBinding!); + }); + } + + if (appState.frameToHighlight) { + renderFrameHighlight(context, appState, appState.frameToHighlight); + } + + if (appState.elementsToHighlight) { + renderElementsBoxHighlight(context, appState, appState.elementsToHighlight); + } + + const isFrameSelected = selectedElements.some((element) => + isFrameElement(element), + ); + + // Getting the element using LinearElementEditor during collab mismatches version - being one head of visible elements due to + // ShapeCache returns empty hence making sure that we get the + // correct element from visible elements + if ( + selectedElements.length === 1 && + appState.editingLinearElement?.elementId === selectedElements[0].id + ) { + renderLinearPointHandles( + context, + appState, + selectedElements[0] as NonDeleted, + ); + } + + if ( + appState.selectedLinearElement && + appState.selectedLinearElement.hoverPointIndex >= 0 + ) { + renderLinearElementPointHighlight(context, appState); + } + // Paint selected elements + if (!appState.multiElement && !appState.editingLinearElement) { + const showBoundingBox = shouldShowBoundingBox(selectedElements, appState); + + const isSingleLinearElementSelected = + selectedElements.length === 1 && isLinearElement(selectedElements[0]); + // render selected linear element points if ( - locallySelectedElements.length === 1 && - appState.editingLinearElement?.elementId === locallySelectedElements[0].id + isSingleLinearElementSelected && + appState.selectedLinearElement?.elementId === selectedElements[0].id && + !selectedElements[0].locked ) { renderLinearPointHandles( context, appState, - renderConfig, - locallySelectedElements[0] as NonDeleted, + selectedElements[0] as ExcalidrawLinearElement, ); } + const selectionColor = renderConfig.selectionColor || oc.black; - if ( - appState.selectedLinearElement && - appState.selectedLinearElement.hoverPointIndex >= 0 - ) { - renderLinearElementPointHighlight(context, appState, renderConfig); - } - // Paint selected elements - if ( - renderSelection && - !appState.multiElement && - !appState.editingLinearElement - ) { - const showBoundingBox = shouldShowBoundingBox( - locallySelectedElements, - appState, + if (showBoundingBox) { + // Optimisation for finding quickly relevant element ids + const locallySelectedIds = selectedElements.reduce( + (acc: Record, element) => { + acc[element.id] = true; + return acc; + }, + {}, ); - const locallySelectedIds = locallySelectedElements.map( - (element) => element.id, - ); - const isSingleLinearElementSelected = - locallySelectedElements.length === 1 && - isLinearElement(locallySelectedElements[0]); - // render selected linear element points - if ( - isSingleLinearElementSelected && - appState.selectedLinearElement?.elementId === - locallySelectedElements[0].id && - !locallySelectedElements[0].locked - ) { - renderLinearPointHandles( - context, - appState, - renderConfig, - locallySelectedElements[0] as ExcalidrawLinearElement, - ); - } - if (showBoundingBox) { - const selections = elements.reduce((acc, element) => { + const selections = elements.reduce( + ( + acc: { + angle: number; + elementX1: number; + elementY1: number; + elementX2: number; + elementY2: number; + selectionColors: string[]; + dashed?: boolean; + cx: number; + cy: number; + activeEmbeddable: boolean; + }[], + element, + ) => { const selectionColors = []; // local user if ( - locallySelectedIds.includes(element.id) && + locallySelectedIds[element.id] && !isSelectedViaGroup(appState, element) ) { selectionColors.push(selectionColor); @@ -707,7 +595,7 @@ export const _renderScene = ({ if (renderConfig.remoteSelectedElementIds[element.id]) { selectionColors.push( ...renderConfig.remoteSelectedElementIds[element.id].map( - (socketId) => { + (socketId: string) => { const background = getClientColor(socketId); return background; }, @@ -734,312 +622,494 @@ export const _renderScene = ({ }); } return acc; - }, [] as { angle: number; elementX1: number; elementY1: number; elementX2: number; elementY2: number; selectionColors: string[]; dashed?: boolean; cx: number; cy: number; activeEmbeddable: boolean }[]); + }, + [], + ); - const addSelectionForGroupId = (groupId: GroupId) => { - const groupElements = getElementsInGroup(elements, groupId); - const [elementX1, elementY1, elementX2, elementY2] = - getCommonBounds(groupElements); - selections.push({ - angle: 0, - elementX1, - elementX2, - elementY1, - elementY2, - selectionColors: [oc.black], - dashed: true, - cx: elementX1 + (elementX2 - elementX1) / 2, - cy: elementY1 + (elementY2 - elementY1) / 2, - activeEmbeddable: false, - }); - }; + const addSelectionForGroupId = (groupId: GroupId) => { + const groupElements = getElementsInGroup(elements, groupId); + const [elementX1, elementY1, elementX2, elementY2] = + getCommonBounds(groupElements); + selections.push({ + angle: 0, + elementX1, + elementX2, + elementY1, + elementY2, + selectionColors: [oc.black], + dashed: true, + cx: elementX1 + (elementX2 - elementX1) / 2, + cy: elementY1 + (elementY2 - elementY1) / 2, + activeEmbeddable: false, + }); + }; - for (const groupId of getSelectedGroupIds(appState)) { - // TODO: support multiplayer selected group IDs - addSelectionForGroupId(groupId); - } - - if (appState.editingGroupId) { - addSelectionForGroupId(appState.editingGroupId); - } - - selections.forEach((selection) => - renderSelectionBorder(context, renderConfig, selection), - ); + for (const groupId of getSelectedGroupIds(appState)) { + // TODO: support multiplayer selected group IDs + addSelectionForGroupId(groupId); } - // Paint resize transformHandles - context.save(); - context.translate(renderConfig.scrollX, renderConfig.scrollY); - if (locallySelectedElements.length === 1) { - context.fillStyle = oc.white; - const transformHandles = getTransformHandles( - locallySelectedElements[0], - renderConfig.zoom, - "mouse", // when we render we don't know which pointer type so use mouse - ); - if (!appState.viewModeEnabled && showBoundingBox) { - renderTransformHandles( - context, - renderConfig, - transformHandles, - locallySelectedElements[0].angle, - ); - } - } else if (locallySelectedElements.length > 1 && !appState.isRotating) { - const dashedLinePadding = - (DEFAULT_SPACING * 2) / renderConfig.zoom.value; - context.fillStyle = oc.white; - const [x1, y1, x2, y2] = getCommonBounds(locallySelectedElements); - const initialLineDash = context.getLineDash(); - context.setLineDash([2 / renderConfig.zoom.value]); - const lineWidth = context.lineWidth; - context.lineWidth = 1 / renderConfig.zoom.value; - context.strokeStyle = selectionColor; - strokeRectWithRotation( + if (appState.editingGroupId) { + addSelectionForGroupId(appState.editingGroupId); + } + + selections.forEach((selection) => + renderSelectionBorder(context, appState, selection), + ); + } + // Paint resize transformHandles + context.save(); + context.translate(appState.scrollX, appState.scrollY); + + if (selectedElements.length === 1) { + context.fillStyle = oc.white; + const transformHandles = getTransformHandles( + selectedElements[0], + appState.zoom, + "mouse", // when we render we don't know which pointer type so use mouse + ); + if (!appState.viewModeEnabled && showBoundingBox) { + renderTransformHandles( context, - x1 - dashedLinePadding, - y1 - dashedLinePadding, - x2 - x1 + dashedLinePadding * 2, - y2 - y1 + dashedLinePadding * 2, - (x1 + x2) / 2, - (y1 + y2) / 2, - 0, + renderConfig, + appState, + transformHandles, + selectedElements[0].angle, ); - context.lineWidth = lineWidth; - context.setLineDash(initialLineDash); - const transformHandles = getTransformHandlesFromCoords( - [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2], - 0, - renderConfig.zoom, - "mouse", - isFrameSelected - ? OMIT_SIDES_FOR_FRAME - : OMIT_SIDES_FOR_MULTIPLE_ELEMENTS, - ); - if (locallySelectedElements.some((element) => !element.locked)) { - renderTransformHandles(context, renderConfig, transformHandles, 0); - } } - context.restore(); + } else if (selectedElements.length > 1 && !appState.isRotating) { + const dashedLinePadding = (DEFAULT_SPACING * 2) / appState.zoom.value; + context.fillStyle = oc.white; + const [x1, y1, x2, y2] = getCommonBounds(selectedElements); + const initialLineDash = context.getLineDash(); + context.setLineDash([2 / appState.zoom.value]); + const lineWidth = context.lineWidth; + context.lineWidth = 1 / appState.zoom.value; + context.strokeStyle = selectionColor; + strokeRectWithRotation( + context, + x1 - dashedLinePadding, + y1 - dashedLinePadding, + x2 - x1 + dashedLinePadding * 2, + y2 - y1 + dashedLinePadding * 2, + (x1 + x2) / 2, + (y1 + y2) / 2, + 0, + ); + context.lineWidth = lineWidth; + context.setLineDash(initialLineDash); + const transformHandles = getTransformHandlesFromCoords( + [x1, y1, x2, y2, (x1 + x2) / 2, (y1 + y2) / 2], + 0, + appState.zoom, + "mouse", + isFrameSelected + ? OMIT_SIDES_FOR_FRAME + : OMIT_SIDES_FOR_MULTIPLE_ELEMENTS, + ); + if (selectedElements.some((element) => !element.locked)) { + renderTransformHandles( + context, + renderConfig, + appState, + transformHandles, + 0, + ); + } + } + context.restore(); + } + + // Reset zoom + context.restore(); + + // Paint remote pointers + for (const clientId in renderConfig.remotePointerViewportCoords) { + let { x, y } = renderConfig.remotePointerViewportCoords[clientId]; + + x -= appState.offsetLeft; + y -= appState.offsetTop; + + const width = 11; + const height = 14; + + const isOutOfBounds = + x < 0 || + x > normalizedWidth - width || + y < 0 || + y > normalizedHeight - height; + + x = Math.max(x, 0); + x = Math.min(x, normalizedWidth - width); + y = Math.max(y, 0); + y = Math.min(y, normalizedHeight - height); + + const background = getClientColor(clientId); + + context.save(); + context.strokeStyle = background; + context.fillStyle = background; + + const userState = renderConfig.remotePointerUserStates[clientId]; + const isInactive = + isOutOfBounds || + userState === UserIdleState.IDLE || + userState === UserIdleState.AWAY; + + if (isInactive) { + context.globalAlpha = 0.3; } - // Reset zoom - context.restore(); - - // Paint remote pointers - for (const clientId in renderConfig.remotePointerViewportCoords) { - let { x, y } = renderConfig.remotePointerViewportCoords[clientId]; - - x -= appState.offsetLeft; - y -= appState.offsetTop; - - const width = 11; - const height = 14; - - const isOutOfBounds = - x < 0 || - x > normalizedCanvasWidth - width || - y < 0 || - y > normalizedCanvasHeight - height; - - x = Math.max(x, 0); - x = Math.min(x, normalizedCanvasWidth - width); - y = Math.max(y, 0); - y = Math.min(y, normalizedCanvasHeight - height); - - const background = getClientColor(clientId); - - context.save(); - context.strokeStyle = background; - context.fillStyle = background; - - const userState = renderConfig.remotePointerUserStates[clientId]; - const isInactive = - isOutOfBounds || - userState === UserIdleState.IDLE || - userState === UserIdleState.AWAY; - - if (isInactive) { - context.globalAlpha = 0.3; - } - - if ( - renderConfig.remotePointerButton && - renderConfig.remotePointerButton[clientId] === "down" - ) { - context.beginPath(); - context.arc(x, y, 15, 0, 2 * Math.PI, false); - context.lineWidth = 3; - context.strokeStyle = "#ffffff88"; - context.stroke(); - context.closePath(); - - context.beginPath(); - context.arc(x, y, 15, 0, 2 * Math.PI, false); - context.lineWidth = 1; - context.strokeStyle = background; - context.stroke(); - context.closePath(); - } - - // Background (white outline) for arrow - context.fillStyle = oc.white; - context.strokeStyle = oc.white; - context.lineWidth = 6; - context.lineJoin = "round"; + if ( + renderConfig.remotePointerButton && + renderConfig.remotePointerButton[clientId] === "down" + ) { context.beginPath(); + context.arc(x, y, 15, 0, 2 * Math.PI, false); + context.lineWidth = 3; + context.strokeStyle = "#ffffff88"; + context.stroke(); + context.closePath(); + + context.beginPath(); + context.arc(x, y, 15, 0, 2 * Math.PI, false); + context.lineWidth = 1; + context.strokeStyle = background; + context.stroke(); + context.closePath(); + } + + // Background (white outline) for arrow + context.fillStyle = oc.white; + context.strokeStyle = oc.white; + context.lineWidth = 6; + context.lineJoin = "round"; + context.beginPath(); + context.moveTo(x, y); + context.lineTo(x + 0, y + 14); + context.lineTo(x + 4, y + 9); + context.lineTo(x + 11, y + 8); + context.closePath(); + context.stroke(); + context.fill(); + + // Arrow + context.fillStyle = background; + context.strokeStyle = background; + context.lineWidth = 2; + context.lineJoin = "round"; + context.beginPath(); + if (isInactive) { + context.moveTo(x - 1, y - 1); + context.lineTo(x - 1, y + 15); + context.lineTo(x + 5, y + 10); + context.lineTo(x + 12, y + 9); + context.closePath(); + context.fill(); + } else { context.moveTo(x, y); context.lineTo(x + 0, y + 14); context.lineTo(x + 4, y + 9); context.lineTo(x + 11, y + 8); context.closePath(); - context.stroke(); context.fill(); - - // Arrow - context.fillStyle = background; - context.strokeStyle = background; - context.lineWidth = 2; - context.lineJoin = "round"; - context.beginPath(); - if (isInactive) { - context.moveTo(x - 1, y - 1); - context.lineTo(x - 1, y + 15); - context.lineTo(x + 5, y + 10); - context.lineTo(x + 12, y + 9); - context.closePath(); - context.fill(); - } else { - context.moveTo(x, y); - context.lineTo(x + 0, y + 14); - context.lineTo(x + 4, y + 9); - context.lineTo(x + 11, y + 8); - context.closePath(); - context.fill(); - context.stroke(); - } - - const username = renderConfig.remotePointerUsernames[clientId] || ""; - - if (!isOutOfBounds && username) { - context.font = "600 12px sans-serif"; // font has to be set before context.measureText() - - const offsetX = x + width / 2; - const offsetY = y + height + 2; - const paddingHorizontal = 5; - const paddingVertical = 3; - const measure = context.measureText(username); - const measureHeight = - measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent; - const finalHeight = Math.max(measureHeight, 12); - - const boxX = offsetX - 1; - const boxY = offsetY - 1; - const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2; - const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2; - if (context.roundRect) { - context.beginPath(); - context.roundRect(boxX, boxY, boxWidth, boxHeight, 8); - context.fillStyle = background; - context.fill(); - context.strokeStyle = oc.white; - context.stroke(); - } else { - roundRect(context, boxX, boxY, boxWidth, boxHeight, 8, oc.white); - } - context.fillStyle = oc.black; - - context.fillText( - username, - offsetX + paddingHorizontal + 1, - offsetY + - paddingVertical + - measure.actualBoundingBoxAscent + - Math.floor((finalHeight - measureHeight) / 2) + - 2, - ); - } - - context.restore(); - context.closePath(); + context.stroke(); } - // Paint scrollbars - let scrollBars; - if (renderScrollbars) { - scrollBars = getScrollBars( - elements, - normalizedCanvasWidth, - normalizedCanvasHeight, - renderConfig, - ); + const username = renderConfig.remotePointerUsernames[clientId] || ""; - context.save(); - context.fillStyle = SCROLLBAR_COLOR; - context.strokeStyle = "rgba(255,255,255,0.8)"; - [scrollBars.horizontal, scrollBars.vertical].forEach((scrollBar) => { - if (scrollBar) { - roundRect( - context, - scrollBar.x, - scrollBar.y, - scrollBar.width, - scrollBar.height, - SCROLLBAR_WIDTH / 2, - ); - } - }); - context.restore(); + if (!isOutOfBounds && username) { + context.font = "600 12px sans-serif"; // font has to be set before context.measureText() + + const offsetX = x + width / 2; + const offsetY = y + height + 2; + const paddingHorizontal = 5; + const paddingVertical = 3; + const measure = context.measureText(username); + const measureHeight = + measure.actualBoundingBoxDescent + measure.actualBoundingBoxAscent; + const finalHeight = Math.max(measureHeight, 12); + + const boxX = offsetX - 1; + const boxY = offsetY - 1; + const boxWidth = measure.width + 2 + paddingHorizontal * 2 + 2; + const boxHeight = finalHeight + 2 + paddingVertical * 2 + 2; + if (context.roundRect) { + context.beginPath(); + context.roundRect(boxX, boxY, boxWidth, boxHeight, 8); + context.fillStyle = background; + context.fill(); + context.strokeStyle = oc.white; + context.stroke(); + } else { + roundRect(context, boxX, boxY, boxWidth, boxHeight, 8, oc.white); + } + context.fillStyle = oc.black; + + context.fillText( + username, + offsetX + paddingHorizontal + 1, + offsetY + + paddingVertical + + measure.actualBoundingBoxAscent + + Math.floor((finalHeight - measureHeight) / 2) + + 2, + ); } context.restore(); - return { atLeastOneVisibleElement: visibleElements.length > 0, scrollBars }; - }; + context.closePath(); + } -const renderSceneThrottled = throttleRAF( - (config: { - elements: readonly NonDeletedExcalidrawElement[]; - appState: AppState; - scale: number; - rc: RoughCanvas; - canvas: HTMLCanvasElement; - renderConfig: RenderConfig; - callback?: (data: ReturnType) => void; - }) => { - const ret = _renderScene(config); + // Paint scrollbars + let scrollBars; + if (renderConfig.renderScrollbars) { + scrollBars = getScrollBars( + elements, + normalizedWidth, + normalizedHeight, + appState, + ); + + context.save(); + context.fillStyle = SCROLLBAR_COLOR; + context.strokeStyle = "rgba(255,255,255,0.8)"; + [scrollBars.horizontal, scrollBars.vertical].forEach((scrollBar) => { + if (scrollBar) { + roundRect( + context, + scrollBar.x, + scrollBar.y, + scrollBar.width, + scrollBar.height, + SCROLLBAR_WIDTH / 2, + ); + } + }); + context.restore(); + } + + return { + scrollBars, + atLeastOneVisibleElement: visibleElements.length > 0, + elements, + }; +}; + +const _renderStaticScene = ({ + canvas, + rc, + elements, + visibleElements, + scale, + appState, + renderConfig, +}: StaticSceneRenderConfig) => { + if (canvas === null) { + return; + } + + const { renderGrid = true, isExporting } = renderConfig; + + const [normalizedWidth, normalizedHeight] = getNormalizedCanvasDimensions( + canvas, + scale, + ); + + const context = bootstrapCanvas({ + canvas, + scale, + normalizedWidth, + normalizedHeight, + theme: appState.theme, + isExporting, + viewBackgroundColor: appState.viewBackgroundColor, + }); + + // Apply zoom + context.scale(appState.zoom.value, appState.zoom.value); + + // Grid + if (renderGrid && appState.gridSize) { + strokeGrid( + context, + appState.gridSize, + -Math.ceil(appState.zoom.value / appState.gridSize) * appState.gridSize + + (appState.scrollX % appState.gridSize), + -Math.ceil(appState.zoom.value / appState.gridSize) * appState.gridSize + + (appState.scrollY % appState.gridSize), + appState.zoom, + normalizedWidth / appState.zoom.value, + normalizedHeight / appState.zoom.value, + ); + } + + const groupsToBeAddedToFrame = new Set(); + + visibleElements.forEach((element) => { + if ( + element.groupIds.length > 0 && + appState.frameToHighlight && + appState.selectedElementIds[element.id] && + (elementOverlapsWithFrame(element, appState.frameToHighlight) || + element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId))) + ) { + element.groupIds.forEach((groupId) => + groupsToBeAddedToFrame.add(groupId), + ); + } + }); + + // Paint visible elements + visibleElements + .filter((el) => !isEmbeddableOrFrameLabel(el)) + .forEach((element) => { + try { + // - when exporting the whole canvas, we DO NOT apply clipping + // - when we are exporting a particular frame, apply clipping + // if the containing frame is not selected, apply clipping + const frameId = element.frameId || appState.frameToHighlight?.id; + + if ( + frameId && + ((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) || + (!renderConfig.isExporting && + appState.frameRendering.enabled && + appState.frameRendering.clip)) + ) { + context.save(); + + const frame = getTargetFrame(element, appState); + + // TODO do we need to check isElementInFrame here? + if (frame && isElementInFrame(element, elements, appState)) { + frameClip(frame, context, renderConfig, appState); + } + renderElement(element, rc, context, renderConfig, appState); + context.restore(); + } else { + renderElement(element, rc, context, renderConfig, appState); + } + if (!isExporting) { + renderLinkIcon(element, context, appState); + } + } catch (error: any) { + console.error(error); + } + }); + + // render embeddables on top + visibleElements + .filter((el) => isEmbeddableOrFrameLabel(el)) + .forEach((element) => { + try { + const render = () => { + renderElement(element, rc, context, renderConfig, appState); + + if ( + isEmbeddableElement(element) && + (isExporting || !element.validated) && + element.width && + element.height + ) { + const label = createPlaceholderEmbeddableLabel(element); + renderElement(label, rc, context, renderConfig, appState); + } + if (!isExporting) { + renderLinkIcon(element, context, appState); + } + }; + // - when exporting the whole canvas, we DO NOT apply clipping + // - when we are exporting a particular frame, apply clipping + // if the containing frame is not selected, apply clipping + const frameId = element.frameId || appState.frameToHighlight?.id; + + if ( + frameId && + ((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) || + (!renderConfig.isExporting && + appState.frameRendering.enabled && + appState.frameRendering.clip)) + ) { + context.save(); + + const frame = getTargetFrame(element, appState); + + if (frame && isElementInFrame(element, elements, appState)) { + frameClip(frame, context, renderConfig, appState); + } + render(); + context.restore(); + } else { + render(); + } + } catch (error: any) { + console.error(error); + } + }); +}; + +/** throttled to animation framerate */ +const renderInteractiveSceneThrottled = throttleRAF( + (config: InteractiveSceneRenderConfig) => { + const ret = _renderInteractiveScene(config); config.callback?.(ret); }, { trailing: true }, ); -/** renderScene throttled to animation framerate */ -export const renderScene = ( - config: { - elements: readonly NonDeletedExcalidrawElement[]; - appState: AppState; - scale: number; - rc: RoughCanvas; - canvas: HTMLCanvasElement; - renderConfig: RenderConfig; - callback?: (data: ReturnType) => void; - }, - /** Whether to throttle rendering. Defaults to false. - * When throttling, no value is returned. Use the callback instead. */ +/** + * Interactive scene is the ui-canvas where we render boundinb boxes, selections + * and other ui stuff. + */ +export const renderInteractiveScene = < + U extends typeof _renderInteractiveScene, + T extends boolean = false, +>( + renderConfig: InteractiveSceneRenderConfig, throttle?: T, -): T extends true ? void : ReturnType => { +): T extends true ? void : ReturnType => { if (throttle) { - renderSceneThrottled(config); - return undefined as T extends true ? void : ReturnType; + renderInteractiveSceneThrottled(renderConfig); + return undefined as T extends true ? void : ReturnType; } - const ret = _renderScene(config); - config.callback?.(ret); - return ret as T extends true ? void : ReturnType; + const ret = _renderInteractiveScene(renderConfig); + renderConfig.callback(ret); + return ret as T extends true ? void : ReturnType; +}; + +/** throttled to animation framerate */ +const renderStaticSceneThrottled = throttleRAF( + (config: StaticSceneRenderConfig) => { + _renderStaticScene(config); + }, + { trailing: true }, +); + +/** + * Static scene is the non-ui canvas where we render elements. + */ +export const renderStaticScene = ( + renderConfig: StaticSceneRenderConfig, + throttle?: boolean, +) => { + if (throttle) { + renderStaticSceneThrottled(renderConfig); + return; + } + + _renderStaticScene(renderConfig); +}; + +export const cancelRender = () => { + renderInteractiveSceneThrottled.cancel(); + renderStaticSceneThrottled.cancel(); }; const renderTransformHandles = ( context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + renderConfig: InteractiveCanvasRenderConfig, + appState: InteractiveCanvasAppState, transformHandles: TransformHandles, angle: number, ): void => { @@ -1049,7 +1119,7 @@ const renderTransformHandles = ( const [x, y, width, height] = transformHandle; context.save(); - context.lineWidth = 1 / renderConfig.zoom.value; + context.lineWidth = 1 / appState.zoom.value; if (renderConfig.selectionColor) { context.strokeStyle = renderConfig.selectionColor; } @@ -1058,7 +1128,7 @@ const renderTransformHandles = ( // prefer round corners if roundRect API is available } else if (context.roundRect) { context.beginPath(); - context.roundRect(x, y, width, height, 2 / renderConfig.zoom.value); + context.roundRect(x, y, width, height, 2 / appState.zoom.value); context.fill(); context.stroke(); } else { @@ -1081,7 +1151,7 @@ const renderTransformHandles = ( const renderSelectionBorder = ( context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, elementProperties: { angle: number; elementX1: number; @@ -1111,13 +1181,13 @@ const renderSelectionBorder = ( const elementWidth = elementX2 - elementX1; const elementHeight = elementY2 - elementY1; - const linePadding = padding / renderConfig.zoom.value; - const lineWidth = 8 / renderConfig.zoom.value; - const spaceWidth = 4 / renderConfig.zoom.value; + const linePadding = padding / appState.zoom.value; + const lineWidth = 8 / appState.zoom.value; + const spaceWidth = 4 / appState.zoom.value; context.save(); - context.translate(renderConfig.scrollX, renderConfig.scrollY); - context.lineWidth = (activeEmbeddable ? 4 : 1) / renderConfig.zoom.value; + context.translate(appState.scrollX, appState.scrollY); + context.lineWidth = (activeEmbeddable ? 4 : 1) / appState.zoom.value; const count = selectionColors.length; for (let index = 0; index < count; ++index) { @@ -1145,7 +1215,7 @@ const renderSelectionBorder = ( const renderBindingHighlight = ( context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, suggestedBinding: SuggestedBinding, ) => { const renderHighlight = Array.isArray(suggestedBinding) @@ -1153,7 +1223,7 @@ const renderBindingHighlight = ( : renderBindingHighlightForBindableElement; context.save(); - context.translate(renderConfig.scrollX, renderConfig.scrollY); + context.translate(appState.scrollX, appState.scrollY); renderHighlight(context, suggestedBinding as any); context.restore(); @@ -1219,7 +1289,7 @@ const renderBindingHighlightForBindableElement = ( const renderFrameHighlight = ( context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, frame: NonDeleted, ) => { const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame); @@ -1227,10 +1297,10 @@ const renderFrameHighlight = ( const height = y2 - y1; context.strokeStyle = "rgb(0,118,255)"; - context.lineWidth = (FRAME_STYLE.strokeWidth * 2) / renderConfig.zoom.value; + context.lineWidth = (FRAME_STYLE.strokeWidth * 2) / appState.zoom.value; context.save(); - context.translate(renderConfig.scrollX, renderConfig.scrollY); + context.translate(appState.scrollX, appState.scrollY); strokeRectWithRotation( context, x1, @@ -1241,16 +1311,15 @@ const renderFrameHighlight = ( y1 + height / 2, frame.angle, false, - FRAME_STYLE.radius / renderConfig.zoom.value, + FRAME_STYLE.radius / appState.zoom.value, ); context.restore(); }; const renderElementsBoxHighlight = ( context: CanvasRenderingContext2D, - renderConfig: RenderConfig, + appState: InteractiveCanvasAppState, elements: NonDeleted[], - appState: AppState, ) => { const individualElements = elements.filter( (element) => element.groupIds.length === 0, @@ -1290,7 +1359,7 @@ const renderElementsBoxHighlight = ( individualElements.map((element) => getSelectionFromElements([element])), ) .forEach((selection) => - renderSelectionBorder(context, renderConfig, selection), + renderSelectionBorder(context, appState, selection), ); }; @@ -1324,7 +1393,7 @@ let linkCanvasCache: any; const renderLinkIcon = ( element: NonDeletedExcalidrawElement, context: CanvasRenderingContext2D, - appState: AppState, + appState: StaticCanvasAppState, ) => { if (element.link && !appState.selectedElementIds[element.id]) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); @@ -1375,42 +1444,6 @@ const renderLinkIcon = ( } }; -export const isVisibleElement = ( - element: ExcalidrawElement, - canvasWidth: number, - canvasHeight: number, - viewTransformations: { - zoom: Zoom; - offsetLeft: number; - offsetTop: number; - scrollX: number; - scrollY: number; - }, -) => { - const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates - const topLeftSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft, - clientY: viewTransformations.offsetTop, - }, - viewTransformations, - ); - const bottomRightSceneCoords = viewportCoordsToSceneCoords( - { - clientX: viewTransformations.offsetLeft + canvasWidth, - clientY: viewTransformations.offsetTop + canvasHeight, - }, - viewTransformations, - ); - - return ( - topLeftSceneCoords.x <= x2 && - topLeftSceneCoords.y <= y2 && - bottomRightSceneCoords.x >= x1 && - bottomRightSceneCoords.y >= y1 - ); -}; - // This should be only called for exporting purposes export const renderSceneToSvg = ( elements: readonly NonDeletedExcalidrawElement[], diff --git a/src/scene/Fonts.ts b/src/scene/Fonts.ts index e245eb16e2..05dddadc48 100644 --- a/src/scene/Fonts.ts +++ b/src/scene/Fonts.ts @@ -2,9 +2,9 @@ import { isTextElement, refreshTextDimensions } from "../element"; import { newElementWith } from "../element/mutateElement"; import { isBoundToContainer } from "../element/typeChecks"; import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types"; -import { invalidateShapeForElement } from "../renderer/renderElement"; import { getFontString } from "../utils"; import type Scene from "./Scene"; +import { ShapeCache } from "./ShapeCache"; export class Fonts { private scene: Scene; @@ -54,7 +54,7 @@ export class Fonts { this.scene.mapElements((element) => { if (isTextElement(element) && !isBoundToContainer(element)) { - invalidateShapeForElement(element); + ShapeCache.delete(element); didUpdate = true; return newElementWith(element, { ...refreshTextDimensions(element), diff --git a/src/scene/Renderer.ts b/src/scene/Renderer.ts new file mode 100644 index 0000000000..1522249510 --- /dev/null +++ b/src/scene/Renderer.ts @@ -0,0 +1,131 @@ +import { isElementInViewport } from "../element/sizeHelpers"; +import { isImageElement } from "../element/typeChecks"; +import { NonDeletedExcalidrawElement } from "../element/types"; +import { cancelRender } from "../renderer/renderScene"; +import { AppState } from "../types"; +import { memoize } from "../utils"; +import Scene from "./Scene"; + +export class Renderer { + private scene: Scene; + + constructor(scene: Scene) { + this.scene = scene; + } + + public getRenderableElements = (() => { + const getVisibleCanvasElements = ({ + elements, + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + height, + width, + }: { + elements: readonly NonDeletedExcalidrawElement[]; + zoom: AppState["zoom"]; + offsetLeft: AppState["offsetLeft"]; + offsetTop: AppState["offsetTop"]; + scrollX: AppState["scrollX"]; + scrollY: AppState["scrollY"]; + height: AppState["height"]; + width: AppState["width"]; + }): readonly NonDeletedExcalidrawElement[] => { + return elements.filter((element) => + isElementInViewport(element, width, height, { + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + }), + ); + }; + + const getCanvasElements = ({ + editingElement, + elements, + pendingImageElementId, + }: { + elements: readonly NonDeletedExcalidrawElement[]; + editingElement: AppState["editingElement"]; + pendingImageElementId: AppState["pendingImageElementId"]; + }) => { + return elements.filter((element) => { + if (isImageElement(element)) { + if ( + // => not placed on canvas yet (but in elements array) + pendingImageElementId === element.id + ) { + return false; + } + } + // we don't want to render text element that's being currently edited + // (it's rendered on remote only) + return ( + !editingElement || + editingElement.type !== "text" || + element.id !== editingElement.id + ); + }); + }; + + return memoize( + ({ + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + height, + width, + editingElement, + pendingImageElementId, + // unused but serves we cache on it to invalidate elements if they + // get mutated + versionNonce: _versionNonce, + }: { + zoom: AppState["zoom"]; + offsetLeft: AppState["offsetLeft"]; + offsetTop: AppState["offsetTop"]; + scrollX: AppState["scrollX"]; + scrollY: AppState["scrollY"]; + height: AppState["height"]; + width: AppState["width"]; + editingElement: AppState["editingElement"]; + pendingImageElementId: AppState["pendingImageElementId"]; + versionNonce: ReturnType["getVersionNonce"]>; + }) => { + const elements = this.scene.getNonDeletedElements(); + + const canvasElements = getCanvasElements({ + elements, + editingElement, + pendingImageElementId, + }); + + const visibleElements = getVisibleCanvasElements({ + elements: canvasElements, + zoom, + offsetLeft, + offsetTop, + scrollX, + scrollY, + height, + width, + }); + + return { canvasElements, visibleElements }; + }, + ); + })(); + + // NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be + // safe to break TS contract here (for upstream cases) + public destroy() { + cancelRender(); + this.getRenderableElements.clear(); + } +} diff --git a/src/scene/Scene.ts b/src/scene/Scene.ts index 5b5b7970b1..e64da78fcd 100644 --- a/src/scene/Scene.ts +++ b/src/scene/Scene.ts @@ -14,6 +14,7 @@ import { isFrameElement } from "../element/typeChecks"; import { getSelectedElements } from "./selection"; import { AppState } from "../types"; import { Assert, SameType } from "../utility-types"; +import { randomInteger } from "../random"; type ElementIdKey = InstanceType["elementId"]; type ElementKey = ExcalidrawElement | ElementIdKey; @@ -105,6 +106,7 @@ class Scene { elements: null, cache: new Map(), }; + private versionNonce: number | undefined; getElementsIncludingDeleted() { return this.elements; @@ -172,6 +174,10 @@ class Scene { return (this.elementsMap.get(id) as T | undefined) || null; } + getVersionNonce() { + return this.versionNonce; + } + getNonDeletedElement( id: ExcalidrawElement["id"], ): NonDeleted | null { @@ -230,6 +236,8 @@ class Scene { } informMutation() { + this.versionNonce = randomInteger(); + for (const callback of Array.from(this.callbacks)) { callback(); } diff --git a/src/scene/ShapeCache.ts b/src/scene/ShapeCache.ts new file mode 100644 index 0000000000..d2237220bf --- /dev/null +++ b/src/scene/ShapeCache.ts @@ -0,0 +1,61 @@ +import { Drawable } from "roughjs/bin/core"; +import { RoughGenerator } from "roughjs/bin/generator"; +import { ExcalidrawElement } from "../element/types"; +import { generateElementShape } from "../renderer/renderElement"; + +type ElementShape = Drawable | Drawable[] | null; + +type ElementShapes = { + freedraw: Drawable | null; + arrow: Drawable[]; + line: Drawable[]; + text: null; + image: null; +}; + +export class ShapeCache { + private static rg = new RoughGenerator(); + private static cache = new WeakMap(); + + public static get = (element: T) => { + return ShapeCache.cache.get( + element, + ) as T["type"] extends keyof ElementShapes + ? ElementShapes[T["type"]] | undefined + : Drawable | null | 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 + * return cached shape. + */ + public static generateElementShape = ( + element: T, + ) => { + const shape = generateElementShape( + element, + ShapeCache.rg, + /* so it prefers cache */ false, + ) as T["type"] extends keyof ElementShapes + ? ElementShapes[T["type"]] + : Drawable | null; + + ShapeCache.cache.set(element, shape); + + return shape; + }; +} diff --git a/src/scene/export.ts b/src/scene/export.ts index 61f235d184..ca838f0530 100644 --- a/src/scene/export.ts +++ b/src/scene/export.ts @@ -1,7 +1,7 @@ import rough from "roughjs/bin/rough"; import { NonDeletedExcalidrawElement } from "../element/types"; import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds"; -import { renderScene, renderSceneToSvg } from "../renderer/renderScene"; +import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene"; import { distance, isOnlyExportingSingleFrame } from "../utils"; import { AppState, BinaryFiles } from "../types"; import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants"; @@ -54,26 +54,23 @@ export const exportToCanvas = async ( const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements); - renderScene({ - elements, - appState, - scale, - rc: rough.canvas(canvas), + renderStaticScene({ canvas, - renderConfig: { + rc: rough.canvas(canvas), + elements, + visibleElements: elements, + scale, + appState: { + ...appState, viewBackgroundColor: exportBackground ? viewBackgroundColor : null, scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding), scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding), zoom: defaultAppState.zoom, - remotePointerViewportCoords: {}, - remoteSelectedElementIds: {}, shouldCacheIgnoreZoom: false, - remotePointerUsernames: {}, - remotePointerUserStates: {}, theme: appState.exportWithDarkMode ? "dark" : "light", + }, + renderConfig: { imageCache, - renderScrollbars: false, - renderSelection: false, renderGrid: false, isExporting: true, }, diff --git a/src/scene/scroll.ts b/src/scene/scroll.ts index 114d6db053..b5b8176e13 100644 --- a/src/scene/scroll.ts +++ b/src/scene/scroll.ts @@ -11,11 +11,7 @@ import { viewportCoordsToSceneCoords, } from "../utils"; -const isOutsideViewPort = ( - appState: AppState, - canvas: HTMLCanvasElement | null, - cords: Array, -) => { +const isOutsideViewPort = (appState: AppState, cords: Array) => { const [x1, y1, x2, y2] = cords; const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords( { sceneX: x1, sceneY: y1 }, @@ -49,7 +45,6 @@ export const centerScrollOn = ({ export const calculateScrollCenter = ( elements: readonly ExcalidrawElement[], appState: AppState, - canvas: HTMLCanvasElement | null, ): { scrollX: number; scrollY: number } => { elements = getVisibleElements(elements); @@ -61,7 +56,7 @@ export const calculateScrollCenter = ( } let [x1, y1, x2, y2] = getCommonBounds(elements); - if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) { + if (isOutsideViewPort(appState, [x1, y1, x2, y2])) { [x1, y1, x2, y2] = getClosestElementBounds( elements, viewportCoordsToSceneCoords( diff --git a/src/scene/scrollbars.ts b/src/scene/scrollbars.ts index 76a04d606e..1d93f688f7 100644 --- a/src/scene/scrollbars.ts +++ b/src/scene/scrollbars.ts @@ -1,6 +1,6 @@ import { ExcalidrawElement } from "../element/types"; import { getCommonBounds } from "../element"; -import { Zoom } from "../types"; +import { InteractiveCanvasAppState } from "../types"; import { ScrollBars } from "./types"; import { getGlobalCSSVariable } from "../utils"; import { getLanguage } from "../i18n"; @@ -13,15 +13,7 @@ export const getScrollBars = ( elements: readonly ExcalidrawElement[], viewportWidth: number, viewportHeight: number, - { - scrollX, - scrollY, - zoom, - }: { - scrollX: number; - scrollY: number; - zoom: Zoom; - }, + appState: InteractiveCanvasAppState, ): ScrollBars => { if (elements.length === 0) { return { @@ -34,8 +26,8 @@ export const getScrollBars = ( getCommonBounds(elements); // Apply zoom - const viewportWidthWithZoom = viewportWidth / zoom.value; - const viewportHeightWithZoom = viewportHeight / zoom.value; + const viewportWidthWithZoom = viewportWidth / appState.zoom.value; + const viewportHeightWithZoom = viewportHeight / appState.zoom.value; const viewportWidthDiff = viewportWidth - viewportWidthWithZoom; const viewportHeightDiff = viewportHeight - viewportHeightWithZoom; @@ -50,8 +42,10 @@ export const getScrollBars = ( const isRTL = getLanguage().rtl; // The viewport is the rectangle currently visible for the user - const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left; - const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top; + const viewportMinX = + -appState.scrollX + viewportWidthDiff / 2 + safeArea.left; + const viewportMinY = + -appState.scrollY + viewportHeightDiff / 2 + safeArea.top; const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right; const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom; diff --git a/src/scene/selection.ts b/src/scene/selection.ts index bbb629d3c2..e678894ab8 100644 --- a/src/scene/selection.ts +++ b/src/scene/selection.ts @@ -3,7 +3,7 @@ import { NonDeletedExcalidrawElement, } from "../element/types"; import { getElementAbsoluteCoords, getElementBounds } from "../element"; -import { AppState } from "../types"; +import { AppState, InteractiveCanvasAppState } from "../types"; import { isBoundToContainer } from "../element/typeChecks"; import { elementOverlapsWithFrame, @@ -146,7 +146,7 @@ export const getCommonAttributeOfSelectedElements = ( export const getSelectedElements = ( elements: readonly NonDeletedExcalidrawElement[], - appState: Pick, + appState: Pick, opts?: { includeBoundTextElement?: boolean; includeElementsInFrames?: boolean; diff --git a/src/scene/types.ts b/src/scene/types.ts index a54b02b265..9298f5967e 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -1,33 +1,63 @@ -import { ExcalidrawTextElement } from "../element/types"; -import { AppClassProperties, AppState } from "../types"; +import { RoughCanvas } from "roughjs/bin/canvas"; +import { + ExcalidrawTextElement, + NonDeletedExcalidrawElement, +} from "../element/types"; +import { + AppClassProperties, + InteractiveCanvasAppState, + StaticCanvasAppState, +} from "../types"; -export type RenderConfig = { - // AppState values - // --------------------------------------------------------------------------- - scrollX: AppState["scrollX"]; - scrollY: AppState["scrollY"]; - /** null indicates transparent bg */ - viewBackgroundColor: AppState["viewBackgroundColor"] | null; - zoom: AppState["zoom"]; - shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"]; - theme: AppState["theme"]; - // collab-related state - // --------------------------------------------------------------------------- - remotePointerViewportCoords: { [id: string]: { x: number; y: number } }; - remotePointerButton?: { [id: string]: string | undefined }; - remoteSelectedElementIds: { [elementId: string]: string[] }; - remotePointerUsernames: { [id: string]: string }; - remotePointerUserStates: { [id: string]: string }; +export type StaticCanvasRenderConfig = { // extra options passed to the renderer // --------------------------------------------------------------------------- imageCache: AppClassProperties["imageCache"]; - renderScrollbars?: boolean; - renderSelection?: boolean; - renderGrid?: boolean; + renderGrid: boolean; /** when exporting the behavior is slightly different (e.g. we can't use - CSS filters), and we disable render optimizations for best output */ + CSS filters), and we disable render optimizations for best output */ isExporting: boolean; +}; + +export type InteractiveCanvasRenderConfig = { + // collab-related state + // --------------------------------------------------------------------------- + remoteSelectedElementIds: { [elementId: string]: string[] }; + remotePointerViewportCoords: { [id: string]: { x: number; y: number } }; + remotePointerUserStates: { [id: string]: string }; + remotePointerUsernames: { [id: string]: string }; + remotePointerButton?: { [id: string]: string | undefined }; selectionColor?: string; + // extra options passed to the renderer + // --------------------------------------------------------------------------- + renderScrollbars?: boolean; +}; + +export type RenderInteractiveSceneCallback = { + atLeastOneVisibleElement: boolean; + elements: readonly NonDeletedExcalidrawElement[]; + scrollBars?: ScrollBars; +}; + +export type StaticSceneRenderConfig = { + canvas: HTMLCanvasElement; + rc: RoughCanvas; + elements: readonly NonDeletedExcalidrawElement[]; + visibleElements: readonly NonDeletedExcalidrawElement[]; + scale: number; + appState: StaticCanvasAppState; + renderConfig: StaticCanvasRenderConfig; +}; + +export type InteractiveSceneRenderConfig = { + canvas: HTMLCanvasElement | null; + elements: readonly NonDeletedExcalidrawElement[]; + visibleElements: readonly NonDeletedExcalidrawElement[]; + selectedElements: readonly NonDeletedExcalidrawElement[]; + scale: number; + appState: InteractiveCanvasAppState; + renderConfig: InteractiveCanvasRenderConfig; + callback: (data: RenderInteractiveSceneCallback) => void; }; export type SceneScroll = { diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 8ed2c30837..608ab8efd4 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -397,7 +397,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -431,7 +431,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -467,7 +467,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of renders 1`] = `7`; +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of renders 1`] = `4`; exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] appState 1`] = ` { @@ -587,14 +587,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -646,14 +646,14 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -666,7 +666,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of elements 1`] = `1`; -exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `14`; +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `6`; exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = ` { @@ -784,14 +784,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -816,14 +816,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": -10, "y": 0, @@ -875,14 +875,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -918,14 +918,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -947,14 +947,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -990,14 +990,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -1019,14 +1019,14 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": -10, "y": 0, @@ -1039,7 +1039,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `18`; +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `10`; exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] appState 1`] = ` { @@ -1157,14 +1157,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -1189,14 +1189,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": -10, "y": 0, @@ -1248,14 +1248,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -1291,14 +1291,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -1320,14 +1320,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -1363,14 +1363,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -1392,14 +1392,14 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": -10, "y": 0, @@ -1412,7 +1412,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `18`; +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `10`; exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] appState 1`] = ` { @@ -1532,14 +1532,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -1591,14 +1591,14 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -1611,7 +1611,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of elements 1`] = `1`; -exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `14`; +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `6`; exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = ` { @@ -1727,14 +1727,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1786,14 +1786,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -1827,14 +1827,14 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 20, "x": -10, "y": 0, @@ -1847,7 +1847,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of elements 1`] = `1`; -exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `14`; +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `7`; exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = ` { @@ -1965,14 +1965,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -1997,14 +1997,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": 0, "y": 10, @@ -2056,14 +2056,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -2099,14 +2099,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -2128,14 +2128,14 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": 0, "y": 10, @@ -2148,7 +2148,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `14`; +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `7`; exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = ` { @@ -2273,14 +2273,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 20, "x": -10, "y": 0, @@ -2307,14 +2307,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -2366,14 +2366,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -2409,14 +2409,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -2438,14 +2438,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 20, "x": 20, "y": 30, @@ -2486,14 +2486,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 20, "x": -10, "y": 0, @@ -2517,14 +2517,14 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 20, "x": 20, "y": 30, @@ -2537,7 +2537,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `20`; +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `10`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = ` { @@ -2657,14 +2657,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 406373543, "width": 20, "x": -10, "y": 0, @@ -2689,14 +2689,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1006504105, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 9, - "versionNonce": 1505387817, + "versionNonce": 1898319239, "width": 20, "x": 20, "y": 30, @@ -2748,79 +2748,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - ], - }, - { - "appState": { - "editingGroupId": null, - "editingLinearElement": null, - "name": "Untitled-201933152653", - "selectedElementIds": { - "id1": true, - }, - "selectedGroupIds": {}, - "viewBackgroundColor": "#ffffff", - }, - "elements": [ - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id1", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -2829,8 +2757,8 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "version": 2, "versionNonce": 401146281, "width": 20, - "x": 20, - "y": 30, + "x": -10, + "y": 0, }, ], }, @@ -2863,14 +2791,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -2892,229 +2820,13 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 453191, - "strokeColor": "#e03131", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 3, - "versionNonce": 2019559783, - "width": 20, - "x": 20, - "y": 30, - }, - ], - }, - { - "appState": { - "editingGroupId": null, - "editingLinearElement": null, - "name": "Untitled-201933152653", - "selectedElementIds": { - "id1": true, - }, - "selectedGroupIds": {}, - "viewBackgroundColor": "#ffffff", - }, - "elements": [ - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1278240551, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - { - "angle": 0, - "backgroundColor": "#a5d8ff", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id1", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 453191, - "strokeColor": "#e03131", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 4, - "versionNonce": 1150084233, - "width": 20, - "x": 20, - "y": 30, - }, - ], - }, - { - "appState": { - "editingGroupId": null, - "editingLinearElement": null, - "name": "Untitled-201933152653", - "selectedElementIds": { - "id1": true, - }, - "selectedGroupIds": {}, - "viewBackgroundColor": "#ffffff", - }, - "elements": [ - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - { - "angle": 0, - "backgroundColor": "#a5d8ff", - "boundElements": null, - "fillStyle": "cross-hatch", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id1", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 453191, - "strokeColor": "#e03131", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 5, - "versionNonce": 1116226695, - "width": 20, - "x": 20, - "y": 30, - }, - ], - }, - { - "appState": { - "editingGroupId": null, - "editingLinearElement": null, - "name": "Untitled-201933152653", - "selectedElementIds": { - "id1": true, - }, - "selectedGroupIds": {}, - "viewBackgroundColor": "#ffffff", - }, - "elements": [ - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - { - "angle": 0, - "backgroundColor": "#a5d8ff", - "boundElements": null, - "fillStyle": "cross-hatch", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id1", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 453191, - "strokeColor": "#e03131", - "strokeStyle": "solid", - "strokeWidth": 2, - "type": "rectangle", - "updated": 1, - "version": 6, "versionNonce": 1014066025, "width": 20, "x": 20, @@ -3151,23 +2863,23 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, }, { "angle": 0, - "backgroundColor": "#a5d8ff", + "backgroundColor": "transparent", "boundElements": null, - "fillStyle": "cross-hatch", + "fillStyle": "hachure", "frameId": null, "groupIds": [], "height": 20, @@ -3180,14 +2892,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#e03131", - "strokeStyle": "dotted", - "strokeWidth": 2, + "strokeStyle": "solid", + "strokeWidth": 1, "type": "rectangle", "updated": 1, - "version": 7, - "versionNonce": 238820263, + "version": 3, + "versionNonce": 400692809, "width": 20, "x": 20, "y": 30, @@ -3223,14 +2935,302 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, + "width": 20, + "x": -10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#e03131", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 1505387817, + "width": 20, + "x": 20, + "y": 30, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 401146281, + "width": 20, + "x": -10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#e03131", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 5, + "versionNonce": 493213705, + "width": 20, + "x": 20, + "y": 30, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 401146281, + "width": 20, + "x": -10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#e03131", + "strokeStyle": "solid", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 6, + "versionNonce": 81784553, + "width": 20, + "x": 20, + "y": 30, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 401146281, + "width": 20, + "x": -10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "#a5d8ff", + "boundElements": null, + "fillStyle": "cross-hatch", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#e03131", + "strokeStyle": "dotted", + "strokeWidth": 2, + "type": "rectangle", + "updated": 1, + "version": 7, + "versionNonce": 1723083209, + "width": 20, + "x": 20, + "y": 30, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -3252,14 +3252,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1006504105, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 8, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 20, "x": 20, "y": 30, @@ -3295,14 +3295,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -3324,14 +3324,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1006504105, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 9, - "versionNonce": 1505387817, + "versionNonce": 1898319239, "width": 20, "x": 20, "y": 30, @@ -3367,14 +3367,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 23633383, + "versionNonce": 406373543, "width": 20, "x": -10, "y": 0, @@ -3396,14 +3396,14 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1006504105, "strokeColor": "#e03131", "strokeStyle": "dotted", "strokeWidth": 2, "type": "rectangle", "updated": 1, "version": 9, - "versionNonce": 1505387817, + "versionNonce": 1898319239, "width": 20, "x": 20, "y": 30, @@ -3416,7 +3416,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `32`; +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `17`; exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = ` { @@ -3534,14 +3534,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": 20, "y": 30, @@ -3566,14 +3566,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -3625,79 +3625,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - ], - }, - { - "appState": { - "editingGroupId": null, - "editingLinearElement": null, - "name": "Untitled-201933152653", - "selectedElementIds": { - "id1": true, - }, - "selectedGroupIds": {}, - "viewBackgroundColor": "#ffffff", - }, - "elements": [ - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id1", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -3706,6 +3634,78 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "version": 2, "versionNonce": 401146281, "width": 20, + "x": -10, + "y": 0, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 401146281, + "width": 20, + "x": -10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 1014066025, + "width": 20, "x": 20, "y": 30, }, @@ -3740,14 +3740,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": 20, "y": 30, @@ -3769,14 +3769,14 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -3789,7 +3789,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `18`; +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `10`; exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] appState 1`] = ` { @@ -3907,14 +3907,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": 20, "y": 30, @@ -3939,14 +3939,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -3998,79 +3998,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - ], - }, - { - "appState": { - "editingGroupId": null, - "editingLinearElement": null, - "name": "Untitled-201933152653", - "selectedElementIds": { - "id1": true, - }, - "selectedGroupIds": {}, - "viewBackgroundColor": "#ffffff", - }, - "elements": [ - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id0", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 1278240551, - "strokeColor": "#1e1e1e", - "strokeStyle": "solid", - "strokeWidth": 1, - "type": "rectangle", - "updated": 1, - "version": 2, - "versionNonce": 449462985, - "width": 20, - "x": -10, - "y": 0, - }, - { - "angle": 0, - "backgroundColor": "transparent", - "boundElements": null, - "fillStyle": "hachure", - "frameId": null, - "groupIds": [], - "height": 20, - "id": "id1", - "isDeleted": false, - "link": null, - "locked": false, - "opacity": 100, - "roughness": 1, - "roundness": { - "type": 3, - }, - "seed": 453191, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -4079,6 +4007,78 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "version": 2, "versionNonce": 401146281, "width": 20, + "x": -10, + "y": 0, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 449462985, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 401146281, + "width": 20, + "x": -10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 20, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1150084233, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 1014066025, + "width": 20, "x": 20, "y": 30, }, @@ -4113,14 +4113,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 20, "x": 20, "y": 30, @@ -4142,14 +4142,14 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -4162,7 +4162,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `18`; +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `10`; exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = ` { @@ -4283,14 +4283,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 20, "x": -10, "y": 0, @@ -4315,14 +4315,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 20, "x": 20, "y": 30, @@ -4374,14 +4374,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -4417,14 +4417,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 20, "x": -10, "y": 0, @@ -4446,14 +4446,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 238820263, "width": 20, "x": 20, "y": 30, @@ -4494,14 +4494,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1505387817, "width": 20, "x": -10, "y": 0, @@ -4525,14 +4525,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 23633383, "width": 20, "x": 20, "y": 30, @@ -4569,14 +4569,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 20, "x": -10, "y": 0, @@ -4598,14 +4598,14 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 20, "x": 20, "y": 30, @@ -4618,7 +4618,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `21`; +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `11`; exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = ` { @@ -5015,14 +5015,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": -10, "y": 0, @@ -5047,14 +5047,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 10, "x": 10, "y": 0, @@ -5106,14 +5106,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": -10, "y": 0, @@ -5149,14 +5149,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": -10, "y": 0, @@ -5178,14 +5178,14 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1014066025, "width": 10, "x": 10, "y": 0, @@ -5198,7 +5198,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `20`; +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `9`; exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = ` { @@ -5599,14 +5599,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 23633383, "width": 10, "x": -10, "y": 0, @@ -5633,14 +5633,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 493213705, "width": 10, "x": 10, "y": 0, @@ -5692,14 +5692,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 10, "x": -10, "y": 0, @@ -5735,14 +5735,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 10, "x": -10, "y": 0, @@ -5764,14 +5764,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 238820263, "width": 10, "x": 10, "y": 0, @@ -5812,14 +5812,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 453191, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 23633383, "width": 10, "x": -10, "y": 0, @@ -5843,14 +5843,14 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro "roundness": { "type": 3, }, - "seed": 453191, + "seed": 1116226695, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 493213705, "width": 10, "x": 10, "y": 0, @@ -5863,7 +5863,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `21`; +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `10`; exports[`contextMenu element > shows context menu for canvas > [end of test] appState 1`] = ` { @@ -6098,7 +6098,7 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] his exports[`contextMenu element > shows context menu for canvas > [end of test] number of elements 1`] = `0`; -exports[`contextMenu element > shows context menu for canvas > [end of test] number of renders 1`] = `6`; +exports[`contextMenu element > shows context menu for canvas > [end of test] number of renders 1`] = `3`; exports[`contextMenu element > shows context menu for element > [end of test] appState 1`] = ` { @@ -6866,14 +6866,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -6898,7 +6898,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -6930,7 +6930,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -6989,14 +6989,14 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 20, "x": -10, "y": 0, @@ -7031,6 +7031,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] nu exports[`contextMenu element > shows context menu for element > [end of test] number of elements 2`] = `2`; -exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `12`; +exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `6`; -exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `11`; +exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `4`; diff --git a/src/tests/__snapshots__/dragCreate.test.tsx.snap b/src/tests/__snapshots__/dragCreate.test.tsx.snap index 3f527ded19..34c994da51 100644 --- a/src/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/src/tests/__snapshots__/dragCreate.test.tsx.snap @@ -33,7 +33,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -42,7 +42,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 30, "x": 30, "y": 20, @@ -69,14 +69,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 30, "x": 30, "y": 20, @@ -103,14 +103,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 30, "x": 30, "y": 20, @@ -148,7 +148,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -157,7 +157,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "type": "line", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 30, "x": 30, "y": 20, @@ -184,14 +184,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 30, "x": 30, "y": 20, diff --git a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap index 190f4b92f7..41e9f2b125 100644 --- a/src/tests/__snapshots__/linearElementEditor.test.tsx.snap +++ b/src/tests/__snapshots__/linearElementEditor.test.tsx.snap @@ -5,7 +5,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: 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: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, 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: 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: Virgil, Segoe UI Emoji;" tabindex="0" wrap="off" /> diff --git a/src/tests/__snapshots__/move.test.tsx.snap b/src/tests/__snapshots__/move.test.tsx.snap index e7b73d67aa..092074ba35 100644 --- a/src/tests/__snapshots__/move.test.tsx.snap +++ b/src/tests/__snapshots__/move.test.tsx.snap @@ -18,14 +18,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 1`] = ` "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 2019559783, + "versionNonce": 238820263, "width": 30, "x": 30, "y": 20, @@ -50,14 +50,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 2`] = ` "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 30, "x": -10, "y": 60, @@ -82,14 +82,14 @@ exports[`move element > rectangle 1`] = ` "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 30, "x": 0, "y": 40, @@ -119,14 +119,14 @@ exports[`move element > rectangles with binding arrow 1`] = ` "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 100, "x": 0, "y": 0, @@ -156,14 +156,14 @@ exports[`move element > rectangles with binding arrow 2`] = ` "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 6, - "versionNonce": 1723083209, + "versionNonce": 927333447, "width": 300, "x": 201, "y": 2, @@ -205,7 +205,7 @@ exports[`move element > rectangles with binding arrow 3`] = ` "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "startArrowhead": null, "startBinding": { "elementId": "id0", @@ -218,7 +218,7 @@ exports[`move element > rectangles with binding arrow 3`] = ` "type": "line", "updated": 1, "version": 11, - "versionNonce": 1006504105, + "versionNonce": 1051383431, "width": 81, "x": 110, "y": 49.981789081137734, diff --git a/src/tests/__snapshots__/multiPointCreate.test.tsx.snap b/src/tests/__snapshots__/multiPointCreate.test.tsx.snap index 27a6647f76..1f488b4d19 100644 --- a/src/tests/__snapshots__/multiPointCreate.test.tsx.snap +++ b/src/tests/__snapshots__/multiPointCreate.test.tsx.snap @@ -38,7 +38,7 @@ exports[`multi point mode in linear elements > arrow 1`] = ` "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -47,7 +47,7 @@ exports[`multi point mode in linear elements > arrow 1`] = ` "type": "arrow", "updated": 1, "version": 7, - "versionNonce": 1150084233, + "versionNonce": 1505387817, "width": 70, "x": 30, "y": 30, @@ -92,7 +92,7 @@ exports[`multi point mode in linear elements > line 1`] = ` "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -101,7 +101,7 @@ exports[`multi point mode in linear elements > line 1`] = ` "type": "line", "updated": 1, "version": 7, - "versionNonce": 1150084233, + "versionNonce": 1505387817, "width": 70, "x": 30, "y": 30, diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 8c8dd713be..a1a76204b1 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -150,14 +150,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -193,14 +193,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -222,14 +222,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 0, "y": 30, @@ -265,14 +265,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -294,14 +294,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 0, "y": 30, @@ -323,14 +323,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 10, "x": 0, "y": 60, @@ -369,14 +369,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 0, "y": 30, @@ -400,14 +400,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 10, "x": 0, "y": 0, @@ -431,14 +431,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 10, "x": 0, "y": 60, @@ -451,7 +451,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of elements 1`] = `0`; -exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `28`; +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `14`; exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] appState 1`] = ` { @@ -605,14 +605,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 100, "x": 0, "y": 0, @@ -648,14 +648,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 100, "x": 0, "y": 0, @@ -677,14 +677,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -720,14 +720,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 100, "x": 0, "y": 0, @@ -749,14 +749,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -778,14 +778,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 100, "x": 220, "y": 220, @@ -824,14 +824,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -855,14 +855,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 100, "x": 0, "y": 0, @@ -886,14 +886,14 @@ exports[`given element A and group of elements B and given both are selected whe "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 100, "x": 220, "y": 220, @@ -906,7 +906,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of elements 1`] = `0`; -exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `24`; +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `14`; exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] appState 1`] = ` { @@ -1051,14 +1051,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -1094,14 +1094,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -1123,14 +1123,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 0, @@ -1171,14 +1171,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 0, "y": 0, @@ -1202,14 +1202,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 30, "y": 0, @@ -1247,14 +1247,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 0, "y": 0, @@ -1278,14 +1278,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 30, "y": 0, @@ -1323,14 +1323,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 0, "y": 0, @@ -1354,14 +1354,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 30, "y": 0, @@ -1383,14 +1383,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1604849351, + "versionNonce": 1723083209, "width": 10, "x": 60, "y": 0, @@ -1433,14 +1433,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 493213705, + "versionNonce": 1315507081, "width": 10, "x": 0, "y": 0, @@ -1465,14 +1465,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 1898319239, "width": 10, "x": 30, "y": 0, @@ -1496,14 +1496,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 81784553, + "versionNonce": 640725609, "width": 10, "x": 60, "y": 0, @@ -1542,14 +1542,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 493213705, + "versionNonce": 1315507081, "width": 10, "x": 0, "y": 0, @@ -1574,14 +1574,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 1898319239, "width": 10, "x": 30, "y": 0, @@ -1605,14 +1605,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 81784553, + "versionNonce": 640725609, "width": 10, "x": 60, "y": 0, @@ -1651,14 +1651,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 493213705, + "versionNonce": 1315507081, "width": 10, "x": 0, "y": 0, @@ -1683,14 +1683,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 1898319239, "width": 10, "x": 30, "y": 0, @@ -1714,14 +1714,14 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 81784553, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 81784553, + "versionNonce": 640725609, "width": 10, "x": 60, "y": 0, @@ -1734,7 +1734,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `43`; +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `15`; exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] appState 1`] = ` { @@ -1881,14 +1881,14 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -1924,14 +1924,14 @@ exports[`regression tests > Drags selected element when hitting only bounding bo "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 25, "y": 25, @@ -1944,7 +1944,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `12`; +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `9`; exports[`regression tests > adjusts z order when grouping > [end of test] appState 1`] = ` { @@ -2094,14 +2094,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -2137,14 +2137,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -2166,14 +2166,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -2209,14 +2209,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -2238,14 +2238,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -2267,14 +2267,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 10, "x": 50, "y": 10, @@ -2313,14 +2313,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -2344,14 +2344,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 10, "x": 10, "y": 10, @@ -2375,14 +2375,14 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 10, "x": 50, "y": 10, @@ -2395,7 +2395,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor exports[`regression tests > adjusts z order when grouping > [end of test] number of elements 1`] = `0`; -exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `22`; +exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `14`; exports[`regression tests > alt-drag duplicates an element > [end of test] appState 1`] = ` { @@ -2542,14 +2542,14 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -2585,14 +2585,14 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 1014066025, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 2019559783, + "versionNonce": 238820263, "width": 10, "x": 10, "y": 10, @@ -2614,14 +2614,14 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 20, "y": 20, @@ -2634,7 +2634,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo exports[`regression tests > alt-drag duplicates an element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `12`; +exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `9`; exports[`regression tests > arrow keys > [end of test] appState 1`] = ` { @@ -2779,14 +2779,14 @@ exports[`regression tests > arrow keys > [end of test] history 1`] = ` "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -2799,7 +2799,7 @@ exports[`regression tests > arrow keys > [end of test] history 1`] = ` exports[`regression tests > arrow keys > [end of test] number of elements 1`] = `0`; -exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `21`; +exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `13`; exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] appState 1`] = ` { @@ -2946,14 +2946,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 200, "x": 100, "y": 100, @@ -2989,14 +2989,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 200, "x": 100, "y": 100, @@ -3018,14 +3018,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 200, "x": 100, "y": 100, @@ -3061,14 +3061,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 200, "x": 100, "y": 100, @@ -3090,14 +3090,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 200, "x": 100, "y": 100, @@ -3119,14 +3119,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 350, "x": 300, "y": 300, @@ -3162,14 +3162,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 200, "x": 100, "y": 100, @@ -3191,14 +3191,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 200, "x": 300, "y": 300, @@ -3220,14 +3220,14 @@ exports[`regression tests > can drag element that covers another element, while "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 350, "x": 300, "y": 300, @@ -3240,7 +3240,7 @@ exports[`regression tests > can drag element that covers another element, while exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `20`; +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `15`; exports[`regression tests > change the properties of a shape > [end of test] appState 1`] = ` { @@ -3385,14 +3385,14 @@ exports[`regression tests > change the properties of a shape > [end of test] his "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -3428,14 +3428,14 @@ exports[`regression tests > change the properties of a shape > [end of test] his "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 10, "x": 10, "y": 10, @@ -3471,14 +3471,14 @@ exports[`regression tests > change the properties of a shape > [end of test] his "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 10, "y": 10, @@ -3514,14 +3514,14 @@ exports[`regression tests > change the properties of a shape > [end of test] his "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1971c2", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 401146281, + "versionNonce": 238820263, "width": 10, "x": 10, "y": 10, @@ -3534,7 +3534,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his exports[`regression tests > change the properties of a shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `14`; +exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `10`; exports[`regression tests > click on an element and drag it > [dragged] appState 1`] = ` { @@ -3654,14 +3654,14 @@ exports[`regression tests > click on an element and drag it > [dragged] element "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 20, "y": 20, @@ -3713,14 +3713,14 @@ exports[`regression tests > click on an element and drag it > [dragged] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -3756,14 +3756,14 @@ exports[`regression tests > click on an element and drag it > [dragged] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 20, "y": 20, @@ -3776,7 +3776,7 @@ exports[`regression tests > click on an element and drag it > [dragged] history exports[`regression tests > click on an element and drag it > [dragged] number of elements 1`] = `1`; -exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `12`; +exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `9`; exports[`regression tests > click on an element and drag it > [end of test] appState 1`] = ` { @@ -3923,14 +3923,14 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -3966,14 +3966,14 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 20, "y": 20, @@ -4009,14 +4009,14 @@ exports[`regression tests > click on an element and drag it > [end of test] hist "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 2019559783, + "versionNonce": 400692809, "width": 10, "x": 10, "y": 10, @@ -4029,7 +4029,7 @@ exports[`regression tests > click on an element and drag it > [end of test] hist exports[`regression tests > click on an element and drag it > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `15`; +exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `11`; exports[`regression tests > click to select a shape > [end of test] appState 1`] = ` { @@ -4176,14 +4176,14 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -4219,14 +4219,14 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -4248,14 +4248,14 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -4268,7 +4268,7 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] exports[`regression tests > click to select a shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `15`; +exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `10`; exports[`regression tests > click-drag to select a group > [end of test] appState 1`] = ` { @@ -4416,14 +4416,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -4459,14 +4459,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -4488,14 +4488,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -4531,14 +4531,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -4560,14 +4560,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -4589,14 +4589,14 @@ exports[`regression tests > click-drag to select a group > [end of test] history "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 10, "x": 50, "y": 10, @@ -4609,7 +4609,479 @@ exports[`regression tests > click-drag to select a group > [end of test] history exports[`regression tests > click-drag to select a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `21`; +exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `13`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] appState 1`] = ` +{ + "activeEmbeddable": null, + "activeTool": { + "customType": null, + "lastActiveTool": null, + "locked": false, + "type": "selection", + }, + "collaborators": Map {}, + "contextMenu": null, + "currentChartType": "bar", + "currentItemBackgroundColor": "transparent", + "currentItemEndArrowhead": "arrow", + "currentItemFillStyle": "hachure", + "currentItemFontFamily": 1, + "currentItemFontSize": 20, + "currentItemOpacity": 100, + "currentItemRoughness": 1, + "currentItemRoundness": "round", + "currentItemStartArrowhead": null, + "currentItemStrokeColor": "#1e1e1e", + "currentItemStrokeStyle": "solid", + "currentItemStrokeWidth": 1, + "currentItemTextAlign": "left", + "cursorButton": "up", + "defaultSidebarDockedPreference": false, + "draggingElement": null, + "editingElement": null, + "editingFrame": null, + "editingGroupId": null, + "editingLinearElement": null, + "elementsToHighlight": null, + "errorMessage": null, + "exportBackground": true, + "exportEmbedScene": false, + "exportScale": 1, + "exportWithDarkMode": false, + "fileHandle": null, + "frameRendering": { + "clip": true, + "enabled": true, + "name": true, + "outline": true, + }, + "frameToHighlight": null, + "gridSize": null, + "height": 768, + "isBindingEnabled": true, + "isLoading": false, + "isResizing": false, + "isRotating": false, + "lastPointerDownWith": "mouse", + "multiElement": null, + "name": "Untitled-201933152653", + "offsetLeft": 0, + "offsetTop": 0, + "openDialog": null, + "openMenu": null, + "openPopup": null, + "openSidebar": null, + "pasteDialog": { + "data": null, + "shown": false, + }, + "penDetected": false, + "penMode": false, + "pendingImageElementId": null, + "previousSelectedElementIds": {}, + "resizingElement": null, + "scrollX": 0, + "scrollY": 0, + "scrolledOutside": false, + "selectedElementIds": { + "id1": true, + }, + "selectedElementsAreBeingDragged": false, + "selectedGroupIds": { + "id4": false, + }, + "selectedLinearElement": null, + "selectionElement": null, + "shouldCacheIgnoreZoom": false, + "showHyperlinkPopup": false, + "showStats": false, + "showWelcomeScreen": true, + "startBoundElement": null, + "suggestedBindings": [], + "theme": "light", + "toast": null, + "viewBackgroundColor": "#ffffff", + "viewModeEnabled": false, + "width": 1024, + "zenModeEnabled": false, + "zoom": { + "value": 1, + }, +} +`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] history 1`] = ` +{ + "recording": false, + "redoStack": [], + "stateHistory": [ + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": {}, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id0": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 453191, + "width": 10, + "x": 10, + "y": 0, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 453191, + "width": 10, + "x": 10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [], + "height": 10, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 2019559783, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 2, + "versionNonce": 1116226695, + "width": 10, + "x": 50, + "y": 0, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id0": true, + "id1": true, + }, + "selectedGroupIds": { + "id4": true, + }, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id0", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1604849351, + "width": 10, + "x": 10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 2019559783, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1505387817, + "width": 10, + "x": 50, + "y": 0, + }, + ], + }, + { + "appState": { + "editingGroupId": "id4", + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": {}, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 493213705, + "width": 10, + "x": 10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 2019559783, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1505387817, + "width": 10, + "x": 50, + "y": 0, + }, + ], + }, + { + "appState": { + "editingGroupId": null, + "editingLinearElement": null, + "name": "Untitled-201933152653", + "selectedElementIds": { + "id1": true, + }, + "selectedGroupIds": { + "id4": false, + }, + "viewBackgroundColor": "#ffffff", + }, + "elements": [ + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id0", + "isDeleted": true, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 1278240551, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 4, + "versionNonce": 493213705, + "width": 10, + "x": 10, + "y": 0, + }, + { + "angle": 0, + "backgroundColor": "transparent", + "boundElements": null, + "fillStyle": "hachure", + "frameId": null, + "groupIds": [ + "id4", + ], + "height": 10, + "id": "id1", + "isDeleted": false, + "link": null, + "locked": false, + "opacity": 100, + "roughness": 1, + "roundness": { + "type": 3, + }, + "seed": 2019559783, + "strokeColor": "#1e1e1e", + "strokeStyle": "solid", + "strokeWidth": 1, + "type": "rectangle", + "updated": 1, + "version": 3, + "versionNonce": 1505387817, + "width": 10, + "x": 50, + "y": 0, + }, + ], + }, + ], +} +`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of elements 1`] = `0`; + +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of renders 1`] = `12`; exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` { @@ -4655,7 +5127,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -4736,7 +5208,7 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -4811,14 +5283,14 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -4854,14 +5326,14 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -4883,14 +5355,14 @@ exports[`regression tests > deselects group of selected elements on pointer down "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 110, "y": 110, @@ -4903,7 +5375,7 @@ exports[`regression tests > deselects group of selected elements on pointer down exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `16`; +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `10`; exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] appState 1`] = ` { @@ -4949,7 +5421,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w "roundness": { "type": 2, }, - "seed": 2019559783, + "seed": 400692809, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -5077,14 +5549,14 @@ exports[`regression tests > deselects group of selected elements on pointer up w "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -5120,14 +5592,14 @@ exports[`regression tests > deselects group of selected elements on pointer up w "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -5149,14 +5621,14 @@ exports[`regression tests > deselects group of selected elements on pointer up w "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 110, "y": 110, @@ -5169,7 +5641,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `17`; +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `10`; exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` { @@ -5215,7 +5687,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -5295,7 +5767,7 @@ exports[`regression tests > deselects selected element on pointer down when poin "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -5370,14 +5842,14 @@ exports[`regression tests > deselects selected element on pointer down when poin "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -5390,7 +5862,7 @@ exports[`regression tests > deselects selected element on pointer down when poin exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `10`; +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `7`; exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] appState 1`] = ` { @@ -5535,14 +6007,14 @@ exports[`regression tests > deselects selected element, on pointer up, when clic "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 100, "x": 0, "y": 0, @@ -5555,7 +6027,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `11`; +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `7`; exports[`regression tests > double click to edit a group > [end of test] appState 1`] = ` { @@ -5700,14 +6172,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -5743,14 +6215,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -5772,14 +6244,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -5815,14 +6287,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -5844,14 +6316,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -5873,14 +6345,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 10, "x": 50, "y": 10, @@ -5922,14 +6394,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 23633383, "width": 10, "x": 10, "y": 10, @@ -5953,14 +6425,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 10, "x": 30, "y": 10, @@ -5984,14 +6456,14 @@ exports[`regression tests > double click to edit a group > [end of test] history "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 10, "x": 50, "y": 10, @@ -6004,7 +6476,7 @@ exports[`regression tests > double click to edit a group > [end of test] history exports[`regression tests > double click to edit a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `20`; +exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `14`; exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] appState 1`] = ` { @@ -6153,14 +6625,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -6196,14 +6668,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -6225,14 +6697,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 110, "y": 110, @@ -6269,14 +6741,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 25, "y": 25, @@ -6298,14 +6770,14 @@ exports[`regression tests > drags selected elements from point inside common bou "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 23633383, "width": 10, "x": 135, "y": 135, @@ -6318,7 +6790,7 @@ exports[`regression tests > drags selected elements from point inside common bou exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of elements 1`] = `0`; -exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `18`; +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `12`; exports[`regression tests > draw every type of shape > [end of test] appState 1`] = ` { @@ -6461,14 +6933,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -6504,14 +6976,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -6533,14 +7005,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -6576,14 +7048,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -6605,14 +7077,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -6634,14 +7106,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -6677,14 +7149,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -6706,14 +7178,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -6735,14 +7207,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -6777,7 +7249,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -6786,7 +7258,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -6822,14 +7294,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -6851,14 +7323,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -6880,14 +7352,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -6922,7 +7394,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -6931,7 +7403,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -6966,7 +7438,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1723083209, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -6975,7 +7447,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 50, "x": 220, "y": -10, @@ -7011,14 +7483,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -7040,14 +7512,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -7069,14 +7541,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -7111,7 +7583,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7120,7 +7592,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -7155,7 +7627,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1723083209, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7164,7 +7636,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 50, "x": 220, "y": -10, @@ -7202,7 +7674,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1505387817, + "seed": 1898319239, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7211,7 +7683,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 5, - "versionNonce": 81784553, + "versionNonce": 1349943049, "width": 50, "x": 310, "y": -10, @@ -7247,14 +7719,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -7276,14 +7748,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -7305,14 +7777,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -7347,7 +7819,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7356,7 +7828,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -7391,7 +7863,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1723083209, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7400,7 +7872,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 50, "x": 220, "y": -10, @@ -7442,7 +7914,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1505387817, + "seed": 1898319239, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7451,7 +7923,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 7, - "versionNonce": 1723083209, + "versionNonce": 1292308681, "width": 80, "x": 310, "y": -10, @@ -7487,14 +7959,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -7516,14 +7988,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -7545,14 +8017,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -7587,7 +8059,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7596,7 +8068,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -7631,7 +8103,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1723083209, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7640,7 +8112,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 50, "x": 220, "y": -10, @@ -7682,7 +8154,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1505387817, + "seed": 1898319239, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7691,7 +8163,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 7, - "versionNonce": 1723083209, + "versionNonce": 1292308681, "width": 80, "x": 310, "y": -10, @@ -7729,7 +8201,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 760410951, + "seed": 1508694887, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7738,7 +8210,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 5, - "versionNonce": 1898319239, + "versionNonce": 1177973545, "width": 50, "x": 430, "y": -10, @@ -7774,14 +8246,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -7803,14 +8275,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -7832,14 +8304,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -7874,7 +8346,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7883,7 +8355,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -7918,7 +8390,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1723083209, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7927,7 +8399,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 50, "x": 220, "y": -10, @@ -7969,7 +8441,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1505387817, + "seed": 1898319239, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -7978,7 +8450,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 7, - "versionNonce": 1723083209, + "versionNonce": 1292308681, "width": 80, "x": 310, "y": -10, @@ -8020,7 +8492,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 760410951, + "seed": 1508694887, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8029,7 +8501,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 7, - "versionNonce": 406373543, + "versionNonce": 271613161, "width": 80, "x": 430, "y": -10, @@ -8063,14 +8535,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -8092,14 +8564,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 20, "x": 40, "y": -10, @@ -8121,14 +8593,14 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 20, "x": 70, "y": -10, @@ -8163,7 +8635,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1150084233, + "seed": 23633383, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8172,7 +8644,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 50, "x": 130, "y": -10, @@ -8207,7 +8679,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 238820263, + "seed": 1723083209, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8216,7 +8688,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 1604849351, + "versionNonce": 289600103, "width": 50, "x": 220, "y": -10, @@ -8258,7 +8730,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 1505387817, + "seed": 1898319239, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8267,7 +8739,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 7, - "versionNonce": 1723083209, + "versionNonce": 1292308681, "width": 80, "x": 310, "y": -10, @@ -8309,7 +8781,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 760410951, + "seed": 1508694887, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -8318,7 +8790,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "line", "updated": 1, "version": 7, - "versionNonce": 406373543, + "versionNonce": 271613161, "width": 80, "x": 430, "y": -10, @@ -8361,7 +8833,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] ], "roughness": 1, "roundness": null, - "seed": 941653321, + "seed": 1189086535, "simulatePressure": false, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -8369,7 +8841,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] "type": "freedraw", "updated": 1, "version": 4, - "versionNonce": 1359939303, + "versionNonce": 1439318121, "width": 50, "x": 550, "y": -10, @@ -8382,7 +8854,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `57`; +exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `36`; exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = ` { @@ -8530,14 +9002,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -8573,14 +9045,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -8602,14 +9074,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -8645,14 +9117,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -8674,14 +9146,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -8703,14 +9175,14 @@ exports[`regression tests > given a group of selected elements with an element t "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 100, "x": 310, "y": 310, @@ -8723,7 +9195,7 @@ exports[`regression tests > given a group of selected elements with an element t exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `21`; +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `13`; exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] appState 1`] = ` { @@ -8871,14 +9343,14 @@ exports[`regression tests > given a selected element A and a not selected elemen "roundness": { "type": 3, }, - "seed": 337897, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 401146281, "width": 1000, "x": 0, "y": 0, @@ -8914,14 +9386,14 @@ exports[`regression tests > given a selected element A and a not selected elemen "roundness": { "type": 3, }, - "seed": 337897, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 401146281, "width": 1000, "x": 0, "y": 0, @@ -8943,14 +9415,14 @@ exports[`regression tests > given a selected element A and a not selected elemen "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 1150084233, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1014066025, "width": 1000, "x": 500, "y": 500, @@ -8963,7 +9435,7 @@ exports[`regression tests > given a selected element A and a not selected elemen exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `19`; +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `11`; exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] appState 1`] = ` { @@ -9110,7 +9582,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -9139,7 +9611,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -9159,7 +9631,7 @@ exports[`regression tests > given selected element A with lower z-index than uns exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of renders 1`] = `12`; +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of renders 1`] = `5`; exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] appState 1`] = ` { @@ -9306,7 +9778,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -9335,7 +9807,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -9378,14 +9850,14 @@ exports[`regression tests > given selected element A with lower z-index than uns "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 401146281, + "versionNonce": 1150084233, "width": 1000, "x": 100, "y": 100, @@ -9407,7 +9879,7 @@ exports[`regression tests > given selected element A with lower z-index than uns "roundness": { "type": 3, }, - "seed": 1278240551, + "seed": 449462985, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -9427,7 +9899,7 @@ exports[`regression tests > given selected element A with lower z-index than uns exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of renders 1`] = `13`; +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of renders 1`] = `7`; exports[`regression tests > key 2 selects rectangle tool > [end of test] appState 1`] = ` { @@ -9572,14 +10044,14 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -9592,7 +10064,7 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] history exports[`regression tests > key 2 selects rectangle tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key 2 selects rectangle tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key 2 selects rectangle tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key 3 selects diamond tool > [end of test] appState 1`] = ` { @@ -9737,14 +10209,14 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] history 1 "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -9757,7 +10229,7 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] history 1 exports[`regression tests > key 3 selects diamond tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key 3 selects diamond tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key 3 selects diamond tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key 4 selects ellipse tool > [end of test] appState 1`] = ` { @@ -9902,14 +10374,14 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] history 1 "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -9922,7 +10394,7 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] history 1 exports[`regression tests > key 4 selects ellipse tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key 4 selects ellipse tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key 4 selects ellipse tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`] = ` { @@ -10103,7 +10575,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -10112,7 +10584,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -10125,7 +10597,7 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] history 1`] exports[`regression tests > key 5 selects arrow tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key 5 selects arrow tool > [end of test] number of renders 1`] = `10`; +exports[`regression tests > key 5 selects arrow tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key 6 selects line tool > [end of test] appState 1`] = ` { @@ -10306,7 +10778,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -10315,7 +10787,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -10328,7 +10800,7 @@ exports[`regression tests > key 6 selects line tool > [end of test] history 1`] exports[`regression tests > key 6 selects line tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key 6 selects line tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key 6 selects line tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key 7 selects freedraw tool > [end of test] appState 1`] = ` { @@ -10490,7 +10962,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] history ], "roughness": 1, "roundness": null, - "seed": 337897, + "seed": 1278240551, "simulatePressure": false, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -10498,7 +10970,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] history "type": "freedraw", "updated": 1, "version": 4, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 10, "y": 10, @@ -10511,7 +10983,7 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] history exports[`regression tests > key 7 selects freedraw tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key 7 selects freedraw tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key 7 selects freedraw tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key a selects arrow tool > [end of test] appState 1`] = ` { @@ -10692,7 +11164,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -10701,7 +11173,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] history 1`] "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -10714,7 +11186,7 @@ exports[`regression tests > key a selects arrow tool > [end of test] history 1`] exports[`regression tests > key a selects arrow tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key a selects arrow tool > [end of test] number of renders 1`] = `10`; +exports[`regression tests > key a selects arrow tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key d selects diamond tool > [end of test] appState 1`] = ` { @@ -10859,14 +11331,14 @@ exports[`regression tests > key d selects diamond tool > [end of test] history 1 "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -10879,7 +11351,7 @@ exports[`regression tests > key d selects diamond tool > [end of test] history 1 exports[`regression tests > key d selects diamond tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key d selects diamond tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key d selects diamond tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key l selects line tool > [end of test] appState 1`] = ` { @@ -11060,7 +11532,7 @@ exports[`regression tests > key l selects line tool > [end of test] history 1`] "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -11069,7 +11541,7 @@ exports[`regression tests > key l selects line tool > [end of test] history 1`] "type": "line", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 10, "x": 10, "y": 10, @@ -11082,7 +11554,7 @@ exports[`regression tests > key l selects line tool > [end of test] history 1`] exports[`regression tests > key l selects line tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key l selects line tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key l selects line tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key o selects ellipse tool > [end of test] appState 1`] = ` { @@ -11227,14 +11699,14 @@ exports[`regression tests > key o selects ellipse tool > [end of test] history 1 "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -11247,7 +11719,7 @@ exports[`regression tests > key o selects ellipse tool > [end of test] history 1 exports[`regression tests > key o selects ellipse tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key o selects ellipse tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key o selects ellipse tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key p selects freedraw tool > [end of test] appState 1`] = ` { @@ -11409,7 +11881,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] history ], "roughness": 1, "roundness": null, - "seed": 337897, + "seed": 1278240551, "simulatePressure": false, "strokeColor": "#1e1e1e", "strokeStyle": "solid", @@ -11417,7 +11889,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] history "type": "freedraw", "updated": 1, "version": 4, - "versionNonce": 453191, + "versionNonce": 1150084233, "width": 10, "x": 10, "y": 10, @@ -11430,7 +11902,7 @@ exports[`regression tests > key p selects freedraw tool > [end of test] history exports[`regression tests > key p selects freedraw tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key p selects freedraw tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key p selects freedraw tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > key r selects rectangle tool > [end of test] appState 1`] = ` { @@ -11575,14 +12047,14 @@ exports[`regression tests > key r selects rectangle tool > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -11595,7 +12067,7 @@ exports[`regression tests > key r selects rectangle tool > [end of test] history exports[`regression tests > key r selects rectangle tool > [end of test] number of elements 1`] = `0`; -exports[`regression tests > key r selects rectangle tool > [end of test] number of renders 1`] = `9`; +exports[`regression tests > key r selects rectangle tool > [end of test] number of renders 1`] = `7`; exports[`regression tests > make a group and duplicate it > [end of test] appState 1`] = ` { @@ -11748,14 +12220,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -11791,14 +12263,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -11820,14 +12292,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -11863,14 +12335,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -11892,14 +12364,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -11921,14 +12393,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 10, "x": 50, "y": 10, @@ -11970,14 +12442,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 10, "x": 10, "y": 10, @@ -12001,14 +12473,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 10, "x": 30, "y": 10, @@ -12032,14 +12504,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 747212839, "width": 10, "x": 50, "y": 10, @@ -12081,14 +12553,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 915032327, + "seed": 941653321, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 81784553, + "versionNonce": 908564423, "width": 10, "x": 10, "y": 10, @@ -12112,14 +12584,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 747212839, + "seed": 1402203177, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 1723083209, + "versionNonce": 1359939303, "width": 10, "x": 30, "y": 10, @@ -12143,14 +12615,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 760410951, + "seed": 1349943049, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 5, - "versionNonce": 1006504105, + "versionNonce": 2004587015, "width": 10, "x": 50, "y": 10, @@ -12174,14 +12646,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1505387817, + "versionNonce": 1006504105, "width": 10, "x": 20, "y": 20, @@ -12205,14 +12677,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 1315507081, "width": 10, "x": 40, "y": 20, @@ -12236,14 +12708,14 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 493213705, + "versionNonce": 640725609, "width": 10, "x": 60, "y": 20, @@ -12256,7 +12728,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor exports[`regression tests > make a group and duplicate it > [end of test] number of elements 1`] = `0`; -exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `24`; +exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `16`; exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] appState 1`] = ` { @@ -12403,14 +12875,14 @@ exports[`regression tests > noop interaction after undo shouldn't create history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -12446,14 +12918,14 @@ exports[`regression tests > noop interaction after undo shouldn't create history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -12475,14 +12947,14 @@ exports[`regression tests > noop interaction after undo shouldn't create history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -12495,7 +12967,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of elements 1`] = `0`; -exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `21`; +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `13`; exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` { @@ -12615,7 +13087,7 @@ exports[`regression tests > pinch-to-zoom works > [end of test] history 1`] = ` exports[`regression tests > pinch-to-zoom works > [end of test] number of elements 1`] = `0`; -exports[`regression tests > pinch-to-zoom works > [end of test] number of renders 1`] = `11`; +exports[`regression tests > pinch-to-zoom works > [end of test] number of renders 1`] = `7`; exports[`regression tests > rerenders UI on language change > [end of test] appState 1`] = ` { @@ -12735,7 +13207,7 @@ exports[`regression tests > rerenders UI on language change > [end of test] hist exports[`regression tests > rerenders UI on language change > [end of test] number of elements 1`] = `0`; -exports[`regression tests > rerenders UI on language change > [end of test] number of renders 1`] = `11`; +exports[`regression tests > rerenders UI on language change > [end of test] number of renders 1`] = `4`; exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] appState 1`] = ` { @@ -12880,14 +13352,14 @@ exports[`regression tests > shift click on selected element should deselect it o "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -12900,7 +13372,7 @@ exports[`regression tests > shift click on selected element should deselect it o exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `11`; +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `7`; exports[`regression tests > shift-click to multiselect, then drag > [end of test] appState 1`] = ` { @@ -13049,14 +13521,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -13092,14 +13564,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -13121,14 +13593,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -13165,14 +13637,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 20, "y": 20, @@ -13194,14 +13666,14 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 493213705, "width": 10, "x": 40, "y": 20, @@ -13214,7 +13686,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of elements 1`] = `0`; -exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `20`; +exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `12`; exports[`regression tests > should group elements and ungroup them > [end of test] appState 1`] = ` { @@ -13365,14 +13837,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -13408,14 +13880,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -13437,14 +13909,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -13480,14 +13952,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 10, @@ -13509,14 +13981,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 30, "y": 10, @@ -13538,14 +14010,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 10, "x": 50, "y": 10, @@ -13587,14 +14059,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 10, "x": 10, "y": 10, @@ -13618,14 +14090,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 10, "x": 30, "y": 10, @@ -13649,14 +14121,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 400692809, + "versionNonce": 747212839, "width": 10, "x": 50, "y": 10, @@ -13694,14 +14166,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 23633383, + "versionNonce": 289600103, "width": 10, "x": 10, "y": 10, @@ -13723,14 +14195,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 493213705, + "versionNonce": 1315507081, "width": 10, "x": 30, "y": 10, @@ -13752,14 +14224,14 @@ exports[`regression tests > should group elements and ungroup them > [end of tes "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 915032327, + "versionNonce": 1898319239, "width": 10, "x": 50, "y": 10, @@ -13772,7 +14244,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes exports[`regression tests > should group elements and ungroup them > [end of test] number of elements 1`] = `0`; -exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `25`; +exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `15`; exports[`regression tests > should show fill icons when element has non transparent background > [end of test] appState 1`] = ` { @@ -13917,14 +14389,14 @@ exports[`regression tests > should show fill icons when element has non transpar "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -13960,14 +14432,14 @@ exports[`regression tests > should show fill icons when element has non transpar "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 2019559783, "width": 10, "x": 0, "y": 0, @@ -13980,7 +14452,7 @@ exports[`regression tests > should show fill icons when element has non transpar exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of elements 1`] = `0`; -exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `13`; +exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `9`; exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = ` { @@ -14135,14 +14607,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 0, @@ -14178,14 +14650,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 10, "y": 0, @@ -14207,14 +14679,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 50, "y": 0, @@ -14255,14 +14727,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 0, @@ -14286,14 +14758,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 50, "y": 0, @@ -14331,14 +14803,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 0, @@ -14362,14 +14834,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 50, "y": 0, @@ -14391,14 +14863,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 10, "x": 10, "y": 50, @@ -14436,14 +14908,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 0, @@ -14467,14 +14939,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 50, "y": 0, @@ -14496,14 +14968,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 238820263, + "versionNonce": 81784553, "width": 10, "x": 10, "y": 50, @@ -14525,14 +14997,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1604849351, + "versionNonce": 1006504105, "width": 10, "x": 50, "y": 50, @@ -14573,14 +15045,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 1604849351, "width": 10, "x": 10, "y": 0, @@ -14604,14 +15076,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 1505387817, "width": 10, "x": 50, "y": 0, @@ -14635,14 +15107,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 493213705, + "versionNonce": 640725609, "width": 10, "x": 10, "y": 50, @@ -14666,14 +15138,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 915032327, + "versionNonce": 406373543, "width": 10, "x": 50, "y": 50, @@ -14717,14 +15189,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 81784553, + "versionNonce": 908564423, "width": 10, "x": 10, "y": 0, @@ -14749,14 +15221,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 747212839, + "versionNonce": 1402203177, "width": 10, "x": 50, "y": 0, @@ -14781,14 +15253,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 1014066025, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1723083209, + "versionNonce": 1359939303, "width": 10, "x": 10, "y": 50, @@ -14813,14 +15285,14 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh "roundness": { "type": 3, }, - "seed": 400692809, + "seed": 1723083209, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 760410951, + "versionNonce": 1349943049, "width": 10, "x": 50, "y": 50, @@ -14833,7 +15305,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `39`; +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `19`; exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] appState 1`] = ` { @@ -14953,7 +15425,7 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] h exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] number of elements 1`] = `0`; -exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] number of renders 1`] = `8`; +exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] number of renders 1`] = `5`; exports[`regression tests > supports nested groups > [end of test] appState 1`] = ` { @@ -15100,14 +15572,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 50, "x": 0, "y": 0, @@ -15143,14 +15615,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 50, "x": 0, "y": 0, @@ -15172,14 +15644,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 50, "x": 100, "y": 100, @@ -15215,14 +15687,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 50, "x": 0, "y": 0, @@ -15244,14 +15716,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 50, "x": 100, "y": 100, @@ -15273,14 +15745,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 50, "x": 200, "y": 200, @@ -15322,14 +15794,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 23633383, "width": 50, "x": 0, "y": 0, @@ -15353,14 +15825,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 50, "x": 100, "y": 100, @@ -15384,14 +15856,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 50, "x": 200, "y": 200, @@ -15430,14 +15902,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1150084233, + "versionNonce": 23633383, "width": 50, "x": 0, "y": 0, @@ -15461,14 +15933,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 50, "x": 100, "y": 100, @@ -15492,14 +15964,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1014066025, + "versionNonce": 915032327, "width": 50, "x": 200, "y": 200, @@ -15540,14 +16012,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 50, "x": 100, "y": 100, @@ -15572,14 +16044,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 400692809, + "versionNonce": 1723083209, "width": 50, "x": 0, "y": 0, @@ -15604,14 +16076,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1604849351, + "versionNonce": 760410951, "width": 50, "x": 200, "y": 200, @@ -15653,14 +16125,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 3, - "versionNonce": 1116226695, + "versionNonce": 493213705, "width": 50, "x": 100, "y": 100, @@ -15685,14 +16157,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 400692809, + "versionNonce": 1723083209, "width": 50, "x": 0, "y": 0, @@ -15717,14 +16189,14 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = "roundness": { "type": 3, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 4, - "versionNonce": 1604849351, + "versionNonce": 760410951, "width": 50, "x": 200, "y": 200, @@ -15737,7 +16209,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = exports[`regression tests > supports nested groups > [end of test] number of elements 1`] = `0`; -exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `32`; +exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `15`; exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] appState 1`] = ` { @@ -15783,7 +16255,7 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 1116226695, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -15866,7 +16338,7 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 1116226695, + "seed": 493213705, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -15941,14 +16413,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -15984,14 +16456,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -16013,14 +16485,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -16056,14 +16528,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -16085,14 +16557,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 100, "x": 110, "y": 110, @@ -16114,14 +16586,14 @@ exports[`regression tests > switches from group of selected elements to another "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 2019559783, + "versionNonce": 1604849351, "width": 100, "x": 310, "y": 310, @@ -16134,7 +16606,7 @@ exports[`regression tests > switches from group of selected elements to another exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of elements 1`] = `0`; -exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `20`; +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `13`; exports[`regression tests > switches selected element on pointer down > [end of test] appState 1`] = ` { @@ -16180,7 +16652,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -16262,7 +16734,7 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, @@ -16337,14 +16809,14 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -16380,14 +16852,14 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 10, "x": 0, "y": 0, @@ -16409,14 +16881,14 @@ exports[`regression tests > switches selected element on pointer down > [end of "roundness": { "type": 2, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 10, "x": 20, "y": 20, @@ -16429,7 +16901,7 @@ exports[`regression tests > switches selected element on pointer down > [end of exports[`regression tests > switches selected element on pointer down > [end of test] number of elements 1`] = `0`; -exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `14`; +exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `10`; exports[`regression tests > two-finger scroll works > [end of test] appState 1`] = ` { @@ -16549,7 +17021,7 @@ exports[`regression tests > two-finger scroll works > [end of test] history 1`] exports[`regression tests > two-finger scroll works > [end of test] number of elements 1`] = `0`; -exports[`regression tests > two-finger scroll works > [end of test] number of renders 1`] = `13`; +exports[`regression tests > two-finger scroll works > [end of test] number of renders 1`] = `8`; exports[`regression tests > undo/redo drawing an element > [end of test] appState 1`] = ` { @@ -16682,14 +17154,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -16711,14 +17183,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 30, "x": 40, "y": 0, @@ -16760,7 +17232,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -16769,7 +17241,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "type": "arrow", "updated": 1, "version": 7, - "versionNonce": 400692809, + "versionNonce": 1006504105, "width": 100, "x": 130, "y": 10, @@ -16805,14 +17277,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -16834,14 +17306,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 30, "x": 40, "y": 0, @@ -16879,7 +17351,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 2, }, - "seed": 401146281, + "seed": 238820263, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -16888,7 +17360,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "type": "arrow", "updated": 1, "version": 5, - "versionNonce": 1014066025, + "versionNonce": 81784553, "width": 60, "x": 130, "y": 10, @@ -16937,14 +17409,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -16980,14 +17452,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 20, "x": 10, "y": -10, @@ -17009,14 +17481,14 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history "roundness": { "type": 3, }, - "seed": 449462985, + "seed": 2019559783, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 453191, + "versionNonce": 1116226695, "width": 30, "x": 40, "y": 0, @@ -17029,7 +17501,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `31`; +exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `21`; exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = ` { @@ -17149,7 +17621,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of elements 1`] = `0`; -exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `7`; +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `5`; exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` { @@ -17269,4 +17741,4 @@ exports[`regression tests > zoom hotkeys > [end of test] history 1`] = ` exports[`regression tests > zoom hotkeys > [end of test] number of elements 1`] = `0`; -exports[`regression tests > zoom hotkeys > [end of test] number of renders 1`] = `7`; +exports[`regression tests > zoom hotkeys > [end of test] number of renders 1`] = `6`; diff --git a/src/tests/__snapshots__/selection.test.tsx.snap b/src/tests/__snapshots__/selection.test.tsx.snap index 3296dca753..33635bca54 100644 --- a/src/tests/__snapshots__/selection.test.tsx.snap +++ b/src/tests/__snapshots__/selection.test.tsx.snap @@ -31,7 +31,7 @@ exports[`select single element on the scene > arrow 1`] = ` "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -40,7 +40,7 @@ exports[`select single element on the scene > arrow 1`] = ` "type": "arrow", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 30, "x": 10, "y": 10, @@ -78,7 +78,7 @@ exports[`select single element on the scene > arrow escape 1`] = ` "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "startArrowhead": null, "startBinding": null, "strokeColor": "#1e1e1e", @@ -87,7 +87,7 @@ exports[`select single element on the scene > arrow escape 1`] = ` "type": "line", "updated": 1, "version": 3, - "versionNonce": 449462985, + "versionNonce": 401146281, "width": 30, "x": 10, "y": 10, @@ -112,14 +112,14 @@ exports[`select single element on the scene > diamond 1`] = ` "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "diamond", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 30, "x": 10, "y": 10, @@ -144,14 +144,14 @@ exports[`select single element on the scene > ellipse 1`] = ` "roundness": { "type": 2, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "ellipse", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 30, "x": 10, "y": 10, @@ -176,14 +176,14 @@ exports[`select single element on the scene > rectangle 1`] = ` "roundness": { "type": 3, }, - "seed": 337897, + "seed": 1278240551, "strokeColor": "#1e1e1e", "strokeStyle": "solid", "strokeWidth": 1, "type": "rectangle", "updated": 1, "version": 2, - "versionNonce": 1278240551, + "versionNonce": 453191, "width": 30, "x": 10, "y": 10, diff --git a/src/tests/contextmenu.test.tsx b/src/tests/contextmenu.test.tsx index c581a0ff13..1474c52247 100644 --- a/src/tests/contextmenu.test.tsx +++ b/src/tests/contextmenu.test.tsx @@ -24,7 +24,7 @@ import { LibraryItem } from "../types"; import { vi } from "vitest"; const checkpoint = (name: string) => { - expect(renderScene.mock.calls.length).toMatchSnapshot( + expect(renderStaticScene.mock.calls.length).toMatchSnapshot( `[${name}] number of renders`, ); expect(h.state).toMatchSnapshot(`[${name}] appState`); @@ -40,10 +40,10 @@ const mouse = new Pointer("mouse"); // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); beforeEach(() => { localStorage.clear(); - renderScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); @@ -52,7 +52,7 @@ const { h } = window; describe("contextMenu element", () => { beforeEach(async () => { localStorage.clear(); - renderScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); setDateTimeForTests("201933152653"); @@ -75,7 +75,7 @@ describe("contextMenu element", () => { }); it("shows context menu for canvas", () => { - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -105,7 +105,7 @@ describe("contextMenu element", () => { mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -159,7 +159,7 @@ describe("contextMenu element", () => { API.setSelectedElements([rect1]); // lower z-index - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 100, clientY: 100, @@ -169,7 +169,7 @@ describe("contextMenu element", () => { // higher z-index API.setSelectedElements([rect2]); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 100, clientY: 100, @@ -193,7 +193,7 @@ describe("contextMenu element", () => { mouse.click(20, 0); }); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -246,7 +246,7 @@ describe("contextMenu element", () => { Keyboard.keyPress(KEYS.G); }); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -285,7 +285,7 @@ describe("contextMenu element", () => { mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -333,7 +333,7 @@ describe("contextMenu element", () => { mouse.reset(); // Copy styles of second rectangle - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 40, clientY: 40, @@ -346,7 +346,7 @@ describe("contextMenu element", () => { mouse.reset(); // Paste styles to first rectangle - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 10, clientY: 10, @@ -370,7 +370,7 @@ describe("contextMenu element", () => { mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -386,7 +386,7 @@ describe("contextMenu element", () => { mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -407,7 +407,7 @@ describe("contextMenu element", () => { mouse.down(10, 10); mouse.up(20, 20); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -430,7 +430,7 @@ describe("contextMenu element", () => { mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 40, clientY: 40, @@ -452,7 +452,7 @@ describe("contextMenu element", () => { mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 10, clientY: 10, @@ -474,7 +474,7 @@ describe("contextMenu element", () => { mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 40, clientY: 40, @@ -495,7 +495,7 @@ describe("contextMenu element", () => { mouse.up(20, 20); mouse.reset(); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 10, clientY: 10, @@ -520,7 +520,7 @@ describe("contextMenu element", () => { mouse.click(10, 10); }); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -550,7 +550,7 @@ describe("contextMenu element", () => { Keyboard.keyPress(KEYS.G); }); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx index 3e01af04f9..2f0b0a27d9 100644 --- a/src/tests/dragCreate.test.tsx +++ b/src/tests/dragCreate.test.tsx @@ -15,10 +15,13 @@ import { vi } from "vitest"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); + beforeEach(() => { localStorage.clear(); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); @@ -32,7 +35,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("rectangle"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -43,7 +46,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -63,7 +67,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("ellipse"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -74,7 +78,9 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -94,7 +100,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("diamond"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -105,7 +111,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -125,7 +132,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("arrow"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -136,7 +143,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -160,7 +168,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("line"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -171,7 +179,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -203,7 +212,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("rectangle"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -211,7 +220,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -222,7 +232,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("ellipse"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -230,7 +240,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -241,7 +252,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("diamond"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -249,7 +260,8 @@ describe("Test dragCreate", () => { // finish (position does not matter) fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -260,7 +272,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("arrow"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -273,7 +285,8 @@ describe("Test dragCreate", () => { key: KEYS.ENTER, }); - expect(renderScene).toHaveBeenCalledTimes(8); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -284,7 +297,7 @@ describe("Test dragCreate", () => { const tool = getByToolName("line"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // start from (30, 20) fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); @@ -297,7 +310,8 @@ describe("Test dragCreate", () => { key: KEYS.ENTER, }); - expect(renderScene).toHaveBeenCalledTimes(8); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index 824a22956e..46361cf381 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -279,7 +279,7 @@ export class API { }; static drop = async (blob: Blob) => { - const fileDropEvent = createEvent.drop(GlobalTestState.canvas); + const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas); const text = await new Promise((resolve, reject) => { try { const reader = new FileReader(); @@ -306,6 +306,6 @@ export class API { }, }, }); - fireEvent(GlobalTestState.canvas, fileDropEvent); + fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent); }; } diff --git a/src/tests/helpers/ui.ts b/src/tests/helpers/ui.ts index c882011330..f815b07dc8 100644 --- a/src/tests/helpers/ui.ts +++ b/src/tests/helpers/ui.ts @@ -107,7 +107,7 @@ export class Pointer { restorePosition(x = 0, y = 0) { this.clientX = x; this.clientY = y; - fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); } private getEvent() { @@ -129,18 +129,18 @@ export class Pointer { if (dx !== 0 || dy !== 0) { this.clientX += dx; this.clientY += dy; - fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); } } down(dx = 0, dy = 0) { this.move(dx, dy); - fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent()); } up(dx = 0, dy = 0) { this.move(dx, dy); - fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent()); } click(dx = 0, dy = 0) { @@ -150,7 +150,7 @@ export class Pointer { doubleClick(dx = 0, dy = 0) { this.move(dx, dy); - fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent()); + fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent()); } // absolute coords @@ -159,19 +159,19 @@ export class Pointer { moveTo(x: number = this.clientX, y: number = this.clientY) { this.clientX = x; this.clientY = y; - fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent()); } downAt(x = this.clientX, y = this.clientY) { this.clientX = x; this.clientY = y; - fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent()); } upAt(x = this.clientX, y = this.clientY) { this.clientX = x; this.clientY = y; - fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent()); + fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent()); } clickAt(x: number, y: number) { @@ -180,7 +180,7 @@ export class Pointer { } rightClickAt(x: number, y: number) { - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: x, clientY: y, @@ -189,7 +189,7 @@ export class Pointer { doubleClickAt(x: number, y: number) { this.moveTo(x, y); - fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent()); + fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent()); } // --------------------------------------------------------------------------- @@ -327,6 +327,13 @@ export class UI { }); } + static ungroup(elements: ExcalidrawElement[]) { + mouse.select(elements); + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.G); + }); + } + static queryContextMenu = () => { return GlobalTestState.renderResult.container.querySelector( ".context-menu", diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index bcca0b1e1c..92e8749181 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -26,26 +26,28 @@ import * as textElementUtils from "../element/textElement"; import { ROUNDNESS, VERTICAL_ALIGN } from "../constants"; import { vi } from "vitest"; -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); const { h } = window; const font = "20px Cascadia, width: Segoe UI Emoji" as FontString; describe("Test Linear Elements", () => { let container: HTMLElement; - let canvas: HTMLCanvasElement; + let interactiveCanvas: HTMLCanvasElement; beforeEach(async () => { // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); localStorage.clear(); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); const comp = await render(); + h.state.width = 1000; + h.state.height = 1000; container = comp.container; - canvas = container.querySelector("canvas")!; - canvas.width = 1000; - canvas.height = 1000; + interactiveCanvas = container.querySelector("canvas.interactive")!; }); const p1: Point = [20, 20]; @@ -120,26 +122,26 @@ describe("Test Linear Elements", () => { }; const drag = (startPoint: Point, endPoint: Point) => { - fireEvent.pointerDown(canvas, { + fireEvent.pointerDown(interactiveCanvas, { clientX: startPoint[0], clientY: startPoint[1], }); - fireEvent.pointerMove(canvas, { + fireEvent.pointerMove(interactiveCanvas, { clientX: endPoint[0], clientY: endPoint[1], }); - fireEvent.pointerUp(canvas, { + fireEvent.pointerUp(interactiveCanvas, { clientX: endPoint[0], clientY: endPoint[1], }); }; const deletePoint = (point: Point) => { - fireEvent.pointerDown(canvas, { + fireEvent.pointerDown(interactiveCanvas, { clientX: point[0], clientY: point[1], }); - fireEvent.pointerUp(canvas, { + fireEvent.pointerUp(interactiveCanvas, { clientX: point[0], clientY: point[1], }); @@ -172,12 +174,14 @@ describe("Test Linear Elements", () => { createTwoPointerLinearElement("line"); const line = h.elements[0] as ExcalidrawLinearElement; - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(4); expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); // drag line from midpoint drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); - expect(renderScene).toHaveBeenCalledTimes(11); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(line.points.length).toEqual(3); expect(line.points).toMatchInlineSnapshot(` [ @@ -199,14 +203,14 @@ describe("Test Linear Elements", () => { it("should allow entering and exiting line editor via context menu", () => { createTwoPointerLinearElement("line"); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: midpoint[0], clientY: midpoint[1], }); // Enter line editor let contextMenu = document.querySelector(".context-menu"); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: midpoint[0], clientY: midpoint[1], @@ -216,13 +220,13 @@ describe("Test Linear Elements", () => { expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id); // Exiting line editor - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: midpoint[0], clientY: midpoint[1], }); contextMenu = document.querySelector(".context-menu"); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: midpoint[0], clientY: midpoint[1], @@ -270,7 +274,8 @@ describe("Test Linear Elements", () => { // drag line from midpoint drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); - expect(renderScene).toHaveBeenCalledTimes(15); + expect(renderInteractiveScene).toHaveBeenCalledTimes(14); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(line.points.length).toEqual(3); expect(line.points).toMatchInlineSnapshot(` @@ -307,7 +312,9 @@ describe("Test Linear Elements", () => { // update roundness fireEvent.click(screen.getByTitle("Round")); - expect(renderScene).toHaveBeenCalledTimes(12); + expect(renderInteractiveScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(6); + const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints( h.elements[0] as ExcalidrawLinearElement, h.state, @@ -351,7 +358,9 @@ describe("Test Linear Elements", () => { // Move the element drag(startPoint, endPoint); - expect(renderScene).toHaveBeenCalledTimes(16); + expect(renderInteractiveScene).toHaveBeenCalledTimes(14); + expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect([line.x, line.y]).toEqual([ points[0][0] + deltaX, points[0][1] + deltaY, @@ -408,7 +417,9 @@ describe("Test Linear Elements", () => { lastSegmentMidpoint[1] + delta, ]); - expect(renderScene).toHaveBeenCalledTimes(21); + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); + expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(line.points.length).toEqual(5); expect((h.elements[0] as ExcalidrawLinearElement).points) @@ -447,7 +458,8 @@ describe("Test Linear Elements", () => { // Drag from first point drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); - expect(renderScene).toHaveBeenCalledTimes(16); + expect(renderInteractiveScene).toHaveBeenCalledTimes(14); + expect(renderStaticScene).toHaveBeenCalledTimes(6); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ @@ -473,7 +485,8 @@ describe("Test Linear Elements", () => { // Drag from first point drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]); - expect(renderScene).toHaveBeenCalledTimes(16); + expect(renderInteractiveScene).toHaveBeenCalledTimes(14); + expect(renderStaticScene).toHaveBeenCalledTimes(6); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ @@ -507,7 +520,8 @@ describe("Test Linear Elements", () => { // delete 3rd point deletePoint(points[2]); expect(line.points.length).toEqual(3); - expect(renderScene).toHaveBeenCalledTimes(22); + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); + expect(renderStaticScene).toHaveBeenCalledTimes(7); const newMidPoints = LinearElementEditor.getEditorMidPoints( line, @@ -553,8 +567,8 @@ describe("Test Linear Elements", () => { lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[1] + delta, ]); - expect(renderScene).toHaveBeenCalledTimes(21); - + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(line.points.length).toEqual(5); expect((h.elements[0] as ExcalidrawLinearElement).points) @@ -629,7 +643,8 @@ describe("Test Linear Elements", () => { // Drag from first point drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]); - expect(renderScene).toHaveBeenCalledTimes(16); + expect(renderInteractiveScene).toHaveBeenCalledTimes(14); + expect(renderStaticScene).toHaveBeenCalledTimes(6); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ @@ -870,10 +885,10 @@ describe("Test Linear Elements", () => { ]); expect((h.elements[1] as ExcalidrawTextElementWithContainer).text) .toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + "Online whiteboard + collaboration made + easy" + `); }); it("should bind text to arrow when clicked on arrow and enter pressed", async () => { @@ -904,10 +919,10 @@ describe("Test Linear Elements", () => { ]); expect((h.elements[1] as ExcalidrawTextElementWithContainer).text) .toMatchInlineSnapshot(` - "Online whiteboard - collaboration made - easy" - `); + "Online whiteboard + collaboration made + easy" + `); }); it("should not bind text to line when double clicked", async () => { @@ -1046,9 +1061,9 @@ describe("Test Linear Elements", () => { `); expect((h.elements[1] as ExcalidrawTextElementWithContainer).text) .toMatchInlineSnapshot(` - "Online whiteboard - collaboration made easy" - `); + "Online whiteboard + collaboration made easy" + `); expect(LinearElementEditor.getElementAbsoluteCoords(container, true)) .toMatchInlineSnapshot(` [ @@ -1206,7 +1221,7 @@ describe("Test Linear Elements", () => { const container = h.elements[0]; API.setSelectedElements([container, text]); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, @@ -1231,7 +1246,7 @@ describe("Test Linear Elements", () => { mouse.up(); API.setSelectedElements([h.elements[0], h.elements[1]]); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 20, clientY: 30, diff --git a/src/tests/move.test.tsx b/src/tests/move.test.tsx index 034a1fff78..e3a9b69b89 100644 --- a/src/tests/move.test.tsx +++ b/src/tests/move.test.tsx @@ -17,10 +17,13 @@ import { vi } from "vitest"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); + beforeEach(() => { localStorage.clear(); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); @@ -29,7 +32,7 @@ const { h } = window; describe("move element", () => { it("rectangle", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element @@ -39,20 +42,23 @@ describe("move element", () => { fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); } fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 }); fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(3); + expect(renderInteractiveScene).toHaveBeenCalledTimes(3); + expect(renderStaticScene).toHaveBeenCalledTimes(2); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]); @@ -78,7 +84,8 @@ describe("move element", () => { // select the second rectangles new Pointer("mouse").clickOn(rectB); - expect(renderScene).toHaveBeenCalledTimes(23); + expect(renderInteractiveScene).toHaveBeenCalledTimes(21); + expect(renderStaticScene).toHaveBeenCalledTimes(16); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(3); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); @@ -87,7 +94,8 @@ describe("move element", () => { expect([line.x, line.y]).toEqual([110, 50]); expect([line.width, line.height]).toEqual([80, 80]); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); // Move selected rectangle Keyboard.keyDown(KEYS.ARROW_RIGHT); @@ -95,7 +103,8 @@ describe("move element", () => { Keyboard.keyDown(KEYS.ARROW_DOWN); // Check that the arrow size has been changed according to moving the rectangle - expect(renderScene).toHaveBeenCalledTimes(3); + expect(renderInteractiveScene).toHaveBeenCalledTimes(3); + expect(renderStaticScene).toHaveBeenCalledTimes(3); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(3); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); @@ -111,7 +120,7 @@ describe("move element", () => { describe("duplicate element on move when ALT is clicked", () => { it("rectangle", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element @@ -121,13 +130,15 @@ describe("duplicate element on move when ALT is clicked", () => { fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(9); + expect(renderInteractiveScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); } fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 }); @@ -141,7 +152,8 @@ describe("duplicate element on move when ALT is clicked", () => { // TODO: This used to be 4, but binding made it go up to 5. Do we need // that additional render? - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(3); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(2); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx index 7037138fbb..a5277088d7 100644 --- a/src/tests/multiPointCreate.test.tsx +++ b/src/tests/multiPointCreate.test.tsx @@ -15,10 +15,13 @@ import { vi } from "vitest"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); + beforeEach(() => { localStorage.clear(); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); @@ -39,11 +42,12 @@ describe("remove shape in non linear elements", () => { const tool = getByToolName("rectangle"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.elements.length).toEqual(0); }); @@ -53,11 +57,12 @@ describe("remove shape in non linear elements", () => { const tool = getByToolName("ellipse"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.elements.length).toEqual(0); }); @@ -67,11 +72,12 @@ describe("remove shape in non linear elements", () => { const tool = getByToolName("diamond"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 }); fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect(h.elements.length).toEqual(0); }); }); @@ -83,7 +89,7 @@ describe("multi point mode in linear elements", () => { const tool = getByToolName("arrow"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // first point is added on pointer down fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 }); @@ -103,7 +109,8 @@ describe("multi point mode in linear elements", () => { key: KEYS.ENTER, }); - expect(renderScene).toHaveBeenCalledTimes(15); + expect(renderInteractiveScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(10); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; @@ -126,7 +133,7 @@ describe("multi point mode in linear elements", () => { const tool = getByToolName("line"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; // first point is added on pointer down fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 }); @@ -146,7 +153,8 @@ describe("multi point mode in linear elements", () => { key: KEYS.ENTER, }); - expect(renderScene).toHaveBeenCalledTimes(15); + expect(renderInteractiveScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(10); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; diff --git a/src/tests/packages/excalidraw.test.tsx b/src/tests/packages/excalidraw.test.tsx index 20852a2ada..02ea208e81 100644 --- a/src/tests/packages/excalidraw.test.tsx +++ b/src/tests/packages/excalidraw.test.tsx @@ -1,6 +1,6 @@ import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils"; import { Excalidraw, Footer, MainMenu } from "../../packages/excalidraw/index"; -import { queryByText, queryByTestId, screen } from "@testing-library/react"; +import { queryByText, queryByTestId } from "@testing-library/react"; import { GRID_SIZE, THEME } from "../../constants"; import { t } from "../../i18n"; import { useMemo } from "react"; @@ -23,7 +23,7 @@ describe("", () => { ).toBe(0); expect(h.state.zenModeEnabled).toBe(false); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -42,8 +42,8 @@ describe("", () => { container.getElementsByClassName("disable-zen-mode--visible").length, ).toBe(0); expect(h.state.zenModeEnabled).toBe(true); - screen.debug(); - fireEvent.contextMenu(GlobalTestState.canvas, { + + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -95,7 +95,7 @@ describe("", () => { expect( container.getElementsByClassName("disable-zen-mode--visible").length, ).toBe(0); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, @@ -114,7 +114,7 @@ describe("", () => { expect( container.getElementsByClassName("disable-zen-mode--visible").length, ).toBe(0); - fireEvent.contextMenu(GlobalTestState.canvas, { + fireEvent.contextMenu(GlobalTestState.interactiveCanvas, { button: 2, clientX: 1, clientY: 1, diff --git a/src/tests/regressionTests.test.tsx b/src/tests/regressionTests.test.tsx index 8cb483c932..47fa9208b2 100644 --- a/src/tests/regressionTests.test.tsx +++ b/src/tests/regressionTests.test.tsx @@ -21,7 +21,7 @@ import { vi } from "vitest"; const { h } = window; -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); const mouse = new Pointer("mouse"); const finger1 = new Pointer("touch", 1); @@ -33,7 +33,7 @@ const finger2 = new Pointer("touch", 2); * to debug where a test failure came from. */ const checkpoint = (name: string) => { - expect(renderScene.mock.calls.length).toMatchSnapshot( + expect(renderStaticScene.mock.calls.length).toMatchSnapshot( `[${name}] number of renders`, ); expect(h.state).toMatchSnapshot(`[${name}] appState`); @@ -48,7 +48,7 @@ beforeEach(async () => { ReactDOM.unmountComponentAtNode(document.getElementById("root")!); localStorage.clear(); - renderScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); setDateTimeForTests("201933152653"); @@ -1056,6 +1056,28 @@ describe("regression tests", () => { expect(API.getSelectedElements()).toEqual(selectedElements_prev); }); + it("deleting last but one element in editing group should unselect the group", () => { + const rect1 = UI.createElement("rectangle", { x: 10 }); + const rect2 = UI.createElement("rectangle", { x: 50 }); + + UI.group([rect1, rect2]); + + mouse.doubleClickOn(rect1); + Keyboard.keyDown(KEYS.DELETE); + + // Clicking on the deleted element, hence in the empty space + mouse.clickOn(rect1); + + expect(h.state.selectedGroupIds).toEqual({}); + expect(API.getSelectedElements()).toEqual([]); + + // Clicking back in and expecting no group selection + mouse.clickOn(rect2); + + expect(h.state.selectedGroupIds).toEqual({ [rect2.groupIds[0]]: false }); + expect(API.getSelectedElements()).toEqual([rect2.get()]); + }); + it("Cmd/Ctrl-click exclusively select element under pointer", () => { const rect1 = UI.createElement("rectangle", { x: 0 }); const rect2 = UI.createElement("rectangle", { x: 30 }); diff --git a/src/tests/resize.test.tsx b/src/tests/resize.test.tsx index 13f757a98a..fdecf8fb6c 100644 --- a/src/tests/resize.test.tsx +++ b/src/tests/resize.test.tsx @@ -14,10 +14,11 @@ import { vi } from "vitest"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); + beforeEach(() => { localStorage.clear(); - renderScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index aa8b64a8af..8df93351d7 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -18,10 +18,13 @@ import { vi } from "vitest"; // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); -const renderScene = vi.spyOn(Renderer, "renderScene"); +const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene"); +const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene"); + beforeEach(() => { localStorage.clear(); - renderScene.mockClear(); + renderInteractiveScene.mockClear(); + renderStaticScene.mockClear(); reseed(7); }); @@ -201,7 +204,7 @@ describe("inner box-selection", () => { }); h.elements = [rect1, rect2, rect3]; Keyboard.withModifierKeys({ ctrl: true }, () => { - mouse.downAt(rect2.x - 20, rect2.x - 20); + mouse.downAt(rect2.x - 20, rect2.y - 20); mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10); assertSelectedElements([rect2.id, rect3.id]); expect(h.state.selectedGroupIds).toEqual({ A: true }); @@ -220,10 +223,11 @@ describe("selection element", () => { const tool = getByToolName("selection"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); - expect(renderScene).toHaveBeenCalledTimes(5); + expect(renderInteractiveScene).toHaveBeenCalledTimes(3); + expect(renderStaticScene).toHaveBeenCalledTimes(3); const selectionElement = h.state.selectionElement!; expect(selectionElement).not.toBeNull(); expect(selectionElement.type).toEqual("selection"); @@ -240,11 +244,12 @@ describe("selection element", () => { const tool = getByToolName("selection"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); - expect(renderScene).toHaveBeenCalledTimes(6); + expect(renderInteractiveScene).toHaveBeenCalledTimes(4); + expect(renderStaticScene).toHaveBeenCalledTimes(3); const selectionElement = h.state.selectionElement!; expect(selectionElement).not.toBeNull(); expect(selectionElement.type).toEqual("selection"); @@ -261,12 +266,13 @@ describe("selection element", () => { const tool = getByToolName("selection"); fireEvent.click(tool); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(7); + expect(renderInteractiveScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(3); expect(h.state.selectionElement).toBeNull(); }); }); @@ -282,7 +288,7 @@ describe("select single element on the scene", () => { it("rectangle", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element const tool = getByToolName("rectangle"); @@ -301,7 +307,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(11); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -311,7 +318,7 @@ describe("select single element on the scene", () => { it("diamond", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element const tool = getByToolName("diamond"); @@ -330,7 +337,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(11); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -340,7 +348,7 @@ describe("select single element on the scene", () => { it("ellipse", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element const tool = getByToolName("ellipse"); @@ -359,7 +367,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(11); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -369,7 +378,7 @@ describe("select single element on the scene", () => { it("arrow", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element const tool = getByToolName("arrow"); @@ -401,7 +410,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(11); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -410,7 +420,7 @@ describe("select single element on the scene", () => { it("arrow escape", async () => { const { getByToolName, container } = await render(); - const canvas = container.querySelector("canvas")!; + const canvas = container.querySelector("canvas.interactive")!; { // create element const tool = getByToolName("line"); @@ -442,7 +452,8 @@ describe("select single element on the scene", () => { fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 }); fireEvent.pointerUp(canvas); - expect(renderScene).toHaveBeenCalledTimes(11); + expect(renderInteractiveScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/tests/test-utils.ts b/src/tests/test-utils.ts index 1855a7fec1..4a8a9f1f11 100644 --- a/src/tests/test-utils.ts +++ b/src/tests/test-utils.ts @@ -49,15 +49,30 @@ const renderApp: TestRenderFn = async (ui, options) => { // child App component isn't likely mounted yet (and thus canvas not // present in DOM) get() { - return renderResult.container.querySelector("canvas")!; + return renderResult.container.querySelector("canvas.static")!; + }, + }); + + Object.defineProperty(GlobalTestState, "interactiveCanvas", { + // must be a getter because at the time of ExcalidrawApp render the + // child App component isn't likely mounted yet (and thus canvas not + // present in DOM) + get() { + return renderResult.container.querySelector("canvas.interactive")!; }, }); await waitFor(() => { - const canvas = renderResult.container.querySelector("canvas"); + const canvas = renderResult.container.querySelector("canvas.static"); if (!canvas) { throw new Error("not initialized yet"); } + + const interactiveCanvas = + renderResult.container.querySelector("canvas.interactive"); + if (!interactiveCanvas) { + throw new Error("not initialized yet"); + } }); return renderResult; @@ -81,11 +96,17 @@ export class GlobalTestState { */ static renderResult: RenderResult = null!; /** - * retrieves canvas for currently rendered app instance + * retrieves static canvas for currently rendered app instance */ static get canvas(): HTMLCanvasElement { return null!; } + /** + * retrieves interactive canvas for currently rendered app instance + */ + static get interactiveCanvas(): HTMLCanvasElement { + return null!; + } } const initLocalStorage = (data: ImportedDataState) => { diff --git a/src/tests/viewMode.test.tsx b/src/tests/viewMode.test.tsx index 277b306eee..bc29f81c3e 100644 --- a/src/tests/viewMode.test.tsx +++ b/src/tests/viewMode.test.tsx @@ -17,7 +17,9 @@ describe("view mode", () => { it("after switching to view mode – cursor type should be pointer", async () => { h.setState({ viewModeEnabled: true }); - expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); }); it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => { @@ -29,7 +31,9 @@ describe("view mode", () => { pointer.move(100, 100); pointer.click(); Keyboard.keyPress(KEYS.SPACE); - expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); }); }); @@ -45,13 +49,19 @@ describe("view mode", () => { pointer.moveTo(50, 50); // eslint-disable-next-line dot-notation if (pointerType["pointerType"] === "mouse") { - expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.MOVE); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.MOVE, + ); } else { - expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); } h.setState({ viewModeEnabled: true }); - expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB); + expect(GlobalTestState.interactiveCanvas.style.cursor).toBe( + CURSOR_TYPE.GRAB, + ); }); }); }); diff --git a/src/tests/zindex.test.tsx b/src/tests/zindex.test.tsx index de421338b8..2047c032e2 100644 --- a/src/tests/zindex.test.tsx +++ b/src/tests/zindex.test.tsx @@ -94,7 +94,7 @@ const populateElements = ( ), ...appState, selectedElementIds, - }); + } as AppState); return selectedElementIds; }; diff --git a/src/types.ts b/src/types.ts index 052bf231ee..9eeaf72830 100644 --- a/src/types.ts +++ b/src/types.ts @@ -104,6 +104,52 @@ export type LastActiveTool = export type SidebarName = string; export type SidebarTabName = string; +export type CommonCanvasAppState = { + zoom: AppState["zoom"]; + scrollX: AppState["scrollX"]; + scrollY: AppState["scrollY"]; + width: AppState["width"]; + height: AppState["height"]; + viewModeEnabled: AppState["viewModeEnabled"]; + editingElement: AppState["editingElement"]; + editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible + selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible + frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible + offsetLeft: AppState["offsetLeft"]; + offsetTop: AppState["offsetTop"]; + theme: AppState["theme"]; + pendingImageElementId: AppState["pendingImageElementId"]; +}; + +export type StaticCanvasAppState = CommonCanvasAppState & { + shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"]; + /** null indicates transparent bg */ + viewBackgroundColor: AppState["viewBackgroundColor"] | null; + exportScale: AppState["exportScale"]; + selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"]; + gridSize: AppState["gridSize"]; + frameRendering: AppState["frameRendering"]; +}; + +export type InteractiveCanvasAppState = CommonCanvasAppState & { + // renderInteractiveScene + activeEmbeddable: AppState["activeEmbeddable"]; + editingLinearElement: AppState["editingLinearElement"]; + selectionElement: AppState["selectionElement"]; + selectedGroupIds: AppState["selectedGroupIds"]; + selectedLinearElement: AppState["selectedLinearElement"]; + multiElement: AppState["multiElement"]; + isBindingEnabled: AppState["isBindingEnabled"]; + suggestedBindings: AppState["suggestedBindings"]; + isRotating: AppState["isRotating"]; + elementsToHighlight: AppState["elementsToHighlight"]; + // App + openSidebar: AppState["openSidebar"]; + showHyperlinkPopup: AppState["showHyperlinkPopup"]; + // Collaborators + collaborators: AppState["collaborators"]; +}; + export type AppState = { contextMenu: { items: ContextMenuItems; @@ -407,13 +453,13 @@ export type ExportOpts = { exportedElements: readonly NonDeletedExcalidrawElement[], appState: UIAppState, files: BinaryFiles, - canvas: HTMLCanvasElement | null, + canvas: HTMLCanvasElement, ) => void; renderCustomUI?: ( exportedElements: readonly NonDeletedExcalidrawElement[], appState: UIAppState, files: BinaryFiles, - canvas: HTMLCanvasElement | null, + canvas: HTMLCanvasElement, ) => JSX.Element; }; @@ -458,7 +504,8 @@ export type AppProps = Merge< * in the app, eg Manager. Factored out into a separate type to keep DRY. */ export type AppClassProperties = { props: AppProps; - canvas: HTMLCanvasElement | null; + canvas: HTMLCanvasElement; + interactiveCanvas: HTMLCanvasElement | null; focusContainer(): void; library: Library; imageCache: Map< diff --git a/src/utils.ts b/src/utils.ts index 407fecd2e1..8b142744ab 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,6 +20,7 @@ import { unstable_batchedUpdates } from "react-dom"; import { SHAPES } from "./shapes"; import { isEraserActive, isHandToolActive } from "./appState"; import { ResolutionType } from "./utility-types"; +import React from "react"; let mockDateTime: string | null = null; @@ -399,22 +400,25 @@ export const updateActiveTool = ( }; }; -export const resetCursor = (canvas: HTMLCanvasElement | null) => { - if (canvas) { - canvas.style.cursor = ""; +export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = ""; } }; -export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => { - if (canvas) { - canvas.style.cursor = cursor; +export const setCursor = ( + interactiveCanvas: HTMLCanvasElement | null, + cursor: string, +) => { + if (interactiveCanvas) { + interactiveCanvas.style.cursor = cursor; } }; let eraserCanvasCache: any; let previewDataURL: string; export const setEraserCursor = ( - canvas: HTMLCanvasElement | null, + interactiveCanvas: HTMLCanvasElement | null, theme: AppState["theme"], ) => { const cursorImageSizePx = 20; @@ -446,7 +450,7 @@ export const setEraserCursor = ( } setCursor( - canvas, + interactiveCanvas, `url(${previewDataURL}) ${cursorImageSizePx / 2} ${ cursorImageSizePx / 2 }, auto`, @@ -454,23 +458,23 @@ export const setEraserCursor = ( }; export const setCursorForShape = ( - canvas: HTMLCanvasElement | null, + interactiveCanvas: HTMLCanvasElement | null, appState: Pick, ) => { - if (!canvas) { + if (!interactiveCanvas) { return; } if (appState.activeTool.type === "selection") { - resetCursor(canvas); + resetCursor(interactiveCanvas); } else if (isHandToolActive(appState)) { - canvas.style.cursor = CURSOR_TYPE.GRAB; + interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB; } else if (isEraserActive(appState)) { - setEraserCursor(canvas, appState.theme); + setEraserCursor(interactiveCanvas, appState.theme); // do nothing if image tool is selected which suggests there's // a image-preview set as the cursor // Ignore custom type as well and let host decide } else if (!["image", "custom"].includes(appState.activeTool.type)) { - canvas.style.cursor = CURSOR_TYPE.CROSSHAIR; + interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR; } }; @@ -927,3 +931,74 @@ export const assertNever = ( throw new Error(message); }; + +/** + * Memoizes on values of `opts` object (strict equality). + */ +export const memoize = , R extends any>( + func: (opts: T) => R, +) => { + let lastArgs: Map | undefined; + let lastResult: R | undefined; + + const ret = function (opts: T) { + const currentArgs = Object.entries(opts); + + if (lastArgs) { + let argsAreEqual = true; + for (const [key, value] of currentArgs) { + if (lastArgs.get(key) !== value) { + argsAreEqual = false; + break; + } + } + if (argsAreEqual) { + return lastResult; + } + } + + const result = func(opts); + + lastArgs = new Map(currentArgs); + lastResult = result; + + return result; + }; + + ret.clear = () => { + lastArgs = undefined; + lastResult = undefined; + }; + + return ret as typeof func & { clear: () => void }; +}; + +export const isRenderThrottlingEnabled = (() => { + // we don't want to throttle in react < 18 because of #5439 and it was + // getting more complex to maintain the fix + let IS_REACT_18_AND_UP: boolean; + try { + const version = React.version.split("."); + IS_REACT_18_AND_UP = Number(version[0]) > 17; + } catch { + IS_REACT_18_AND_UP = false; + } + + let hasWarned = false; + + return () => { + if (window.EXCALIDRAW_THROTTLE_RENDER === true) { + if (!IS_REACT_18_AND_UP) { + if (!hasWarned) { + hasWarned = true; + console.warn( + "Excalidraw: render throttling is disabled on React versions < 18.", + ); + } + return false; + } + return true; + } + return false; + }; +})(); diff --git a/yarn.lock b/yarn.lock index 0dce0be7c3..a6c986fdce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2813,40 +2813,39 @@ test-exclude "^6.0.0" v8-to-istanbul "^9.1.0" -"@vitest/expect@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720" - integrity sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q== +"@vitest/expect@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.1.tgz#2ba6cb96695f4b4388c6d955423a81afc79b8da0" + integrity sha512-q2CD8+XIsQ+tHwypnoCk8Mnv5e6afLFvinVGCq3/BOT4kQdVQmY6rRfyKkwcg635lbliLPqbunXZr+L1ssUWiQ== dependencies: - "@vitest/spy" "0.32.2" - "@vitest/utils" "0.32.2" + "@vitest/spy" "0.34.1" + "@vitest/utils" "0.34.1" chai "^4.3.7" -"@vitest/runner@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.2.tgz#18dd979ce4e8766bcc90948d11b4c8ae6ed90b89" - integrity sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw== +"@vitest/runner@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.1.tgz#23c21ba1db8bff610988c72744db590d0fb6c4ba" + integrity sha512-YfQMpYzDsYB7yqgmlxZ06NI4LurHWfrH7Wy3Pvf/z/vwUSgq1zLAb1lWcItCzQG+NVox+VvzlKQrYEXb47645g== dependencies: - "@vitest/utils" "0.32.2" - concordance "^5.0.4" + "@vitest/utils" "0.34.1" p-limit "^4.0.0" - pathe "^1.1.0" + pathe "^1.1.1" -"@vitest/snapshot@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.2.tgz#500b6453e88e4c50a0aded39839352c16b519b9e" - integrity sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A== +"@vitest/snapshot@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.1.tgz#814c65f8e714eaf255f47838541004b2a2ba28e6" + integrity sha512-0O9LfLU0114OqdF8lENlrLsnn024Tb1CsS9UwG0YMWY2oGTQfPtkW+B/7ieyv0X9R2Oijhi3caB1xgGgEgclSQ== dependencies: - magic-string "^0.30.0" - pathe "^1.1.0" - pretty-format "^27.5.1" + magic-string "^0.30.1" + pathe "^1.1.1" + pretty-format "^29.5.0" -"@vitest/spy@0.32.2": - version "0.32.2" - resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.2.tgz#f3ef7afe0d34e863b90df7c959fa5af540a6aaf9" - integrity sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug== +"@vitest/spy@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.1.tgz#2f77234a3d554c5dea664943f2caaab92d304f3c" + integrity sha512-UT4WcI3EAPUNO8n6y9QoEqynGGEPmmRxC+cLzneFFXpmacivjHZsNbiKD88KUScv5DCHVDgdBsLD7O7s1enFcQ== dependencies: - tinyspy "^2.1.0" + tinyspy "^2.1.1" "@vitest/ui@0.32.2": version "0.32.2" @@ -2870,6 +2869,15 @@ loupe "^2.3.6" pretty-format "^27.5.1" +"@vitest/utils@0.34.1": + version "0.34.1" + resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.1.tgz#e5545c6618775fb9a2dae2a80d94fc2f35222233" + integrity sha512-/ql9dsFi4iuEbiNcjNHQWXBum7aL8pyhxvfnD9gNtbjR9fUKAjxhj4AA3yfLXg6gJpMGGecvtF8Au2G9y3q47Q== + dependencies: + diff-sequences "^29.4.3" + loupe "^2.3.6" + pretty-format "^29.5.0" + abab@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291" @@ -3236,11 +3244,6 @@ blob@0.0.5: resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683" integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig== -blueimp-md5@^2.10.0: - version "2.19.0" - resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0" - integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w== - brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -3510,20 +3513,6 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== -concordance@^5.0.4: - version "5.0.4" - resolved "https://registry.yarnpkg.com/concordance/-/concordance-5.0.4.tgz#9896073261adced72f88d60e4d56f8efc4bbbbd2" - integrity sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw== - dependencies: - date-time "^3.1.0" - esutils "^2.0.3" - fast-diff "^1.2.0" - js-string-escape "^1.0.1" - lodash "^4.17.15" - md5-hex "^3.0.1" - semver "^7.3.2" - well-known-symbols "^2.0.0" - confusing-browser-globals@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" @@ -3638,13 +3627,6 @@ data-urls@^4.0.0: whatwg-mimetype "^3.0.0" whatwg-url "^12.0.0" -date-time@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/date-time/-/date-time-3.1.0.tgz#0d1e934d170579f481ed8df1e2b8ff70ee845e1e" - integrity sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg== - dependencies: - time-zone "^1.0.0" - debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" @@ -4294,7 +4276,7 @@ estree-walker@^2.0.2: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== -esutils@^2.0.2, esutils@^2.0.3: +esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== @@ -4347,11 +4329,6 @@ fast-diff@^1.1.2: resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03" integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w== -fast-diff@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0" - integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw== - fast-glob@^3.2.12, fast-glob@^3.2.9: version "3.2.12" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" @@ -5245,11 +5222,6 @@ jotai@1.13.1: resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.13.1.tgz#20cc46454cbb39096b12fddfa635b873b3668236" integrity sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw== -js-string-escape@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef" - integrity sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg== - "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -5561,13 +5533,6 @@ magic-string@^0.27.0: dependencies: "@jridgewell/sourcemap-codec" "^1.4.13" -magic-string@^0.30.0: - version "0.30.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529" - integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - magic-string@^0.30.1: version "0.30.2" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.2.tgz#dcf04aad3d0d1314bc743d076c50feb29b3c7aca" @@ -5582,13 +5547,6 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" -md5-hex@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c" - integrity sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw== - dependencies: - blueimp-md5 "^2.10.0" - merge-stream@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" @@ -5660,7 +5618,7 @@ mkdirp@^0.5.6: dependencies: minimist "^1.2.6" -mlly@^1.2.0: +mlly@^1.2.0, mlly@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b" integrity sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg== @@ -6525,7 +6483,7 @@ semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== -semver@^7.2.1, semver@^7.3.2, semver@^7.3.7: +semver@^7.2.1, semver@^7.3.7: version "7.4.0" resolved "https://registry.yarnpkg.com/semver/-/semver-7.4.0.tgz#8481c92feffc531ab1e012a8ffc15bdd3a0f4318" integrity sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw== @@ -6703,7 +6661,7 @@ stackback@0.0.2: resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b" integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== -std-env@^3.3.2, std-env@^3.3.3: +std-env@^3.3.3: version "3.3.3" resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe" integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg== @@ -6930,11 +6888,6 @@ through@^2.3.8: resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== -time-zone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/time-zone/-/time-zone-1.0.0.tgz#99c5bf55958966af6d06d83bdf3800dc82faec5d" - integrity sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA== - tiny-invariant@^1.1.0: version "1.3.1" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" @@ -6945,12 +6898,12 @@ tinybench@^2.5.0: resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5" integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA== -tinypool@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.5.0.tgz#3861c3069bf71e4f1f5aa2d2e6b3aaacc278961e" - integrity sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ== +tinypool@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021" + integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww== -tinyspy@^2.1.0: +tinyspy@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c" integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w== @@ -7231,15 +7184,15 @@ v8-to-istanbul@^9.1.0: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" -vite-node@0.32.2: - version "0.32.2" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096" - integrity sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA== +vite-node@0.34.1: + version "0.34.1" + resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.1.tgz#144900ca4bd54cc419c501d671350bcbc07eb1ee" + integrity sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w== dependencies: cac "^6.7.14" debug "^4.3.4" - mlly "^1.2.0" - pathe "^1.1.0" + mlly "^1.4.0" + pathe "^1.1.1" picocolors "^1.0.0" vite "^3.0.0 || ^4.0.0" @@ -7321,35 +7274,34 @@ vitest-canvas-mock@0.3.2: dependencies: jest-canvas-mock "~2.4.0" -vitest@0.32.2: - version "0.32.2" - resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.2.tgz#758ce2220f609e240ac054eca7ad11a5140679ab" - integrity sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ== +vitest@0.34.1: + version "0.34.1" + resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.1.tgz#3ad7f845e7a9fb0d72ab703cae832a54b8469e1e" + integrity sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ== dependencies: "@types/chai" "^4.3.5" "@types/chai-subset" "^1.3.3" "@types/node" "*" - "@vitest/expect" "0.32.2" - "@vitest/runner" "0.32.2" - "@vitest/snapshot" "0.32.2" - "@vitest/spy" "0.32.2" - "@vitest/utils" "0.32.2" - acorn "^8.8.2" + "@vitest/expect" "0.34.1" + "@vitest/runner" "0.34.1" + "@vitest/snapshot" "0.34.1" + "@vitest/spy" "0.34.1" + "@vitest/utils" "0.34.1" + acorn "^8.9.0" acorn-walk "^8.2.0" cac "^6.7.14" chai "^4.3.7" - concordance "^5.0.4" debug "^4.3.4" local-pkg "^0.4.3" - magic-string "^0.30.0" - pathe "^1.1.0" + magic-string "^0.30.1" + pathe "^1.1.1" picocolors "^1.0.0" - std-env "^3.3.2" + std-env "^3.3.3" strip-literal "^1.0.1" tinybench "^2.5.0" - tinypool "^0.5.0" + tinypool "^0.7.0" vite "^3.0.0 || ^4.0.0" - vite-node "0.32.2" + vite-node "0.34.1" why-is-node-running "^2.2.2" vscode-jsonrpc@6.0.0: @@ -7437,11 +7389,6 @@ webworkify@^1.5.0: resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c" integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g== -well-known-symbols@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5" - integrity sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q== - whatwg-encoding@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53" @@ -7534,9 +7481,9 @@ why-is-node-running@^2.2.2: stackback "0.0.2" word-wrap@^1.2.3: - version "1.2.5" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" - integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== workbox-background-sync@7.0.0: version "7.0.0" From 2b14a5c233ebaefb72bffc7f764ca9ac186c3039 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Mon, 14 Aug 2023 15:47:18 +0530 Subject: [PATCH 07/23] build: increase limit for bundle by 1kb (#6880) --- src/packages/excalidraw/.size-limit.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/excalidraw/.size-limit.json b/src/packages/excalidraw/.size-limit.json index 2a66376cb4..e253863545 100644 --- a/src/packages/excalidraw/.size-limit.json +++ b/src/packages/excalidraw/.size-limit.json @@ -1,7 +1,7 @@ [ { "path": "dist/excalidraw.production.min.js", - "limit": "290 kB" + "limit": "291 kB" }, { "path": "dist/excalidraw-assets/locales", From c29f19a88b919254bc41d4818bd22ae54d52b36e Mon Sep 17 00:00:00 2001 From: Rahul Date: Mon, 14 Aug 2023 16:41:08 +0530 Subject: [PATCH 08/23] perf: Limiting the suggested binding to fix performance issue (#6877) --- src/components/App.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/components/App.tsx b/src/components/App.tsx index 2a39bc6b2f..eccc01743a 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -7379,6 +7379,9 @@ class App extends React.Component { private maybeSuggestBindingForAll( selectedElements: NonDeleted[], ): void { + if (selectedElements.length > 50) { + return; + } const suggestedBindings = getEligibleElementsForBinding(selectedElements); this.setState({ suggestedBindings }); } From 9e0bfd178e77e7a524496aa4fa22d81dd3b4e526 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Mon, 14 Aug 2023 13:52:25 +0200 Subject: [PATCH 09/23] refactor: factor out shape generation from `renderElement.ts` pt 2 (#6878) --- src/element/bounds.ts | 2 +- src/renderer/renderElement.ts | 425 +++------------------------------- src/scene/Shape.ts | 362 +++++++++++++++++++++++++++++ src/scene/ShapeCache.ts | 47 ++-- src/scene/types.ts | 18 +- 5 files changed, 438 insertions(+), 416 deletions(-) create mode 100644 src/scene/Shape.ts diff --git a/src/element/bounds.ts b/src/element/bounds.ts index c5af069745..c90d145d83 100644 --- a/src/element/bounds.ts +++ b/src/element/bounds.ts @@ -10,7 +10,7 @@ import { distance2d, rotate, rotatePoint } from "../math"; import rough from "roughjs/bin/rough"; import { Drawable, Op } from "roughjs/bin/core"; import { Point } from "../types"; -import { generateRoughOptions } from "../renderer/renderElement"; +import { generateRoughOptions } from "../scene/Shape"; import { isArrowElement, isFreeDrawElement, diff --git a/src/renderer/renderElement.ts b/src/renderer/renderElement.ts index 837fd62de3..9b040e9a52 100644 --- a/src/renderer/renderElement.ts +++ b/src/renderer/renderElement.ts @@ -1,8 +1,6 @@ import { ExcalidrawElement, - ExcalidrawLinearElement, ExcalidrawTextElement, - Arrowhead, NonDeletedExcalidrawElement, ExcalidrawFreeDrawElement, ExcalidrawImageElement, @@ -16,24 +14,13 @@ import { isArrowElement, hasBoundTextElement, } from "../element/typeChecks"; -import { - getDiamondPoints, - getElementAbsoluteCoords, - getArrowheadPoints, -} from "../element/bounds"; -import { RoughCanvas } from "roughjs/bin/canvas"; -import { Drawable, Options } from "roughjs/bin/core"; -import { RoughSVG } from "roughjs/bin/svg"; -import { RoughGenerator } from "roughjs/bin/generator"; +import { getElementAbsoluteCoords } from "../element/bounds"; +import type { RoughCanvas } from "roughjs/bin/canvas"; +import type { Drawable } from "roughjs/bin/core"; +import type { RoughSVG } from "roughjs/bin/svg"; import { StaticCanvasRenderConfig } from "../scene/types"; -import { - distance, - getFontString, - getFontFamilyString, - isRTL, - isTransparent, -} from "../utils"; +import { distance, getFontString, getFontFamilyString, isRTL } from "../utils"; import { getCornerRadius, isPathALoop, isRightAngle } from "../math"; import rough from "roughjs/bin/rough"; import { @@ -97,10 +84,6 @@ const shouldResetImageFilter = ( ); }; -const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; - -const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth]; - const getCanvasPadding = (element: ExcalidrawElement) => element.type === "freedraw" ? element.strokeWidth * 12 : 20; @@ -384,369 +367,11 @@ const drawElementOnCanvas = ( context.globalAlpha = 1; }; -const elementWithCanvasCache = new WeakMap< +export const elementWithCanvasCache = new WeakMap< ExcalidrawElement, ExcalidrawElementWithCanvas >(); -export const generateRoughOptions = ( - element: ExcalidrawElement, - continuousPath = false, -): Options => { - const options: Options = { - seed: element.seed, - strokeLineDash: - element.strokeStyle === "dashed" - ? getDashArrayDashed(element.strokeWidth) - : element.strokeStyle === "dotted" - ? getDashArrayDotted(element.strokeWidth) - : undefined, - // for non-solid strokes, disable multiStroke because it tends to make - // dashes/dots overlay each other - disableMultiStroke: element.strokeStyle !== "solid", - // for non-solid strokes, increase the width a bit to make it visually - // similar to solid strokes, because we're also disabling multiStroke - strokeWidth: - element.strokeStyle !== "solid" - ? element.strokeWidth + 0.5 - : element.strokeWidth, - // when increasing strokeWidth, we must explicitly set fillWeight and - // hachureGap because if not specified, roughjs uses strokeWidth to - // calculate them (and we don't want the fills to be modified) - fillWeight: element.strokeWidth / 2, - hachureGap: element.strokeWidth * 4, - roughness: element.roughness, - stroke: element.strokeColor, - preserveVertices: continuousPath, - }; - - switch (element.type) { - case "rectangle": - case "embeddable": - case "diamond": - case "ellipse": { - options.fillStyle = element.fillStyle; - options.fill = isTransparent(element.backgroundColor) - ? undefined - : element.backgroundColor; - if (element.type === "ellipse") { - options.curveFitting = 1; - } - return options; - } - case "line": - case "freedraw": { - if (isPathALoop(element.points)) { - options.fillStyle = element.fillStyle; - options.fill = - element.backgroundColor === "transparent" - ? undefined - : element.backgroundColor; - } - return options; - } - case "arrow": - return options; - default: { - throw new Error(`Unimplemented type ${element.type}`); - } - } -}; - -const modifyEmbeddableForRoughOptions = ( - element: NonDeletedExcalidrawElement, - isExporting: boolean, -) => { - if ( - element.type === "embeddable" && - (isExporting || !element.validated) && - isTransparent(element.backgroundColor) && - isTransparent(element.strokeColor) - ) { - return { - ...element, - roughness: 0, - backgroundColor: "#d3d3d3", - fillStyle: "solid", - } as const; - } - return element; -}; - -/** - * Generates the element's shape and puts it into the cache. - * @param element - * @param generator - */ -export const generateElementShape = ( - element: NonDeletedExcalidrawElement, - generator: RoughGenerator, - isExporting: boolean = false, -): Drawable | Drawable[] | null => { - const cachedShape = isExporting ? undefined : ShapeCache.get(element); - - if (cachedShape) { - return cachedShape; - } - - // `null` indicates no rc shape applicable for this element type - // (= do not generate anything) - if (cachedShape === undefined) { - let shape: Drawable | Drawable[] | null = null; - - elementWithCanvasCache.delete(element); - - switch (element.type) { - case "rectangle": - case "embeddable": { - // this is for rendering the stroke/bg of the embeddable, especially - // when the src url is not set - - if (element.roundness) { - const w = element.width; - const h = element.height; - const r = getCornerRadius(Math.min(w, h), element); - shape = generator.path( - `M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${ - h - r - } Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${ - h - r - } L 0 ${r} Q 0 0, ${r} 0`, - generateRoughOptions( - modifyEmbeddableForRoughOptions(element, isExporting), - true, - ), - ); - } else { - shape = generator.rectangle( - 0, - 0, - element.width, - element.height, - generateRoughOptions( - modifyEmbeddableForRoughOptions(element, isExporting), - false, - ), - ); - } - ShapeCache.set(element, shape); - - break; - } - case "diamond": { - const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = - getDiamondPoints(element); - if (element.roundness) { - const verticalRadius = getCornerRadius( - Math.abs(topX - leftX), - element, - ); - - const horizontalRadius = getCornerRadius( - Math.abs(rightY - topY), - element, - ); - - shape = generator.path( - `M ${topX + verticalRadius} ${topY + horizontalRadius} L ${ - rightX - verticalRadius - } ${rightY - horizontalRadius} - C ${rightX} ${rightY}, ${rightX} ${rightY}, ${ - rightX - verticalRadius - } ${rightY + horizontalRadius} - L ${bottomX + verticalRadius} ${bottomY - horizontalRadius} - C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${ - bottomX - verticalRadius - } ${bottomY - horizontalRadius} - L ${leftX + verticalRadius} ${leftY + horizontalRadius} - C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${ - leftY - horizontalRadius - } - L ${topX - verticalRadius} ${topY + horizontalRadius} - C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${ - topY + horizontalRadius - }`, - generateRoughOptions(element, true), - ); - } else { - shape = generator.polygon( - [ - [topX, topY], - [rightX, rightY], - [bottomX, bottomY], - [leftX, leftY], - ], - generateRoughOptions(element), - ); - } - ShapeCache.set(element, shape); - - break; - } - case "ellipse": - shape = generator.ellipse( - element.width / 2, - element.height / 2, - element.width, - element.height, - generateRoughOptions(element), - ); - ShapeCache.set(element, shape); - - break; - case "line": - case "arrow": { - const options = generateRoughOptions(element); - - // 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 : [[0, 0]]; - - // curve is always the first element - // this simplifies finding the curve for an element - if (!element.roundness) { - if (options.fill) { - shape = [generator.polygon(points as [number, number][], options)]; - } else { - shape = [ - generator.linearPath(points as [number, number][], options), - ]; - } - } else { - shape = [generator.curve(points as [number, number][], options)]; - } - - // add lines only in arrow - if (element.type === "arrow") { - const { startArrowhead = null, endArrowhead = "arrow" } = element; - - const getArrowheadShapes = ( - element: ExcalidrawLinearElement, - shape: Drawable[], - position: "start" | "end", - arrowhead: Arrowhead, - ) => { - const arrowheadPoints = getArrowheadPoints( - element, - shape, - position, - arrowhead, - ); - - if (arrowheadPoints === null) { - return []; - } - - // Other arrowheads here... - if (arrowhead === "dot") { - const [x, y, r] = arrowheadPoints; - - return [ - generator.circle(x, y, r, { - ...options, - fill: element.strokeColor, - fillStyle: "solid", - stroke: "none", - }), - ]; - } - - if (arrowhead === "triangle") { - const [x, y, x2, y2, x3, y3] = arrowheadPoints; - - // always use solid stroke for triangle arrowhead - delete options.strokeLineDash; - - return [ - generator.polygon( - [ - [x, y], - [x2, y2], - [x3, y3], - [x, y], - ], - { - ...options, - fill: element.strokeColor, - fillStyle: "solid", - }, - ), - ]; - } - - // Arrow arrowheads - const [x2, y2, x3, y3, x4, y4] = arrowheadPoints; - - if (element.strokeStyle === "dotted") { - // for dotted arrows caps, reduce gap to make it more legible - const dash = getDashArrayDotted(element.strokeWidth - 1); - options.strokeLineDash = [dash[0], dash[1] - 1]; - } else { - // for solid/dashed, keep solid arrow cap - delete options.strokeLineDash; - } - return [ - generator.line(x3, y3, x2, y2, options), - generator.line(x4, y4, x2, y2, options), - ]; - }; - - if (startArrowhead !== null) { - const shapes = getArrowheadShapes( - element, - shape, - "start", - startArrowhead, - ); - shape.push(...shapes); - } - - if (endArrowhead !== null) { - if (endArrowhead === undefined) { - // Hey, we have an old arrow here! - } - - const shapes = getArrowheadShapes( - element, - shape, - "end", - endArrowhead, - ); - shape.push(...shapes); - } - } - - ShapeCache.set(element, shape); - - break; - } - case "freedraw": { - generateFreeDrawShape(element); - - if (isPathALoop(element.points)) { - // generate rough polygon to fill freedraw shape - shape = generator.polygon(element.points as [number, number][], { - ...generateRoughOptions(element), - stroke: "none", - }); - } else { - shape = null; - } - ShapeCache.set(element, shape); - break; - } - case "text": - case "image": { - // just to ensure we don't regenerate element.canvas on rerenders - ShapeCache.set(element, null); - break; - } - } - return shape; - } - return null; -}; - const generateElementWithCanvas = ( element: NonDeletedExcalidrawElement, renderConfig: StaticCanvasRenderConfig, @@ -962,7 +587,6 @@ export const renderElement = ( renderConfig: StaticCanvasRenderConfig, appState: StaticCanvasAppState, ) => { - const generator = rc.generator; switch (element.type) { case "frame": { if ( @@ -1000,7 +624,10 @@ export const renderElement = ( break; } case "freedraw": { - generateElementShape(element, generator); + // TODO investigate if we can do this in situ. Right now we need to call + // beforehand because math helpers (such as getElementAbsoluteCoords) + // rely on existing shapes + ShapeCache.generateElementShape(element); if (renderConfig.isExporting) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); @@ -1038,7 +665,10 @@ export const renderElement = ( case "image": case "text": case "embeddable": { - generateElementShape(element, generator, renderConfig.isExporting); + // TODO investigate if we can do this in situ. Right now we need to call + // beforehand because math helpers (such as getElementAbsoluteCoords) + // rely on existing shapes + ShapeCache.generateElementShape(element, renderConfig.isExporting); if (renderConfig.isExporting) { const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const cx = (x1 + x2) / 2 + appState.scrollX; @@ -1255,7 +885,6 @@ export const renderElementToSvg = ( } } const degree = (180 * element.angle) / Math.PI; - const generator = rsvg.generator; // element to append node to, most of the time svgRoot let root = svgRoot; @@ -1280,10 +909,10 @@ export const renderElementToSvg = ( case "rectangle": case "diamond": case "ellipse": { - generateElementShape(element, generator); + const shape = ShapeCache.generateElementShape(element); const node = roughSVGDrawWithPrecision( rsvg, - ShapeCache.get(element)!, + shape, MAX_DECIMALS_FOR_SVG_EXPORT, ); if (opacity !== 1) { @@ -1310,10 +939,10 @@ export const renderElementToSvg = ( } case "embeddable": { // render placeholder rectangle - generateElementShape(element, generator, true); + const shape = ShapeCache.generateElementShape(element, true); const node = roughSVGDrawWithPrecision( rsvg, - ShapeCache.get(element)!, + shape, MAX_DECIMALS_FOR_SVG_EXPORT, ); const opacity = element.opacity / 100; @@ -1347,7 +976,7 @@ export const renderElementToSvg = ( // render embeddable element + iframe const embeddableNode = roughSVGDrawWithPrecision( rsvg, - ShapeCache.get(element)!, + shape, MAX_DECIMALS_FOR_SVG_EXPORT, ); embeddableNode.setAttribute("stroke-linecap", "round"); @@ -1453,14 +1082,14 @@ export const renderElementToSvg = ( maskRectInvisible.setAttribute("opacity", "1"); maskPath.appendChild(maskRectInvisible); } - generateElementShape(element, generator); const group = svgRoot.ownerDocument!.createElementNS(SVG_NS, "g"); if (boundText) { group.setAttribute("mask", `url(#mask-${element.id})`); } group.setAttribute("stroke-linecap", "round"); - ShapeCache.get(element)!.forEach((shape) => { + const shapes = ShapeCache.generateElementShape(element); + shapes.forEach((shape) => { const node = roughSVGDrawWithPrecision( rsvg, shape, @@ -1501,11 +1130,13 @@ export const renderElementToSvg = ( break; } case "freedraw": { - generateElementShape(element, generator); - generateFreeDrawShape(element); - const shape = ShapeCache.get(element); - const node = shape - ? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT) + const backgroundFillShape = ShapeCache.generateElementShape(element); + const node = backgroundFillShape + ? roughSVGDrawWithPrecision( + rsvg, + backgroundFillShape, + MAX_DECIMALS_FOR_SVG_EXPORT, + ) : svgRoot.ownerDocument!.createElementNS(SVG_NS, "g"); if (opacity !== 1) { node.setAttribute("stroke-opacity", `${opacity}`); diff --git a/src/scene/Shape.ts b/src/scene/Shape.ts new file mode 100644 index 0000000000..f43a1260d4 --- /dev/null +++ b/src/scene/Shape.ts @@ -0,0 +1,362 @@ +import type { Drawable, Options } from "roughjs/bin/core"; +import type { RoughGenerator } from "roughjs/bin/generator"; +import { getDiamondPoints, getArrowheadPoints } from "../element"; +import type { ElementShapes } from "./types"; +import type { + ExcalidrawElement, + NonDeletedExcalidrawElement, + ExcalidrawSelectionElement, + ExcalidrawLinearElement, + Arrowhead, +} from "../element/types"; +import { isPathALoop, getCornerRadius } from "../math"; +import { generateFreeDrawShape } from "../renderer/renderElement"; +import { isTransparent, assertNever } from "../utils"; + +const getDashArrayDashed = (strokeWidth: number) => [8, 8 + strokeWidth]; + +const getDashArrayDotted = (strokeWidth: number) => [1.5, 6 + strokeWidth]; + +export const generateRoughOptions = ( + element: ExcalidrawElement, + continuousPath = false, +): Options => { + const options: Options = { + seed: element.seed, + strokeLineDash: + element.strokeStyle === "dashed" + ? getDashArrayDashed(element.strokeWidth) + : element.strokeStyle === "dotted" + ? getDashArrayDotted(element.strokeWidth) + : undefined, + // for non-solid strokes, disable multiStroke because it tends to make + // dashes/dots overlay each other + disableMultiStroke: element.strokeStyle !== "solid", + // for non-solid strokes, increase the width a bit to make it visually + // similar to solid strokes, because we're also disabling multiStroke + strokeWidth: + element.strokeStyle !== "solid" + ? element.strokeWidth + 0.5 + : element.strokeWidth, + // when increasing strokeWidth, we must explicitly set fillWeight and + // hachureGap because if not specified, roughjs uses strokeWidth to + // calculate them (and we don't want the fills to be modified) + fillWeight: element.strokeWidth / 2, + hachureGap: element.strokeWidth * 4, + roughness: element.roughness, + stroke: element.strokeColor, + preserveVertices: continuousPath, + }; + + switch (element.type) { + case "rectangle": + case "embeddable": + case "diamond": + case "ellipse": { + options.fillStyle = element.fillStyle; + options.fill = isTransparent(element.backgroundColor) + ? undefined + : element.backgroundColor; + if (element.type === "ellipse") { + options.curveFitting = 1; + } + return options; + } + case "line": + case "freedraw": { + if (isPathALoop(element.points)) { + options.fillStyle = element.fillStyle; + options.fill = + element.backgroundColor === "transparent" + ? undefined + : element.backgroundColor; + } + return options; + } + case "arrow": + return options; + default: { + throw new Error(`Unimplemented type ${element.type}`); + } + } +}; + +const modifyEmbeddableForRoughOptions = ( + element: NonDeletedExcalidrawElement, + isExporting: boolean, +) => { + if ( + element.type === "embeddable" && + (isExporting || !element.validated) && + isTransparent(element.backgroundColor) && + isTransparent(element.strokeColor) + ) { + return { + ...element, + roughness: 0, + backgroundColor: "#d3d3d3", + fillStyle: "solid", + } as const; + } + return element; +}; + +/** + * Generates the roughjs shape for given element. + * + * Low-level. Use `ShapeCache.generateElementShape` instead. + * + * @private + */ +export const _generateElementShape = ( + element: Exclude, + generator: RoughGenerator, + isExporting: boolean = false, +): Drawable | Drawable[] | null => { + switch (element.type) { + case "rectangle": + case "embeddable": { + let shape: ElementShapes[typeof element.type]; + // this is for rendering the stroke/bg of the embeddable, especially + // when the src url is not set + + if (element.roundness) { + const w = element.width; + const h = element.height; + const r = getCornerRadius(Math.min(w, h), element); + shape = generator.path( + `M ${r} 0 L ${w - r} 0 Q ${w} 0, ${w} ${r} L ${w} ${ + h - r + } Q ${w} ${h}, ${w - r} ${h} L ${r} ${h} Q 0 ${h}, 0 ${ + h - r + } L 0 ${r} Q 0 0, ${r} 0`, + generateRoughOptions( + modifyEmbeddableForRoughOptions(element, isExporting), + true, + ), + ); + } else { + shape = generator.rectangle( + 0, + 0, + element.width, + element.height, + generateRoughOptions( + modifyEmbeddableForRoughOptions(element, isExporting), + false, + ), + ); + } + return shape; + } + case "diamond": { + let shape: ElementShapes[typeof element.type]; + + const [topX, topY, rightX, rightY, bottomX, bottomY, leftX, leftY] = + getDiamondPoints(element); + if (element.roundness) { + const verticalRadius = getCornerRadius(Math.abs(topX - leftX), element); + + const horizontalRadius = getCornerRadius( + Math.abs(rightY - topY), + element, + ); + + shape = generator.path( + `M ${topX + verticalRadius} ${topY + horizontalRadius} L ${ + rightX - verticalRadius + } ${rightY - horizontalRadius} + C ${rightX} ${rightY}, ${rightX} ${rightY}, ${ + rightX - verticalRadius + } ${rightY + horizontalRadius} + L ${bottomX + verticalRadius} ${bottomY - horizontalRadius} + C ${bottomX} ${bottomY}, ${bottomX} ${bottomY}, ${ + bottomX - verticalRadius + } ${bottomY - horizontalRadius} + L ${leftX + verticalRadius} ${leftY + horizontalRadius} + C ${leftX} ${leftY}, ${leftX} ${leftY}, ${leftX + verticalRadius} ${ + leftY - horizontalRadius + } + L ${topX - verticalRadius} ${topY + horizontalRadius} + C ${topX} ${topY}, ${topX} ${topY}, ${topX + verticalRadius} ${ + topY + horizontalRadius + }`, + generateRoughOptions(element, true), + ); + } else { + shape = generator.polygon( + [ + [topX, topY], + [rightX, rightY], + [bottomX, bottomY], + [leftX, leftY], + ], + generateRoughOptions(element), + ); + } + return shape; + } + case "ellipse": { + const shape: ElementShapes[typeof element.type] = generator.ellipse( + element.width / 2, + element.height / 2, + element.width, + element.height, + generateRoughOptions(element), + ); + return shape; + } + case "line": + case "arrow": { + let shape: ElementShapes[typeof element.type]; + const options = generateRoughOptions(element); + + // 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 : [[0, 0]]; + + // curve is always the first element + // this simplifies finding the curve for an element + if (!element.roundness) { + if (options.fill) { + shape = [generator.polygon(points as [number, number][], options)]; + } else { + shape = [generator.linearPath(points as [number, number][], options)]; + } + } else { + shape = [generator.curve(points as [number, number][], options)]; + } + + // add lines only in arrow + if (element.type === "arrow") { + const { startArrowhead = null, endArrowhead = "arrow" } = element; + + const getArrowheadShapes = ( + element: ExcalidrawLinearElement, + shape: Drawable[], + position: "start" | "end", + arrowhead: Arrowhead, + ) => { + const arrowheadPoints = getArrowheadPoints( + element, + shape, + position, + arrowhead, + ); + + if (arrowheadPoints === null) { + return []; + } + + // Other arrowheads here... + if (arrowhead === "dot") { + const [x, y, r] = arrowheadPoints; + + return [ + generator.circle(x, y, r, { + ...options, + fill: element.strokeColor, + fillStyle: "solid", + stroke: "none", + }), + ]; + } + + if (arrowhead === "triangle") { + const [x, y, x2, y2, x3, y3] = arrowheadPoints; + + // always use solid stroke for triangle arrowhead + delete options.strokeLineDash; + + return [ + generator.polygon( + [ + [x, y], + [x2, y2], + [x3, y3], + [x, y], + ], + { + ...options, + fill: element.strokeColor, + fillStyle: "solid", + }, + ), + ]; + } + + // Arrow arrowheads + const [x2, y2, x3, y3, x4, y4] = arrowheadPoints; + + if (element.strokeStyle === "dotted") { + // for dotted arrows caps, reduce gap to make it more legible + const dash = getDashArrayDotted(element.strokeWidth - 1); + options.strokeLineDash = [dash[0], dash[1] - 1]; + } else { + // for solid/dashed, keep solid arrow cap + delete options.strokeLineDash; + } + return [ + generator.line(x3, y3, x2, y2, options), + generator.line(x4, y4, x2, y2, options), + ]; + }; + + if (startArrowhead !== null) { + const shapes = getArrowheadShapes( + element, + shape, + "start", + startArrowhead, + ); + shape.push(...shapes); + } + + if (endArrowhead !== null) { + if (endArrowhead === undefined) { + // Hey, we have an old arrow here! + } + + const shapes = getArrowheadShapes( + element, + shape, + "end", + endArrowhead, + ); + shape.push(...shapes); + } + } + return shape; + } + case "freedraw": { + let shape: ElementShapes[typeof element.type]; + generateFreeDrawShape(element); + + if (isPathALoop(element.points)) { + // generate rough polygon to fill freedraw shape + shape = generator.polygon(element.points as [number, number][], { + ...generateRoughOptions(element), + stroke: "none", + }); + } else { + shape = null; + } + return shape; + } + case "frame": + case "text": + case "image": { + const shape: ElementShapes[typeof element.type] = null; + // we return (and cache) `null` to make sure we don't regenerate + // `element.canvas` on rerenders + return shape; + } + default: { + assertNever( + element, + `generateElementShape(): Unimplemented type ${(element as any)?.type}`, + ); + return null; + } + } +}; diff --git a/src/scene/ShapeCache.ts b/src/scene/ShapeCache.ts index d2237220bf..ded1b88faa 100644 --- a/src/scene/ShapeCache.ts +++ b/src/scene/ShapeCache.ts @@ -1,28 +1,27 @@ import { Drawable } from "roughjs/bin/core"; import { RoughGenerator } from "roughjs/bin/generator"; -import { ExcalidrawElement } from "../element/types"; -import { generateElementShape } from "../renderer/renderElement"; - -type ElementShape = Drawable | Drawable[] | null; - -type ElementShapes = { - freedraw: Drawable | null; - arrow: Drawable[]; - line: Drawable[]; - text: null; - image: null; -}; +import { + ExcalidrawElement, + ExcalidrawSelectionElement, +} from "../element/types"; +import { elementWithCanvasCache } from "../renderer/renderElement"; +import { _generateElementShape } from "./Shape"; +import { ElementShape, ElementShapes } from "./types"; 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 - : Drawable | null | undefined; + : ElementShape | undefined; }; public static set = ( @@ -41,15 +40,29 @@ export class ShapeCache { /** * Generates & caches shape for element if not already cached, otherwise - * return cached shape. + * returns cached shape. */ - public static generateElementShape = ( + public static generateElementShape = < + T extends Exclude, + >( element: T, + isExporting = false, ) => { - const shape = generateElementShape( + // when exporting, always regenerated to guarantee the latest shape + const cachedShape = 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, - /* so it prefers cache */ false, + isExporting, ) as T["type"] extends keyof ElementShapes ? ElementShapes[T["type"]] : Drawable | null; diff --git a/src/scene/types.ts b/src/scene/types.ts index 9298f5967e..f1c861a9f7 100644 --- a/src/scene/types.ts +++ b/src/scene/types.ts @@ -1,4 +1,5 @@ -import { RoughCanvas } from "roughjs/bin/canvas"; +import type { RoughCanvas } from "roughjs/bin/canvas"; +import { Drawable } from "roughjs/bin/core"; import { ExcalidrawTextElement, NonDeletedExcalidrawElement, @@ -90,3 +91,18 @@ export type ScrollBars = { height: number; } | null; }; + +export type ElementShape = Drawable | Drawable[] | null; + +export type ElementShapes = { + rectangle: Drawable; + ellipse: Drawable; + diamond: Drawable; + embeddable: Drawable; + freedraw: Drawable | null; + arrow: Drawable[]; + line: Drawable[]; + text: null; + image: null; + frame: null; +}; From 59b53eb9cb8318d15dd46c9276ada5ac96a8ac88 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:25:31 +0200 Subject: [PATCH 10/23] build(deps): bump protobufjs from 6.11.3 to 6.11.4 (#6890) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index a6c986fdce..2ec79f0b4d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6072,9 +6072,9 @@ prop-types@^15.8.1: react-is "^16.13.1" protobufjs@^6.8.6: - version "6.11.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" - integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + version "6.11.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.4.tgz#29a412c38bf70d89e537b6d02d904a6f448173aa" + integrity sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" @@ -6091,9 +6091,9 @@ protobufjs@^6.8.6: long "^4.0.0" protobufjs@^7.0.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.3.tgz#01af019e40d9c6133c49acbb3ff9e30f4f0f70b2" - integrity sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg== + version "7.2.4" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.4.tgz#3fc1ec0cdc89dd91aef9ba6037ba07408485c3ae" + integrity sha512-AT+RJgD2sH8phPmCf7OUZR8xGdcJRga4+1cOaXJ64hvcSkVhNcRHOwIxUatPH15+nj59WAGTDv3LSGZPEQbJaQ== dependencies: "@protobufjs/aspromise" "^1.1.2" "@protobufjs/base64" "^1.1.2" From 9b727025fdfa6c9d33b9b5951cc5eb4d8d2b0c4e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:38:16 +0200 Subject: [PATCH 11/23] build(deps): bump semver from 6.3.0 to 6.3.1 in /src/packages/excalidraw (#6754) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/packages/excalidraw/yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packages/excalidraw/yarn.lock b/src/packages/excalidraw/yarn.lock index 789571d873..91789d834e 100644 --- a/src/packages/excalidraw/yarn.lock +++ b/src/packages/excalidraw/yarn.lock @@ -3935,14 +3935,14 @@ semver@7.5.3: lru-cache "^6.0.0" semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: - version "7.3.7" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.7.tgz#12c5b649afdbf9049707796e22a4028814ce523f" - integrity sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" From 3cf8259e71ebb317ede9220c4bfec398084212fe Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 11:38:36 +0200 Subject: [PATCH 12/23] build(deps): bump semver from 6.3.0 to 6.3.1 in /src/packages/utils (#6755) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- src/packages/utils/yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/packages/utils/yarn.lock b/src/packages/utils/yarn.lock index c5d00fd23f..c10a5aed8c 100644 --- a/src/packages/utils/yarn.lock +++ b/src/packages/utils/yarn.lock @@ -2259,14 +2259,14 @@ semver@7.0.0: integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== semver@^7.3.4, semver@^7.3.5: - version "7.3.5" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" - integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" From 991f5570ce420c17c61cfdb3c9520dcdd8269f8c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 16 Aug 2023 16:11:43 +0530 Subject: [PATCH 13/23] build(deps): bump word-wrap from 1.2.3 to 1.2.5 (#6892) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.5. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.5) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 2ec79f0b4d..166532f4e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7481,9 +7481,9 @@ why-is-node-running@^2.2.2: stackback "0.0.2" word-wrap@^1.2.3: - version "1.2.3" - resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" - integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + version "1.2.5" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34" + integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== workbox-background-sync@7.0.0: version "7.0.0" From 1bd416002ca9307f1dfc595e77b71713c8a811da Mon Sep 17 00:00:00 2001 From: zsviczian Date: Wed, 16 Aug 2023 23:59:37 +0200 Subject: [PATCH 14/23] fix: scope `--color-selection` retrieval to given instance (#6886) Co-authored-by: dwelle --- src/components/App.tsx | 1 + src/components/canvases/InteractiveCanvas.tsx | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index eccc01743a..dad7a7b039 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1226,6 +1226,7 @@ class App extends React.Component { }} /> ; canvas: HTMLCanvasElement | null; elements: readonly NonDeletedExcalidrawElement[]; visibleElements: readonly NonDeletedExcalidrawElement[]; @@ -105,9 +106,12 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => { cursorButton[socketId] = user.button; }); - const selectionColor = getComputedStyle( - document.querySelector(".excalidraw")!, - ).getPropertyValue("--color-selection"); + const selectionColor = + (props.containerRef?.current && + getComputedStyle(props.containerRef.current).getPropertyValue( + "--color-selection", + )) || + "#6965db"; renderInteractiveScene( { From d140d1b8b393efeb53d57d1f75bebb7d0e213703 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Thu, 17 Aug 2023 13:39:15 +0200 Subject: [PATCH 15/23] fix: make canvas compos memoize appState on props they declare (#6897) --- src/components/canvases/InteractiveCanvas.tsx | 2 +- src/components/canvases/StaticCanvas.tsx | 11 +-- src/groups.ts | 9 ++- src/renderer/renderScene.ts | 4 +- .../__snapshots__/contextmenu.test.tsx.snap | 32 ++++---- .../regressionTests.test.tsx.snap | 76 +++++++++---------- src/tests/dragCreate.test.tsx | 20 ++--- src/tests/linearElementEditor.test.tsx | 22 +++--- src/tests/move.test.tsx | 6 +- src/tests/multiPointCreate.test.tsx | 10 +-- src/tests/selection.test.tsx | 10 +-- src/types.ts | 61 ++++++++------- 12 files changed, 133 insertions(+), 130 deletions(-) diff --git a/src/components/canvases/InteractiveCanvas.tsx b/src/components/canvases/InteractiveCanvas.tsx index a4ee9ee590..a4d0070576 100644 --- a/src/components/canvases/InteractiveCanvas.tsx +++ b/src/components/canvases/InteractiveCanvas.tsx @@ -166,7 +166,7 @@ const InteractiveCanvas = (props: InteractiveCanvasProps) => { const getRelevantAppStateProps = ( appState: AppState, -): Omit => ({ +): InteractiveCanvasAppState => ({ zoom: appState.zoom, scrollX: appState.scrollX, scrollY: appState.scrollY, diff --git a/src/components/canvases/StaticCanvas.tsx b/src/components/canvases/StaticCanvas.tsx index 8babdb7456..f32133f35f 100644 --- a/src/components/canvases/StaticCanvas.tsx +++ b/src/components/canvases/StaticCanvas.tsx @@ -61,13 +61,7 @@ const StaticCanvas = (props: StaticCanvasProps) => { const getRelevantAppStateProps = ( appState: AppState, -): Omit< - StaticCanvasAppState, - | "editingElement" - | "selectedElementIds" - | "editingGroupId" - | "frameToHighlight" -> => ({ +): StaticCanvasAppState => ({ zoom: appState.zoom, scrollX: appState.scrollX, scrollY: appState.scrollY, @@ -84,6 +78,9 @@ const getRelevantAppStateProps = ( selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged, gridSize: appState.gridSize, frameRendering: appState.frameRendering, + selectedElementIds: appState.selectedElementIds, + frameToHighlight: appState.frameToHighlight, + editingGroupId: appState.editingGroupId, }); const areEqual = ( diff --git a/src/groups.ts b/src/groups.ts index e7b222b3c6..8dbe2ba5c3 100644 --- a/src/groups.ts +++ b/src/groups.ts @@ -12,6 +12,7 @@ import { import { getSelectedElements } from "./scene"; import { getBoundTextElement } from "./element/textElement"; import { makeNextSelectedElementIds } from "./scene/selection"; +import { Mutable } from "./utility-types"; export const selectGroup = ( groupId: GroupId, @@ -155,9 +156,11 @@ export const selectGroupsForSelectedElements = (function () { * you don't care about optimizing selectElements retrieval */ app: AppClassProperties | null, - ): Pick< - InteractiveCanvasAppState, - "selectedGroupIds" | "editingGroupId" | "selectedElementIds" + ): Mutable< + Pick< + InteractiveCanvasAppState, + "selectedGroupIds" | "editingGroupId" | "selectedElementIds" + > > => { const selectedElements = app ? app.scene.getSelectedElements({ diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 03de49b175..736edb99d8 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -6,8 +6,8 @@ import { StaticCanvasAppState, BinaryFiles, Point, - CommonCanvasAppState, Zoom, + AppState, } from "../types"; import { ExcalidrawElement, @@ -407,7 +407,7 @@ const bootstrapCanvas = ({ scale: number; normalizedWidth: number; normalizedHeight: number; - theme?: CommonCanvasAppState["theme"]; + theme?: AppState["theme"]; isExporting?: StaticCanvasRenderConfig["isExporting"]; viewBackgroundColor?: StaticCanvasAppState["viewBackgroundColor"]; }): CanvasRenderingContext2D => { diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 608ab8efd4..488cd624af 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -467,7 +467,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of renders 1`] = `4`; +exports[`contextMenu element > right-clicking on a group should select whole group > [end of test] number of renders 1`] = `5`; exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] appState 1`] = ` { @@ -666,7 +666,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of elements 1`] = `1`; -exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `6`; +exports[`contextMenu element > selecting 'Add to library' in context menu adds element to library > [end of test] number of renders 1`] = `7`; exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] appState 1`] = ` { @@ -1039,7 +1039,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `10`; +exports[`contextMenu element > selecting 'Bring forward' in context menu brings element forward > [end of test] number of renders 1`] = `13`; exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] appState 1`] = ` { @@ -1412,7 +1412,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `10`; +exports[`contextMenu element > selecting 'Bring to front' in context menu brings element to front > [end of test] number of renders 1`] = `13`; exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] appState 1`] = ` { @@ -1611,7 +1611,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of elements 1`] = `1`; -exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `6`; +exports[`contextMenu element > selecting 'Copy styles' in context menu copies styles > [end of test] number of renders 1`] = `7`; exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] appState 1`] = ` { @@ -1847,7 +1847,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of elements 1`] = `1`; -exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `7`; +exports[`contextMenu element > selecting 'Delete' in context menu deletes element > [end of test] number of renders 1`] = `8`; exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] appState 1`] = ` { @@ -2148,7 +2148,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `7`; +exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates element > [end of test] number of renders 1`] = `8`; exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] appState 1`] = ` { @@ -2537,7 +2537,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `10`; +exports[`contextMenu element > selecting 'Group selection' in context menu groups selected elements > [end of test] number of renders 1`] = `13`; exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] appState 1`] = ` { @@ -3416,7 +3416,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `17`; +exports[`contextMenu element > selecting 'Paste styles' in context menu pastes styles > [end of test] number of renders 1`] = `20`; exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] appState 1`] = ` { @@ -3789,7 +3789,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `10`; +exports[`contextMenu element > selecting 'Send backward' in context menu sends element backward > [end of test] number of renders 1`] = `12`; exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] appState 1`] = ` { @@ -4162,7 +4162,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `10`; +exports[`contextMenu element > selecting 'Send to back' in context menu sends element to back > [end of test] number of renders 1`] = `12`; exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] appState 1`] = ` { @@ -4618,7 +4618,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `11`; +exports[`contextMenu element > selecting 'Ungroup selection' in context menu ungroups selected group > [end of test] number of renders 1`] = `14`; exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] appState 1`] = ` { @@ -5198,7 +5198,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `9`; +exports[`contextMenu element > shows 'Group selection' in context menu for multiple selected elements > [end of test] number of renders 1`] = `13`; exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] appState 1`] = ` { @@ -5863,7 +5863,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of elements 1`] = `2`; -exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `10`; +exports[`contextMenu element > shows 'Ungroup selection' in context menu for group inside selected elements > [end of test] number of renders 1`] = `14`; exports[`contextMenu element > shows context menu for canvas > [end of test] appState 1`] = ` { @@ -7031,6 +7031,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] nu exports[`contextMenu element > shows context menu for element > [end of test] number of elements 2`] = `2`; -exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `6`; +exports[`contextMenu element > shows context menu for element > [end of test] number of renders 1`] = `7`; -exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `4`; +exports[`contextMenu element > shows context menu for element > [end of test] number of renders 2`] = `6`; diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index a1a76204b1..e9bbfc7ba1 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -451,7 +451,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of elements 1`] = `0`; -exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `14`; +exports[`given element A and group of elements B and given both are selected when user clicks on B, on pointer up only elements from B should be selected > [end of test] number of renders 1`] = `22`; exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] appState 1`] = ` { @@ -906,7 +906,7 @@ exports[`given element A and group of elements B and given both are selected whe exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of elements 1`] = `0`; -exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `14`; +exports[`given element A and group of elements B and given both are selected when user shift-clicks on B, on pointer up only element A should be selected > [end of test] number of renders 1`] = `20`; exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] appState 1`] = ` { @@ -1734,7 +1734,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `15`; +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `32`; exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] appState 1`] = ` { @@ -1944,7 +1944,7 @@ exports[`regression tests > Drags selected element when hitting only bounding bo exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `9`; +exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] number of renders 1`] = `10`; exports[`regression tests > adjusts z order when grouping > [end of test] appState 1`] = ` { @@ -2395,7 +2395,7 @@ exports[`regression tests > adjusts z order when grouping > [end of test] histor exports[`regression tests > adjusts z order when grouping > [end of test] number of elements 1`] = `0`; -exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `14`; +exports[`regression tests > adjusts z order when grouping > [end of test] number of renders 1`] = `19`; exports[`regression tests > alt-drag duplicates an element > [end of test] appState 1`] = ` { @@ -2634,7 +2634,7 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] histo exports[`regression tests > alt-drag duplicates an element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `9`; +exports[`regression tests > alt-drag duplicates an element > [end of test] number of renders 1`] = `10`; exports[`regression tests > arrow keys > [end of test] appState 1`] = ` { @@ -2799,7 +2799,7 @@ exports[`regression tests > arrow keys > [end of test] history 1`] = ` exports[`regression tests > arrow keys > [end of test] number of elements 1`] = `0`; -exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `13`; +exports[`regression tests > arrow keys > [end of test] number of renders 1`] = `14`; exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] appState 1`] = ` { @@ -3240,7 +3240,7 @@ exports[`regression tests > can drag element that covers another element, while exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `15`; +exports[`regression tests > can drag element that covers another element, while another elem is selected > [end of test] number of renders 1`] = `19`; exports[`regression tests > change the properties of a shape > [end of test] appState 1`] = ` { @@ -3534,7 +3534,7 @@ exports[`regression tests > change the properties of a shape > [end of test] his exports[`regression tests > change the properties of a shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `10`; +exports[`regression tests > change the properties of a shape > [end of test] number of renders 1`] = `11`; exports[`regression tests > click on an element and drag it > [dragged] appState 1`] = ` { @@ -3776,7 +3776,7 @@ exports[`regression tests > click on an element and drag it > [dragged] history exports[`regression tests > click on an element and drag it > [dragged] number of elements 1`] = `1`; -exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `9`; +exports[`regression tests > click on an element and drag it > [dragged] number of renders 1`] = `10`; exports[`regression tests > click on an element and drag it > [end of test] appState 1`] = ` { @@ -4029,7 +4029,7 @@ exports[`regression tests > click on an element and drag it > [end of test] hist exports[`regression tests > click on an element and drag it > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `11`; +exports[`regression tests > click on an element and drag it > [end of test] number of renders 1`] = `12`; exports[`regression tests > click to select a shape > [end of test] appState 1`] = ` { @@ -4268,7 +4268,7 @@ exports[`regression tests > click to select a shape > [end of test] history 1`] exports[`regression tests > click to select a shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `10`; +exports[`regression tests > click to select a shape > [end of test] number of renders 1`] = `13`; exports[`regression tests > click-drag to select a group > [end of test] appState 1`] = ` { @@ -4609,7 +4609,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history exports[`regression tests > click-drag to select a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `13`; +exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `19`; exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] appState 1`] = ` { @@ -5081,7 +5081,7 @@ exports[`regression tests > deleting last but one element in editing group shoul exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of renders 1`] = `12`; +exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] number of renders 1`] = `20`; exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` { @@ -5375,7 +5375,7 @@ exports[`regression tests > deselects group of selected elements on pointer down exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `10`; +exports[`regression tests > deselects group of selected elements on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `14`; exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] appState 1`] = ` { @@ -5641,7 +5641,7 @@ exports[`regression tests > deselects group of selected elements on pointer up w exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `10`; +exports[`regression tests > deselects group of selected elements on pointer up when pointer hits common bounding box without hitting any element > [end of test] number of renders 1`] = `14`; exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] appState 1`] = ` { @@ -5862,7 +5862,7 @@ exports[`regression tests > deselects selected element on pointer down when poin exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `7`; +exports[`regression tests > deselects selected element on pointer down when pointer doesn't hit any element > [end of test] number of renders 1`] = `9`; exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] appState 1`] = ` { @@ -6027,7 +6027,7 @@ exports[`regression tests > deselects selected element, on pointer up, when clic exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `7`; +exports[`regression tests > deselects selected element, on pointer up, when click hits element bounding box but doesn't hit the element > [end of test] number of renders 1`] = `9`; exports[`regression tests > double click to edit a group > [end of test] appState 1`] = ` { @@ -6476,7 +6476,7 @@ exports[`regression tests > double click to edit a group > [end of test] history exports[`regression tests > double click to edit a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `14`; +exports[`regression tests > double click to edit a group > [end of test] number of renders 1`] = `19`; exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] appState 1`] = ` { @@ -6790,7 +6790,7 @@ exports[`regression tests > drags selected elements from point inside common bou exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of elements 1`] = `0`; -exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `12`; +exports[`regression tests > drags selected elements from point inside common bounding box that doesn't hit any element and keeps elements selected after dragging > [end of test] number of renders 1`] = `15`; exports[`regression tests > draw every type of shape > [end of test] appState 1`] = ` { @@ -8854,7 +8854,7 @@ exports[`regression tests > draw every type of shape > [end of test] history 1`] exports[`regression tests > draw every type of shape > [end of test] number of elements 1`] = `0`; -exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `36`; +exports[`regression tests > draw every type of shape > [end of test] number of renders 1`] = `44`; exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] appState 1`] = ` { @@ -9195,7 +9195,7 @@ exports[`regression tests > given a group of selected elements with an element t exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `13`; +exports[`regression tests > given a group of selected elements with an element that is not selected inside the group common bounding box when element that is not selected is clicked should switch selection to not selected element on pointer up > [end of test] number of renders 1`] = `18`; exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] appState 1`] = ` { @@ -9435,7 +9435,7 @@ exports[`regression tests > given a selected element A and a not selected elemen exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `11`; +exports[`regression tests > given a selected element A and a not selected element B with higher z-index than A and given B partially overlaps A when there's a shift-click on the overlapped section B is added to the selection > [end of test] number of renders 1`] = `15`; exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] appState 1`] = ` { @@ -9631,7 +9631,7 @@ exports[`regression tests > given selected element A with lower z-index than uns exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of renders 1`] = `5`; +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when clicking intersection between A and B B should be selected on pointer up > [end of test] number of renders 1`] = `7`; exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] appState 1`] = ` { @@ -9899,7 +9899,7 @@ exports[`regression tests > given selected element A with lower z-index than uns exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of elements 1`] = `0`; -exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of renders 1`] = `7`; +exports[`regression tests > given selected element A with lower z-index than unselected element B and given B is partially over A when dragging on intersection between A and B A should be dragged and keep being selected > [end of test] number of renders 1`] = `8`; exports[`regression tests > key 2 selects rectangle tool > [end of test] appState 1`] = ` { @@ -12728,7 +12728,7 @@ exports[`regression tests > make a group and duplicate it > [end of test] histor exports[`regression tests > make a group and duplicate it > [end of test] number of elements 1`] = `0`; -exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `16`; +exports[`regression tests > make a group and duplicate it > [end of test] number of renders 1`] = `21`; exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] appState 1`] = ` { @@ -12967,7 +12967,7 @@ exports[`regression tests > noop interaction after undo shouldn't create history exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of elements 1`] = `0`; -exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `13`; +exports[`regression tests > noop interaction after undo shouldn't create history entry > [end of test] number of renders 1`] = `17`; exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = ` { @@ -13207,7 +13207,7 @@ exports[`regression tests > rerenders UI on language change > [end of test] hist exports[`regression tests > rerenders UI on language change > [end of test] number of elements 1`] = `0`; -exports[`regression tests > rerenders UI on language change > [end of test] number of renders 1`] = `4`; +exports[`regression tests > rerenders UI on language change > [end of test] number of renders 1`] = `5`; exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] appState 1`] = ` { @@ -13372,7 +13372,7 @@ exports[`regression tests > shift click on selected element should deselect it o exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of elements 1`] = `0`; -exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `7`; +exports[`regression tests > shift click on selected element should deselect it on pointer up > [end of test] number of renders 1`] = `9`; exports[`regression tests > shift-click to multiselect, then drag > [end of test] appState 1`] = ` { @@ -13686,7 +13686,7 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of elements 1`] = `0`; -exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `12`; +exports[`regression tests > shift-click to multiselect, then drag > [end of test] number of renders 1`] = `16`; exports[`regression tests > should group elements and ungroup them > [end of test] appState 1`] = ` { @@ -14244,7 +14244,7 @@ exports[`regression tests > should group elements and ungroup them > [end of tes exports[`regression tests > should group elements and ungroup them > [end of test] number of elements 1`] = `0`; -exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `15`; +exports[`regression tests > should group elements and ungroup them > [end of test] number of renders 1`] = `22`; exports[`regression tests > should show fill icons when element has non transparent background > [end of test] appState 1`] = ` { @@ -14452,7 +14452,7 @@ exports[`regression tests > should show fill icons when element has non transpar exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of elements 1`] = `0`; -exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `9`; +exports[`regression tests > should show fill icons when element has non transparent background > [end of test] number of renders 1`] = `10`; exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] appState 1`] = ` { @@ -15305,7 +15305,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `19`; +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `31`; exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] appState 1`] = ` { @@ -16209,7 +16209,7 @@ exports[`regression tests > supports nested groups > [end of test] history 1`] = exports[`regression tests > supports nested groups > [end of test] number of elements 1`] = `0`; -exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `15`; +exports[`regression tests > supports nested groups > [end of test] number of renders 1`] = `27`; exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] appState 1`] = ` { @@ -16606,7 +16606,7 @@ exports[`regression tests > switches from group of selected elements to another exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of elements 1`] = `0`; -exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `13`; +exports[`regression tests > switches from group of selected elements to another element on pointer down > [end of test] number of renders 1`] = `18`; exports[`regression tests > switches selected element on pointer down > [end of test] appState 1`] = ` { @@ -16901,7 +16901,7 @@ exports[`regression tests > switches selected element on pointer down > [end of exports[`regression tests > switches selected element on pointer down > [end of test] number of elements 1`] = `0`; -exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `10`; +exports[`regression tests > switches selected element on pointer down > [end of test] number of renders 1`] = `13`; exports[`regression tests > two-finger scroll works > [end of test] appState 1`] = ` { @@ -17501,7 +17501,7 @@ exports[`regression tests > undo/redo drawing an element > [end of test] history exports[`regression tests > undo/redo drawing an element > [end of test] number of elements 1`] = `0`; -exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `21`; +exports[`regression tests > undo/redo drawing an element > [end of test] number of renders 1`] = `24`; exports[`regression tests > updates fontSize & fontFamily appState > [end of test] appState 1`] = ` { @@ -17621,7 +17621,7 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of elements 1`] = `0`; -exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `5`; +exports[`regression tests > updates fontSize & fontFamily appState > [end of test] number of renders 1`] = `6`; exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = ` { diff --git a/src/tests/dragCreate.test.tsx b/src/tests/dragCreate.test.tsx index 2f0b0a27d9..71dc63eb52 100644 --- a/src/tests/dragCreate.test.tsx +++ b/src/tests/dragCreate.test.tsx @@ -47,7 +47,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -79,7 +79,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); @@ -112,7 +112,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -144,7 +144,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -180,7 +180,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); @@ -221,7 +221,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -241,7 +241,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -261,7 +261,7 @@ describe("Test dragCreate", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -286,7 +286,7 @@ describe("Test dragCreate", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); @@ -311,7 +311,7 @@ describe("Test dragCreate", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(0); }); diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index 92e8749181..bedcb0dfa5 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -175,13 +175,13 @@ describe("Test Linear Elements", () => { const line = h.elements[0] as ExcalidrawLinearElement; expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(4); + expect(renderStaticScene).toHaveBeenCalledTimes(5); expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2); // drag line from midpoint drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(line.points.length).toEqual(3); expect(line.points).toMatchInlineSnapshot(` [ @@ -275,7 +275,7 @@ describe("Test Linear Elements", () => { // drag line from midpoint drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); expect(renderInteractiveScene).toHaveBeenCalledTimes(14); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(8); expect(line.points.length).toEqual(3); expect(line.points).toMatchInlineSnapshot(` @@ -313,7 +313,7 @@ describe("Test Linear Elements", () => { fireEvent.click(screen.getByTitle("Round")); expect(renderInteractiveScene).toHaveBeenCalledTimes(10); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(8); const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints( h.elements[0] as ExcalidrawLinearElement, @@ -359,7 +359,7 @@ describe("Test Linear Elements", () => { drag(startPoint, endPoint); expect(renderInteractiveScene).toHaveBeenCalledTimes(14); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect([line.x, line.y]).toEqual([ points[0][0] + deltaX, @@ -418,7 +418,7 @@ describe("Test Linear Elements", () => { ]); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(11); expect(line.points.length).toEqual(5); @@ -459,7 +459,7 @@ describe("Test Linear Elements", () => { drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); expect(renderInteractiveScene).toHaveBeenCalledTimes(14); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(8); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ @@ -486,7 +486,7 @@ describe("Test Linear Elements", () => { drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]); expect(renderInteractiveScene).toHaveBeenCalledTimes(14); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(8); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ @@ -521,7 +521,7 @@ describe("Test Linear Elements", () => { deletePoint(points[2]); expect(line.points.length).toEqual(3); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(12); const newMidPoints = LinearElementEditor.getEditorMidPoints( line, @@ -568,7 +568,7 @@ describe("Test Linear Elements", () => { lastSegmentMidpoint[1] + delta, ]); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(11); expect(line.points.length).toEqual(5); expect((h.elements[0] as ExcalidrawLinearElement).points) @@ -644,7 +644,7 @@ describe("Test Linear Elements", () => { drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]); expect(renderInteractiveScene).toHaveBeenCalledTimes(14); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(8); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); expect([newPoints[0][0], newPoints[0][1]]).toEqual([ diff --git a/src/tests/move.test.tsx b/src/tests/move.test.tsx index e3a9b69b89..4fe7ab0a6a 100644 --- a/src/tests/move.test.tsx +++ b/src/tests/move.test.tsx @@ -43,7 +43,7 @@ describe("move element", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -85,7 +85,7 @@ describe("move element", () => { new Pointer("mouse").clickOn(rectB); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(16); + expect(renderStaticScene).toHaveBeenCalledTimes(20); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(3); expect(h.state.selectedElementIds[rectB.id]).toBeTruthy(); @@ -131,7 +131,7 @@ describe("duplicate element on move when ALT is clicked", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(6); - expect(renderStaticScene).toHaveBeenCalledTimes(6); + expect(renderStaticScene).toHaveBeenCalledTimes(7); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/tests/multiPointCreate.test.tsx b/src/tests/multiPointCreate.test.tsx index a5277088d7..d26207eea5 100644 --- a/src/tests/multiPointCreate.test.tsx +++ b/src/tests/multiPointCreate.test.tsx @@ -47,7 +47,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.elements.length).toEqual(0); }); @@ -62,7 +62,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.elements.length).toEqual(0); }); @@ -77,7 +77,7 @@ describe("remove shape in non linear elements", () => { fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 }); expect(renderInteractiveScene).toHaveBeenCalledTimes(5); - expect(renderStaticScene).toHaveBeenCalledTimes(5); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(h.elements.length).toEqual(0); }); }); @@ -110,7 +110,7 @@ describe("multi point mode in linear elements", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(10); - expect(renderStaticScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(11); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; @@ -154,7 +154,7 @@ describe("multi point mode in linear elements", () => { }); expect(renderInteractiveScene).toHaveBeenCalledTimes(10); - expect(renderStaticScene).toHaveBeenCalledTimes(10); + expect(renderStaticScene).toHaveBeenCalledTimes(11); expect(h.elements.length).toEqual(1); const element = h.elements[0] as ExcalidrawLinearElement; diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index 8df93351d7..1afde34ad5 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -308,7 +308,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -338,7 +338,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -368,7 +368,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -411,7 +411,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -453,7 +453,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(7); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); diff --git a/src/types.ts b/src/types.ts index 9eeaf72830..bc77708663 100644 --- a/src/types.ts +++ b/src/types.ts @@ -104,14 +104,13 @@ export type LastActiveTool = export type SidebarName = string; export type SidebarTabName = string; -export type CommonCanvasAppState = { +type _CommonCanvasAppState = { zoom: AppState["zoom"]; scrollX: AppState["scrollX"]; scrollY: AppState["scrollY"]; width: AppState["width"]; height: AppState["height"]; viewModeEnabled: AppState["viewModeEnabled"]; - editingElement: AppState["editingElement"]; editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible @@ -121,34 +120,38 @@ export type CommonCanvasAppState = { pendingImageElementId: AppState["pendingImageElementId"]; }; -export type StaticCanvasAppState = CommonCanvasAppState & { - shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"]; - /** null indicates transparent bg */ - viewBackgroundColor: AppState["viewBackgroundColor"] | null; - exportScale: AppState["exportScale"]; - selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"]; - gridSize: AppState["gridSize"]; - frameRendering: AppState["frameRendering"]; -}; +export type StaticCanvasAppState = Readonly< + _CommonCanvasAppState & { + shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"]; + /** null indicates transparent bg */ + viewBackgroundColor: AppState["viewBackgroundColor"] | null; + exportScale: AppState["exportScale"]; + selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"]; + gridSize: AppState["gridSize"]; + frameRendering: AppState["frameRendering"]; + } +>; -export type InteractiveCanvasAppState = CommonCanvasAppState & { - // renderInteractiveScene - activeEmbeddable: AppState["activeEmbeddable"]; - editingLinearElement: AppState["editingLinearElement"]; - selectionElement: AppState["selectionElement"]; - selectedGroupIds: AppState["selectedGroupIds"]; - selectedLinearElement: AppState["selectedLinearElement"]; - multiElement: AppState["multiElement"]; - isBindingEnabled: AppState["isBindingEnabled"]; - suggestedBindings: AppState["suggestedBindings"]; - isRotating: AppState["isRotating"]; - elementsToHighlight: AppState["elementsToHighlight"]; - // App - openSidebar: AppState["openSidebar"]; - showHyperlinkPopup: AppState["showHyperlinkPopup"]; - // Collaborators - collaborators: AppState["collaborators"]; -}; +export type InteractiveCanvasAppState = Readonly< + _CommonCanvasAppState & { + // renderInteractiveScene + activeEmbeddable: AppState["activeEmbeddable"]; + editingLinearElement: AppState["editingLinearElement"]; + selectionElement: AppState["selectionElement"]; + selectedGroupIds: AppState["selectedGroupIds"]; + selectedLinearElement: AppState["selectedLinearElement"]; + multiElement: AppState["multiElement"]; + isBindingEnabled: AppState["isBindingEnabled"]; + suggestedBindings: AppState["suggestedBindings"]; + isRotating: AppState["isRotating"]; + elementsToHighlight: AppState["elementsToHighlight"]; + // App + openSidebar: AppState["openSidebar"]; + showHyperlinkPopup: AppState["showHyperlinkPopup"]; + // Collaborators + collaborators: AppState["collaborators"]; + } +>; export type AppState = { contextMenu: { From 49e9a2ab33c5db24b814ef5d93ef6dacaa262a9b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 17 Aug 2023 13:47:32 +0200 Subject: [PATCH 16/23] build(deps): bump @excalidraw/excalidraw from 0.15.2 to 0.15.3 in /dev-docs (#6896) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- dev-docs/package.json | 2 +- dev-docs/yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dev-docs/package.json b/dev-docs/package.json index 0aee8e01f4..cbf0d4334f 100644 --- a/dev-docs/package.json +++ b/dev-docs/package.json @@ -18,7 +18,7 @@ "@docusaurus/core": "2.2.0", "@docusaurus/preset-classic": "2.2.0", "@docusaurus/theme-live-codeblock": "2.2.0", - "@excalidraw/excalidraw": "0.15.2", + "@excalidraw/excalidraw": "0.15.3", "@mdx-js/react": "^1.6.22", "clsx": "^1.2.1", "docusaurus-plugin-sass": "0.2.3", diff --git a/dev-docs/yarn.lock b/dev-docs/yarn.lock index 194a38e750..a2b61d255f 100644 --- a/dev-docs/yarn.lock +++ b/dev-docs/yarn.lock @@ -1631,10 +1631,10 @@ url-loader "^4.1.1" webpack "^5.73.0" -"@excalidraw/excalidraw@0.15.2": - version "0.15.2" - resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.2.tgz#7dba4f6e10c52015a007efb75a9fc1afe598574c" - integrity sha512-rTI02kgWSTXiUdIkBxt9u/581F3eXcqQgJdIxmz54TFtG3ughoxO5fr4t7Fr2LZIturBPqfocQHGKZ0t2KLKgw== +"@excalidraw/excalidraw@0.15.3": + version "0.15.3" + resolved "https://registry.yarnpkg.com/@excalidraw/excalidraw/-/excalidraw-0.15.3.tgz#5dea570f76451adf68bc24d4bfdd67a375cfeab1" + integrity sha512-/gpY7fgMO/AEaFLWnPqzbY8H7ly+/zocFf7D0Is5sWNMD2mhult5tana12lXKLSJ6EAz7ubo1A7LajXzvJXJDA== "@hapi/hoek@^9.0.0": version "9.3.0" From 8101a351dbbf269b92b525e885e4dda5cdd51c2a Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 18 Aug 2023 00:28:26 +0200 Subject: [PATCH 17/23] fix: resetting deleted elements on duplication (#6906) --- src/components/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/App.tsx b/src/components/App.tsx index dad7a7b039..a975b89286 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5868,7 +5868,7 @@ class App extends React.Component { .map((element) => element.id), ); - const elements = this.scene.getNonDeletedElements(); + const elements = this.scene.getElementsIncludingDeleted(); for (const element of elements) { if ( From 9cd5e1591718c7a717d135d80c5146d9fbe31879 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 18 Aug 2023 16:14:57 +0200 Subject: [PATCH 18/23] fix: stabilize `selectedElementIds` when box selecting (#6912) --- src/groups.ts | 14 +++-- .../regressionTests.test.tsx.snap | 6 +-- src/tests/linearElementEditor.test.tsx | 8 +-- src/tests/selection.test.tsx | 53 +++++++++++++++++-- 4 files changed, 64 insertions(+), 17 deletions(-) diff --git a/src/groups.ts b/src/groups.ts index 8dbe2ba5c3..dd5512ba12 100644 --- a/src/groups.ts +++ b/src/groups.ts @@ -71,6 +71,7 @@ export const selectGroupsForSelectedElements = (function () { selectedElements: readonly NonDeleted[], elements: readonly NonDeleted[], appState: Pick, + prevAppState: InteractiveCanvasAppState, ): SelectGroupsReturnType => { if ( lastReturnValue !== undefined && @@ -134,10 +135,13 @@ export const selectGroupsForSelectedElements = (function () { lastReturnValue = { editingGroupId: appState.editingGroupId, selectedGroupIds, - selectedElementIds: { - ...appState.selectedElementIds, - ...selectedElementIdsInGroups, - }, + selectedElementIds: makeNextSelectedElementIds( + { + ...appState.selectedElementIds, + ...selectedElementIdsInGroups, + }, + prevAppState, + ), }; return lastReturnValue; @@ -181,7 +185,7 @@ export const selectGroupsForSelectedElements = (function () { }; } - return _selectGroups(selectedElements, elements, appState); + return _selectGroups(selectedElements, elements, appState, prevAppState); }; selectGroupsForSelectedElements.clearCache = () => { diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index e9bbfc7ba1..832f1652a3 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -1734,7 +1734,7 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of elements 1`] = `0`; -exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `32`; +exports[`regression tests > Cmd/Ctrl-click exclusively select element under pointer > [end of test] number of renders 1`] = `30`; exports[`regression tests > Drags selected element when hitting only bounding box and keeps element selected > [end of test] appState 1`] = ` { @@ -4609,7 +4609,7 @@ exports[`regression tests > click-drag to select a group > [end of test] history exports[`regression tests > click-drag to select a group > [end of test] number of elements 1`] = `0`; -exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `19`; +exports[`regression tests > click-drag to select a group > [end of test] number of renders 1`] = `18`; exports[`regression tests > deleting last but one element in editing group should unselect the group > [end of test] appState 1`] = ` { @@ -15305,7 +15305,7 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of elements 1`] = `0`; -exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `31`; +exports[`regression tests > single-clicking on a subgroup of a selected group should not alter selection > [end of test] number of renders 1`] = `30`; exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] appState 1`] = ` { diff --git a/src/tests/linearElementEditor.test.tsx b/src/tests/linearElementEditor.test.tsx index bedcb0dfa5..7c14b6ef71 100644 --- a/src/tests/linearElementEditor.test.tsx +++ b/src/tests/linearElementEditor.test.tsx @@ -275,7 +275,7 @@ describe("Test Linear Elements", () => { // drag line from midpoint drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]); expect(renderInteractiveScene).toHaveBeenCalledTimes(14); - expect(renderStaticScene).toHaveBeenCalledTimes(8); + expect(renderStaticScene).toHaveBeenCalledTimes(6); expect(line.points.length).toEqual(3); expect(line.points).toMatchInlineSnapshot(` @@ -418,7 +418,7 @@ describe("Test Linear Elements", () => { ]); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(11); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(line.points.length).toEqual(5); @@ -521,7 +521,7 @@ describe("Test Linear Elements", () => { deletePoint(points[2]); expect(line.points.length).toEqual(3); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(12); + expect(renderStaticScene).toHaveBeenCalledTimes(9); const newMidPoints = LinearElementEditor.getEditorMidPoints( line, @@ -568,7 +568,7 @@ describe("Test Linear Elements", () => { lastSegmentMidpoint[1] + delta, ]); expect(renderInteractiveScene).toHaveBeenCalledTimes(21); - expect(renderStaticScene).toHaveBeenCalledTimes(11); + expect(renderStaticScene).toHaveBeenCalledTimes(9); expect(line.points.length).toEqual(5); expect((h.elements[0] as ExcalidrawLinearElement).points) diff --git a/src/tests/selection.test.tsx b/src/tests/selection.test.tsx index 1afde34ad5..acae9dd8cd 100644 --- a/src/tests/selection.test.tsx +++ b/src/tests/selection.test.tsx @@ -308,7 +308,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(8); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -338,7 +338,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(8); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -368,7 +368,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(8); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -411,7 +411,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(8); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -453,7 +453,7 @@ describe("select single element on the scene", () => { fireEvent.pointerUp(canvas); expect(renderInteractiveScene).toHaveBeenCalledTimes(9); - expect(renderStaticScene).toHaveBeenCalledTimes(9); + expect(renderStaticScene).toHaveBeenCalledTimes(8); expect(h.state.selectionElement).toBeNull(); expect(h.elements.length).toEqual(1); expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy(); @@ -477,3 +477,46 @@ describe("tool locking & selection", () => { } }); }); + +describe("selectedElementIds stability", () => { + beforeEach(async () => { + await render(); + }); + + it("box-selection should be stable when not changing selection", () => { + const rectangle = API.createElement({ + type: "rectangle", + x: 0, + y: 0, + width: 10, + height: 10, + }); + + h.elements = [rectangle]; + + const selectedElementIds_1 = h.state.selectedElementIds; + + mouse.downAt(-100, -100); + mouse.moveTo(-50, -50); + mouse.up(); + + expect(h.state.selectedElementIds).toBe(selectedElementIds_1); + + mouse.downAt(-50, -50); + mouse.moveTo(50, 50); + + const selectedElementIds_2 = h.state.selectedElementIds; + + expect(selectedElementIds_2).toEqual({ [rectangle.id]: true }); + + mouse.moveTo(60, 60); + + // box-selecting further without changing selection should keep + // selectedElementIds stable (the same object) + expect(h.state.selectedElementIds).toBe(selectedElementIds_2); + + mouse.up(); + + expect(h.state.selectedElementIds).toBe(selectedElementIds_2); + }); +}); From de1ebad75534d915234cca83c3205acf5446cd2d Mon Sep 17 00:00:00 2001 From: David Luzar Date: Fri, 18 Aug 2023 16:34:01 +0200 Subject: [PATCH 19/23] fix: regression in indexing when adding elements to frame (#6904) --- src/frame.test.tsx | 128 +++++++++++++++++++++++++++++++++++++++ src/frame.ts | 12 +--- src/tests/helpers/api.ts | 11 ++++ 3 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 src/frame.test.tsx diff --git a/src/frame.test.tsx b/src/frame.test.tsx new file mode 100644 index 0000000000..e9562a6f39 --- /dev/null +++ b/src/frame.test.tsx @@ -0,0 +1,128 @@ +import { + convertToExcalidrawElements, + Excalidraw, +} from "./packages/excalidraw/index"; +import { API } from "./tests/helpers/api"; +import { Pointer } from "./tests/helpers/ui"; +import { render } from "./tests/test-utils"; + +const { h } = window; +const mouse = new Pointer("mouse"); + +describe("adding elements to frames", () => { + type ElementType = string; + const assertOrder = ( + els: readonly { type: ElementType }[], + order: ElementType[], + ) => { + expect(els.map((el) => el.type)).toEqual(order); + }; + + const reorderElements = ( + els: readonly T[], + order: ElementType[], + ) => { + return order.reduce((acc: T[], el) => { + acc.push(els.find((e) => e.type === el)!); + return acc; + }, []); + }; + + describe("resizing frame over elements", () => { + const testElements = async ( + containerType: "arrow" | "rectangle", + initialOrder: ElementType[], + expectedOrder: ElementType[], + ) => { + await render(); + + const frame = API.createElement({ type: "frame", x: 0, y: 0 }); + + h.elements = reorderElements( + [ + frame, + ...convertToExcalidrawElements([ + { + type: containerType, + x: 100, + y: 100, + height: 10, + label: { text: "xx" }, + }, + ]), + ], + initialOrder, + ); + + assertOrder(h.elements, initialOrder); + + expect(h.elements[1].frameId).toBe(null); + expect(h.elements[2].frameId).toBe(null); + + const container = h.elements[1]; + + mouse.clickAt(0, 0); + mouse.downAt(frame.x + frame.width, frame.y + frame.height); + mouse.moveTo( + container.x + container.width + 100, + container.y + container.height + 100, + ); + mouse.up(); + assertOrder(h.elements, expectedOrder); + + expect(h.elements[0].frameId).toBe(frame.id); + expect(h.elements[1].frameId).toBe(frame.id); + }; + + it("resizing over text containers / labelled arrows", async () => { + await testElements( + "rectangle", + ["frame", "rectangle", "text"], + ["rectangle", "text", "frame"], + ); + await testElements( + "rectangle", + ["frame", "text", "rectangle"], + ["rectangle", "text", "frame"], + ); + await testElements( + "rectangle", + ["rectangle", "text", "frame"], + ["rectangle", "text", "frame"], + ); + await testElements( + "rectangle", + ["text", "rectangle", "frame"], + ["text", "rectangle", "frame"], + ); + + await testElements( + "arrow", + ["frame", "arrow", "text"], + ["arrow", "text", "frame"], + ); + await testElements( + "arrow", + ["text", "arrow", "frame"], + ["text", "arrow", "frame"], + ); + + // FIXME failing in tests (it fails to add elements to frame for some + // reason) but works in browser. (╯°□°)╯︵ ┻━┻ + // + // Looks like the `getElementsCompletelyInFrame()` doesn't work + // in these cases. + // + // await testElements( + // "arrow", + // ["arrow", "text", "frame"], + // ["arrow", "text", "frame"], + // ); + // await testElements( + // "arrow", + // ["frame", "text", "arrow"], + // ["text", "arrow", "frame"], + // ); + }); + }); +}); diff --git a/src/frame.ts b/src/frame.ts index 7f0f42d7ae..d5599157cb 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -469,14 +469,6 @@ export const addElementsToFrame = ( } let nextElements = allElements.slice(); - // Optimisation since findIndex on "newElements" is slow - const nextElementsIndex = nextElements.reduce( - (acc: Record, element, index) => { - acc[element.id] = index; - return acc; - }, - {}, - ); const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id); for (const element of omitGroupsContainingFrames( @@ -492,8 +484,8 @@ export const addElementsToFrame = ( false, ); - const frameIndex = nextElementsIndex[frame.id] ?? -1; - const elementIndex = nextElementsIndex[element.id] ?? -1; + const frameIndex = findIndex(nextElements, (e) => e.id === frame.id); + const elementIndex = findIndex(nextElements, (e) => e.id === element.id); if (elementIndex < frameBoundary) { nextElements = [ diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index 46361cf381..1b1943de08 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -17,6 +17,7 @@ import path from "path"; import { getMimeType } from "../../data/blob"; import { newEmbeddableElement, + newFrameElement, newFreeDrawElement, newImageElement, } from "../../element/newElement"; @@ -24,6 +25,7 @@ import { Point } from "../../types"; import { getSelectedElements } from "../../scene/selection"; import { isLinearElementType } from "../../element/typeChecks"; import { Mutable } from "../../utility-types"; +import { assertNever } from "../../utils"; const readFile = util.promisify(fs.readFile); @@ -244,6 +246,15 @@ export class API { scale: rest.scale || [1, 1], }); break; + case "frame": + element = newFrameElement({ ...base, width, height }); + break; + default: + assertNever( + type, + `API.createElement: unimplemented element type ${type}}`, + ); + break; } if (element.type === "arrow") { element.startBinding = rest.startBinding ?? null; From 188921c24753e37aaa64939eea6ca4ec491bec29 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sun, 27 Aug 2023 19:30:47 +0200 Subject: [PATCH 20/23] fix: grid jittery after partition PR (#6935) --- src/renderer/renderScene.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/renderer/renderScene.ts b/src/renderer/renderScene.ts index 736edb99d8..0af732ce6f 100644 --- a/src/renderer/renderScene.ts +++ b/src/renderer/renderScene.ts @@ -934,10 +934,8 @@ const _renderStaticScene = ({ strokeGrid( context, appState.gridSize, - -Math.ceil(appState.zoom.value / appState.gridSize) * appState.gridSize + - (appState.scrollX % appState.gridSize), - -Math.ceil(appState.zoom.value / appState.gridSize) * appState.gridSize + - (appState.scrollY % appState.gridSize), + appState.scrollX, + appState.scrollY, appState.zoom, normalizedWidth / appState.zoom.value, normalizedHeight / appState.zoom.value, From 27fd150a2049e4634833daf620d7c7a90e7b8ec2 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 5 Sep 2023 12:06:48 +0200 Subject: [PATCH 21/23] fix: canvas flickering due to resetting canvas on skipped frames (#6960) --- src/components/canvases/StaticCanvas.tsx | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/components/canvases/StaticCanvas.tsx b/src/components/canvases/StaticCanvas.tsx index f32133f35f..dfdf8b51e1 100644 --- a/src/components/canvases/StaticCanvas.tsx +++ b/src/components/canvases/StaticCanvas.tsx @@ -37,10 +37,25 @@ const StaticCanvas = (props: StaticCanvasProps) => { canvas.classList.add("excalidraw__canvas", "static"); } - canvas.style.width = `${props.appState.width}px`; - canvas.style.height = `${props.appState.height}px`; - canvas.width = props.appState.width * props.scale; - canvas.height = props.appState.height * props.scale; + const widthString = `${props.appState.width}px`; + const heightString = `${props.appState.height}px`; + if (canvas.style.width !== widthString) { + canvas.style.width = widthString; + } + if (canvas.style.height !== heightString) { + canvas.style.height = heightString; + } + + const scaledWidth = props.appState.width * props.scale; + const scaledHeight = props.appState.height * props.scale; + // setting width/height resets the canvas even if dimensions not changed, + // which would cause flicker when we skip frame (due to throttling) + if (canvas.width !== scaledWidth) { + canvas.width = scaledWidth; + } + if (canvas.height !== scaledHeight) { + canvas.height = scaledHeight; + } renderStaticScene( { From 5191cdbe267cea32e3189d2900a2d84bb24effaa Mon Sep 17 00:00:00 2001 From: Alex Kim <45559664+alex-kim-dev@users.noreply.github.com> Date: Tue, 5 Sep 2023 20:50:27 +0500 Subject: [PATCH 22/23] fix: stale labeled arrow bounds cache after editing the label (#6893) * fix stale labeled arrow bounds cache after editing the label * add arrow bounds test * fix test to check the arrow version * fix * fix test - remove unused import * Update src/element/textWysiwyg.test.tsx --------- Co-authored-by: Aakansha Doshi --- src/element/textWysiwyg.test.tsx | 26 ++++++++++++++++++++++++++ src/element/textWysiwyg.tsx | 5 ++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/element/textWysiwyg.test.tsx b/src/element/textWysiwyg.test.tsx index a2301b9645..c855de3573 100644 --- a/src/element/textWysiwyg.test.tsx +++ b/src/element/textWysiwyg.test.tsx @@ -1509,4 +1509,30 @@ describe("textWysiwyg", () => { expect(text.text).toBe("Excalidraw"); }); }); + + it("should bump the version of labelled arrow when label updated", async () => { + await render(); + const arrow = UI.createElement("arrow", { + width: 300, + height: 0, + }); + + mouse.select(arrow); + Keyboard.keyPress(KEYS.ENTER); + let editor = getTextEditor(); + await new Promise((r) => setTimeout(r, 0)); + updateTextEditor(editor, "Hello"); + editor.blur(); + + const { version } = arrow; + + mouse.select(arrow); + Keyboard.keyPress(KEYS.ENTER); + editor = getTextEditor(); + await new Promise((r) => setTimeout(r, 0)); + updateTextEditor(editor, "Hello\nworld!"); + editor.blur(); + + expect(arrow.version).toEqual(version + 1); + }); }); diff --git a/src/element/textWysiwyg.tsx b/src/element/textWysiwyg.tsx index b60fdeed2b..d7fed6981d 100644 --- a/src/element/textWysiwyg.tsx +++ b/src/element/textWysiwyg.tsx @@ -20,7 +20,7 @@ import { ExcalidrawTextContainer, } from "./types"; import { AppState } from "../types"; -import { mutateElement } from "./mutateElement"; +import { bumpVersion, mutateElement } from "./mutateElement"; import { getBoundTextElementId, getContainerElement, @@ -541,6 +541,9 @@ export const textWysiwyg = ({ id: element.id, }), }); + } else if (isArrowElement(container)) { + // updating an arrow label may change bounds, prevent stale cache: + bumpVersion(container); } } else { mutateElement(container, { From 134df7bfbb749e6b150b7f1cc5d33b6fa4cb0b27 Mon Sep 17 00:00:00 2001 From: Aakansha Doshi Date: Wed, 6 Sep 2023 10:39:04 +0530 Subject: [PATCH 23/23] fix: update size-limit so react is not installed as dependency (#6964) --- src/packages/excalidraw/package.json | 4 +- src/packages/excalidraw/yarn.lock | 131 ++++++++++----------------- 2 files changed, 51 insertions(+), 84 deletions(-) diff --git a/src/packages/excalidraw/package.json b/src/packages/excalidraw/package.json index c449498f07..5e2e3583d6 100644 --- a/src/packages/excalidraw/package.json +++ b/src/packages/excalidraw/package.json @@ -52,7 +52,7 @@ "@babel/preset-env": "7.18.6", "@babel/preset-react": "7.18.6", "@babel/preset-typescript": "7.18.6", - "@size-limit/preset-big-lib": "8.2.6", + "@size-limit/preset-big-lib": "9.0.0", "autoprefixer": "10.4.7", "babel-loader": "8.2.5", "babel-plugin-transform-class-properties": "6.24.1", @@ -63,7 +63,7 @@ "mini-css-extract-plugin": "2.6.1", "postcss-loader": "7.0.1", "sass-loader": "13.0.2", - "size-limit": "8.2.4", + "size-limit": "9.0.0", "style-loader": "3.3.3", "terser-webpack-plugin": "5.3.3", "ts-loader": "9.3.1", diff --git a/src/packages/excalidraw/yarn.lock b/src/packages/excalidraw/yarn.lock index 91789d834e..293e596e8a 100644 --- a/src/packages/excalidraw/yarn.lock +++ b/src/packages/excalidraw/yarn.lock @@ -1112,38 +1112,37 @@ dependencies: debug "^4.1.1" -"@size-limit/file@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-8.2.6.tgz#0e17045a0fa8009fc787c85e3c09f611316f908c" - integrity sha512-B7ayjxiJsbtXdIIWazJkB5gezi5WBMecdHTFPMDhI3NwEML1RVvUjAkrb1mPAAkIpt2LVHPnhdCUHjqDdjugwg== +"@size-limit/file@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/file/-/file-9.0.0.tgz#eed5415f5bcc8407979e47ffa49ffaf12d2d2378" + integrity sha512-oM2UaH2FRq4q22k+R+P6xCpzET10T94LFdSjb9svVu/vOD7NaB9LGcG6se8TW1BExXiyXO4GEhLsBt3uMKM3qA== dependencies: - semver "7.5.3" + semver "7.5.4" -"@size-limit/preset-big-lib@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-8.2.6.tgz#fbff51e7a03fc36b6b3d9103cbe5b3909e35a83e" - integrity sha512-63a+yos0QNMVCfx1OWnxBrdQVTlBVGzW5fDXwpWq/hKfP3B89XXHYGeL2Z2f8IXSVeGkAHXnDcTZyIPRaXffVg== +"@size-limit/preset-big-lib@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/preset-big-lib/-/preset-big-lib-9.0.0.tgz#ddcf30e7646b66ecc0f8a1a6498a5eda6d82876d" + integrity sha512-wc+VNLXjn0z11s1IWevo8+utP7uZGPVDNNe5cNyMFYHv7/pwJtgsd8w2onEkbK1h8x1oJfWlcqFNKAnvD1Bylw== dependencies: - "@size-limit/file" "8.2.6" - "@size-limit/time" "8.2.6" - "@size-limit/webpack" "8.2.6" - size-limit "8.2.6" + "@size-limit/file" "9.0.0" + "@size-limit/time" "9.0.0" + "@size-limit/webpack" "9.0.0" + size-limit "9.0.0" -"@size-limit/time@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-8.2.6.tgz#5d1912bcfc6437f6f59804737ad0538b25c207ed" - integrity sha512-fUEPvz7Uq6+oUQxSYbNlJt3tTgQBl1VY21USi/B7ebdnVKLnUx1JyPI9v7imN6XEkB2VpJtnYgjFeLgNrirzMA== +"@size-limit/time@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/time/-/time-9.0.0.tgz#44ba75b3cba30736b133dbb3fd740f894a642c87" + integrity sha512-//Yba5fRkYqpBZ6MFtjDTSjCpQonDMqkwofpe0G1hMd/5l/3PZXVLDCAU2BW3nQFqTkpeyytFG6Y3jxUqSddiw== dependencies: estimo "^2.3.6" - react "^17.0.2" -"@size-limit/webpack@8.2.6": - version "8.2.6" - resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-8.2.6.tgz#3a3c98293b80f7c5fb6e8499199ae6f94f05b463" - integrity sha512-y2sB66m5sJxIjZ8SEAzpWbiw3/+bnQHDHfk9cSbV5ChKklq02AlYg8BS5KxGWmMpdyUo4TzpjSCP9oEudY+hxQ== +"@size-limit/webpack@9.0.0": + version "9.0.0" + resolved "https://registry.yarnpkg.com/@size-limit/webpack/-/webpack-9.0.0.tgz#4514851d3607490e228bf22bc95286643f64a490" + integrity sha512-0YwdvmBj9rS4bXE/PY9vSdc5lCiQXmT0794EsG7yvlDMWyrWa/dsgcRok/w0MoZstfuLaS6lv03VI5UJRFU/lg== dependencies: nanoid "^3.3.6" - webpack "^5.88.0" + webpack "^5.88.2" "@types/body-parser@*": version "1.19.2" @@ -1694,9 +1693,9 @@ ansi-styles@^4.1.0: color-convert "^2.0.1" anymatch@~3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" - integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== dependencies: normalize-path "^3.0.0" picomatch "^2.0.4" @@ -2553,9 +2552,9 @@ fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== fast-glob@^3.2.9: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== + version "3.3.1" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.1.tgz#784b4e897340f3dbbef17413b3f11acf03c874c4" + integrity sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== dependencies: "@nodelib/fs.stat" "^2.0.2" "@nodelib/fs.walk" "^1.2.3" @@ -2672,9 +2671,9 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== function-bind@^1.1.1: version "1.1.1" @@ -2993,7 +2992,7 @@ is-docker@^2.0.0, is-docker@^2.1.1: is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.3" @@ -3105,7 +3104,7 @@ klona@^2.0.4, klona@^2.0.5: resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.5.tgz#d166574d90076395d9963aa7a928fabb8d76afbc" integrity sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ== -lilconfig@^2.0.6, lilconfig@^2.1.0: +lilconfig@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== @@ -3146,7 +3145,7 @@ lodash@^4.17.20, lodash@^4.17.4: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== -loose-envify@^1.0.0, loose-envify@^1.1.0: +loose-envify@^1.0.0: version "1.4.0" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== @@ -3360,11 +3359,6 @@ npm-run-path@^4.0.1: dependencies: path-key "^3.0.0" -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - object-inspect@^1.9.0: version "1.12.0" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" @@ -3519,16 +3513,16 @@ picocolors@^1.0.0: resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3: - version "2.3.0" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" - integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== - -picomatch@^2.3.1: +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== +picomatch@^2.2.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.0.tgz#f1f061de8f6a4bf022892e2d128234fb98302972" + integrity sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw== + pkg-dir@4.2.0, pkg-dir@^4.1.0, pkg-dir@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" @@ -3685,14 +3679,6 @@ raw-body@2.5.1: iconv-lite "0.4.24" unpipe "1.0.0" -react@^17.0.2: - version "17.0.2" - resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" - integrity sha512-gnhPt75i/dq/z3/6q/0asP78D0u592D5L1pd7M8P+dck6Fu/jJeL6iVVK23fptSUZj8Vjf++7wXA8UNclGQcbA== - dependencies: - loose-envify "^1.1.0" - object-assign "^4.1.1" - readable-stream@^2.0.1: version "2.3.7" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" @@ -3927,10 +3913,10 @@ semver@7.0.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.5.3: - version "7.5.3" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.3.tgz#161ce8c2c6b4b3bdca6caadc9fa3317a4c4fe88e" - integrity sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ== +semver@7.5.4, semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: + version "7.5.4" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" + integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== dependencies: lru-cache "^6.0.0" @@ -3939,13 +3925,6 @@ semver@^6.0.0, semver@^6.1.1, semver@^6.1.2, semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.5, semver@^7.3.7: - version "7.5.4" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" - integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== - dependencies: - lru-cache "^6.0.0" - send@0.18.0: version "0.18.0" resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" @@ -4054,22 +4033,10 @@ sirv@^1.0.7: mime "^2.3.1" totalist "^1.0.0" -size-limit@8.2.4: - version "8.2.4" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.4.tgz#0ab0df7cbc89007d544a50b451f5fb4d110694ca" - integrity sha512-Un16nSreD1v2CYwSorattiJcHuAWqXvg4TsGgzpjnoByqQwsSfCIEQHuaD14HNStzredR8cdsO9oGH91ibypTA== - dependencies: - bytes-iec "^3.1.1" - chokidar "^3.5.3" - globby "^11.1.0" - lilconfig "^2.0.6" - nanospinner "^1.1.0" - picocolors "^1.0.0" - -size-limit@8.2.6: - version "8.2.6" - resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-8.2.6.tgz#e41dbc74a4d7fc13be72551b6ef31ea50007d18d" - integrity sha512-zpznim/tX/NegjoQuRKgWTF4XiB0cn2qt90uJzxYNTFAqexk4b94DOAkBD3TwhC6c3kw2r0KcnA5upziVMZqDg== +size-limit@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/size-limit/-/size-limit-9.0.0.tgz#203c47303462a8351976eb26175acea5f4e80447" + integrity sha512-DrA7o2DeRN3s+vwCA9nn7Ck9Y4pn9t0GNUwQRpKqBtBmNkl6LA2s/NlNCdtKHrEkRTeYA1ZQ65mnYveo9rUqgA== dependencies: bytes-iec "^3.1.1" chokidar "^3.5.3" @@ -4556,7 +4523,7 @@ webpack@5.76.0: watchpack "^2.4.0" webpack-sources "^3.2.3" -webpack@^5.88.0: +webpack@^5.88.2: version "5.88.2" resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.88.2.tgz#f62b4b842f1c6ff580f3fcb2ed4f0b579f4c210e" integrity sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==