From f2d2f97546d9ae2ba4ed024d548aced598a48243 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 29 Mar 2022 17:00:19 +0200 Subject: [PATCH 01/29] fix: using stale state when switching tools (#4989) --- src/components/Actions.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/Actions.tsx b/src/components/Actions.tsx index 347e34537..8806fe316 100644 --- a/src/components/Actions.tsx +++ b/src/components/Actions.tsx @@ -219,13 +219,17 @@ export const ShapesSwitcher = ({ penMode: true, }); } + const nextActiveTool = { ...activeTool, type: activeToolType }; setAppState({ - activeTool: { ...activeTool, type: activeToolType }, + activeTool: nextActiveTool, multiElement: null, selectedElementIds: {}, }); - setCursorForShape(canvas, { ...appState, activeTool }); - if (activeTool.type === "image") { + setCursorForShape(canvas, { + ...appState, + activeTool: nextActiveTool, + }); + if (activeToolType === "image") { onImageAction({ pointerType }); } }, From 734bb4d2ed3e7b1fac2f05cd5bca455d386bf4b9 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Tue, 29 Mar 2022 21:37:09 +0200 Subject: [PATCH 02/29] fix: decouple actionFinalize and actionErase (#4984) * Update actionCanvas.tsx * Update actionFinalize.tsx * lint * remove Escape trigger from actionErase * revert to lastActiveTool only if coming from eraser tool * unrelated: fix restoring `appState.activeTool` * one more restoring fix * fix tests Co-authored-by: dwelle --- src/actions/actionCanvas.tsx | 7 +------ src/actions/actionFinalize.tsx | 17 +++++++++++------ src/data/restore.ts | 10 +++++++--- src/tests/data/restore.test.ts | 4 +++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index dd88c73ce..8dc2c293c 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -323,12 +323,7 @@ export const actionErase = register({ commitToHistory: true, }; }, - keyTest: (event, appState) => { - return ( - event.key === KEYS.E || - (event.key === KEYS.ESCAPE && isEraserActive(appState)) - ); - }, + keyTest: (event) => event.key === KEYS.E, PanelComponent: ({ elements, appState, updateData, data }) => ( - !isEraserActive(appState) && - ((event.key === KEYS.ESCAPE && + (event.key === KEYS.ESCAPE && (appState.editingLinearElement !== null || (!appState.draggingElement && appState.multiElement === null))) || - ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && - appState.multiElement !== null)), + ((event.key === KEYS.ESCAPE || event.key === KEYS.ENTER) && + appState.multiElement !== null), PanelComponent: ({ appState, updateData, data }) => ( { stubImportedAppState, stubLocalAppState, ); - expect(restoredAppState.activeTool).toBe(stubImportedAppState.activeTool); + expect(restoredAppState.activeTool).toEqual( + stubImportedAppState.activeTool, + ); expect(restoredAppState.cursorButton).toBe( stubImportedAppState.cursorButton, ); From 9ba7ca38456094e699c885a37e800cbb537ee870 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 30 Mar 2022 10:53:22 +0200 Subject: [PATCH 03/29] feat: hide penMode button on reload if not enabled (#4992) --- src/data/restore.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/data/restore.ts b/src/data/restore.ts index 97f6f8a62..421223ef0 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -252,6 +252,10 @@ export const restoreAppState = ( } return { ...nextAppState, + // reset on fresh restore so as to hide the UI button if penMode not active + penDetected: + localAppState?.penDetected ?? + (appState.penMode ? appState.penDetected ?? false : false), activeTool: { lastActiveToolBeforeEraser: null, locked: nextAppState.activeTool.locked ?? false, From 880e4feedebab16c6265de97cb8073497f3a23fd Mon Sep 17 00:00:00 2001 From: Valerii Gusev Date: Fri, 1 Apr 2022 17:25:21 +0100 Subject: [PATCH 04/29] fix: update cursorButton once freedraw is released (#4996) Co-authored-by: dwelle --- src/actions/actionFinalize.tsx | 2 ++ src/data/restore.ts | 1 + .../__snapshots__/regressionTests.test.tsx.snap | 6 +++--- src/tests/data/restore.test.ts | 12 +++--------- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 9f081600e..4a7772207 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -39,6 +39,7 @@ export const actionFinalize = register({ : undefined, appState: { ...appState, + cursorButton: "up", editingLinearElement: null, }, commitToHistory: true, @@ -140,6 +141,7 @@ export const actionFinalize = register({ elements: newElements, appState: { ...appState, + cursorButton: "up", activeTool: (appState.activeTool.locked || appState.activeTool.type === "freedraw") && diff --git a/src/data/restore.ts b/src/data/restore.ts index 421223ef0..519544876 100644 --- a/src/data/restore.ts +++ b/src/data/restore.ts @@ -252,6 +252,7 @@ export const restoreAppState = ( } return { ...nextAppState, + cursorButton: localAppState?.cursorButton || "up", // reset on fresh restore so as to hide the UI button if penMode not active penDetected: localAppState?.penDetected ?? diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index e6ef53c1c..9b22aab36 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -6729,7 +6729,7 @@ Object { "currentItemStrokeStyle": "solid", "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", - "cursorButton": "down", + "cursorButton": "up", "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -11029,7 +11029,7 @@ Object { "currentItemStrokeStyle": "solid", "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", - "cursorButton": "down", + "cursorButton": "up", "draggingElement": null, "editingElement": null, "editingGroupId": null, @@ -12181,7 +12181,7 @@ Object { "currentItemStrokeStyle": "solid", "currentItemStrokeWidth": 1, "currentItemTextAlign": "left", - "cursorButton": "down", + "cursorButton": "up", "draggingElement": null, "editingElement": null, "editingGroupId": null, diff --git a/src/tests/data/restore.test.ts b/src/tests/data/restore.test.ts index 88647167a..3ce03e58d 100644 --- a/src/tests/data/restore.test.ts +++ b/src/tests/data/restore.test.ts @@ -313,9 +313,7 @@ describe("restoreAppState", () => { expect(restoredAppState.activeTool).toEqual( stubImportedAppState.activeTool, ); - expect(restoredAppState.cursorButton).toBe( - stubImportedAppState.cursorButton, - ); + expect(restoredAppState.cursorButton).toBe("up"); expect(restoredAppState.name).toBe(stubImportedAppState.name); }); @@ -347,9 +345,7 @@ describe("restoreAppState", () => { stubImportedAppState, null, ); - expect(restoredAppState.cursorButton).toBe( - stubImportedAppState.cursorButton, - ); + expect(restoredAppState.cursorButton).toBe("up"); expect(restoredAppState.name).toBe(stubImportedAppState.name); }); @@ -502,9 +498,7 @@ describe("restore", () => { importedDataState.appState = stubImportedAppState; const restoredData = restore.restore(importedDataState, null, null); - expect(restoredData.appState.cursorButton).toBe( - stubImportedAppState.cursorButton, - ); + expect(restoredData.appState.cursorButton).toBe("up"); expect(restoredData.appState.name).toBe(stubImportedAppState.name); }); From 873afdacd36abeef2aaf6a2f08d2cd5f3e8110c9 Mon Sep 17 00:00:00 2001 From: Achille Lacoin Date: Tue, 5 Apr 2022 14:35:38 +0200 Subject: [PATCH 05/29] feat: create and expose serializeLibraryAsJSON (#5009) Co-authored-by: David Luzar --- src/data/json.ts | 8 ++++++-- src/packages/excalidraw/CHANGELOG.md | 1 + src/packages/excalidraw/README_NEXT.md | 11 +++++++++++ src/packages/utils.ts | 2 +- 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/data/json.ts b/src/data/json.ts index e7ce5276b..fc52b8741 100644 --- a/src/data/json.ts +++ b/src/data/json.ts @@ -123,14 +123,18 @@ export const isValidLibrary = (json: any) => { ); }; -export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => { +export const serializeLibraryAsJSON = (libraryItems: LibraryItems) => { const data: ExportedLibraryData = { type: EXPORT_DATA_TYPES.excalidrawLibrary, version: VERSIONS.excalidrawLibrary, source: EXPORT_SOURCE, libraryItems, }; - const serialized = JSON.stringify(data, null, 2); + return JSON.stringify(data, null, 2); +}; + +export const saveLibraryAsJSON = async (libraryItems: LibraryItems) => { + const serialized = serializeLibraryAsJSON(libraryItems); await fileSave( new Blob([serialized], { type: MIME_TYPES.excalidrawlib, diff --git a/src/packages/excalidraw/CHANGELOG.md b/src/packages/excalidraw/CHANGELOG.md index 23badc8ec..31da04e06 100644 --- a/src/packages/excalidraw/CHANGELOG.md +++ b/src/packages/excalidraw/CHANGELOG.md @@ -18,6 +18,7 @@ Please add the latest change on the top under the correct section. #### Refactor - Rename `appState.elementLocked` to `appState.activeTool.locked` [#4983](https://github.com/excalidraw/excalidraw/pull/4983). +- Expose [`serializeLibraryAsJSON`](https://github.com/excalidraw/excalidraw/blob/master/src/packages/excalidraw/README.md#serializeLibraryAsJSON) helper that we use when saving Excalidraw Library to a file. ##### BREAKING CHANGE diff --git a/src/packages/excalidraw/README_NEXT.md b/src/packages/excalidraw/README_NEXT.md index 268e65fbf..aefa4c798 100644 --- a/src/packages/excalidraw/README_NEXT.md +++ b/src/packages/excalidraw/README_NEXT.md @@ -906,6 +906,17 @@ serializeAsJSON({ Takes the scene elements and state and returns a JSON string. Deleted `elements`as well as most properties from `AppState` are removed from the resulting JSON. (see [`serializeAsJSON()`](https://github.com/excalidraw/excalidraw/blob/master/src/data/json.ts#L16) source for details). +#### `serializeLibraryAsJSON` + +**_Signature_** + +
+serializeLibraryAsJSON({
+  libraryItems: LibraryItems[],
+
+ +Takes the library items and returns a JSON string. + #### `getSceneVersion` **How to use** diff --git a/src/packages/utils.ts b/src/packages/utils.ts index c09d10181..abb36dade 100644 --- a/src/packages/utils.ts +++ b/src/packages/utils.ts @@ -139,6 +139,6 @@ export const exportToSvg = async ({ ); }; -export { serializeAsJSON } from "../data/json"; +export { serializeAsJSON, serializeLibraryAsJSON } from "../data/json"; export { loadFromBlob, loadLibraryFromBlob } from "../data/blob"; export { getFreeDrawSvgPath } from "../renderer/renderElement"; From 670ceafc84e5ac63a86ddd22bfc4a4aedd92165b Mon Sep 17 00:00:00 2001 From: Faustino Kialungila Date: Tue, 5 Apr 2022 15:31:19 +0200 Subject: [PATCH 06/29] feat: Copy to clipboard all text nodes as text (#5013) * Copy to clipboard all text nodes as text * fix: only show the option for text elements --- src/actions/actionClipboard.tsx | 18 +++++++++++++++++- src/actions/index.ts | 1 + src/actions/types.ts | 1 + src/components/App.tsx | 15 +++++++++++++++ src/locales/en.json | 1 + 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 64c70532e..cceaf3535 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -1,11 +1,12 @@ import { CODES, KEYS } from "../keys"; import { register } from "./register"; -import { copyToClipboard } from "../clipboard"; +import { copyTextToSystemClipboard, copyToClipboard } from "../clipboard"; import { actionDeleteSelected } from "./actionDeleteSelected"; import { getSelectedElements } from "../scene/selection"; import { exportCanvas } from "../data/index"; import { getNonDeletedElements } from "../element"; import { t } from "../i18n"; +import { ExcalidrawTextElement } from "../element/types"; export const actionCopy = register({ name: "copy", @@ -126,3 +127,18 @@ export const actionCopyAsPng = register({ contextItemLabel: "labels.copyAsPng", keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey, }); + +export const copyAllTextNodesAsText = register({ + name: "copyAllTextNodesAsText", + trackEvent: { category: "element" }, + perform: (elements) => { + const text = ( + getNonDeletedElements(elements) as ExcalidrawTextElement[] + ).reduce((acc, element) => `${acc}${element.text}\n`, ""); + copyTextToSystemClipboard(text); + return { + commitToHistory: false, + }; + }, + contextItemLabel: "labels.copyAllTextNodesAsText", +}); diff --git a/src/actions/index.ts b/src/actions/index.ts index d9fdfa709..34f842108 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -75,6 +75,7 @@ export { actionCut, actionCopyAsPng, actionCopyAsSvg, + copyAllTextNodesAsText, } from "./actionClipboard"; export { actionToggleGridMode } from "./actionToggleGridMode"; diff --git a/src/actions/types.ts b/src/actions/types.ts index 029d1d25d..fe6044a36 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -41,6 +41,7 @@ export type ActionName = | "paste" | "copyAsPng" | "copyAsSvg" + | "copyAllTextNodesAsText" | "sendBackward" | "bringForward" | "sendToBack" diff --git a/src/components/App.tsx b/src/components/App.tsx index 9e287d270..a59a45d47 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -11,6 +11,7 @@ import { actionCopy, actionCopyAsPng, actionCopyAsSvg, + copyAllTextNodesAsText, actionCopyStyles, actionCut, actionDeleteSelected, @@ -5475,6 +5476,8 @@ class App extends React.Component { const elements = this.scene.getElements(); + const isTextNodesOnly = elements.every((element) => isTextElement(element)); + const options: ContextMenuOption[] = []; if (probablySupportsClipboardBlob && elements.length > 0) { options.push(actionCopyAsPng); @@ -5483,6 +5486,14 @@ class App extends React.Component { if (probablySupportsClipboardWriteText && elements.length > 0) { options.push(actionCopyAsSvg); } + + if ( + probablySupportsClipboardWriteText && + elements.length > 0 && + isTextNodesOnly + ) { + options.push(copyAllTextNodesAsText); + } if (type === "canvas") { const viewModeOptions = [ ...options, @@ -5526,6 +5537,10 @@ class App extends React.Component { probablySupportsClipboardWriteText && elements.length > 0 && actionCopyAsSvg, + probablySupportsClipboardWriteText && + elements.length > 0 && + isTextNodesOnly && + copyAllTextNodesAsText, ((probablySupportsClipboardBlob && elements.length > 0) || (probablySupportsClipboardWriteText && elements.length > 0)) && separator, diff --git a/src/locales/en.json b/src/locales/en.json index 58152c67a..08eef15ce 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -9,6 +9,7 @@ "copy": "Copy", "copyAsPng": "Copy to clipboard as PNG", "copyAsSvg": "Copy to clipboard as SVG", + "copyAllTextNodesAsText": "Copy to clipboard as a single text element", "bringForward": "Bring forward", "sendToBack": "Send to back", "bringToFront": "Bring to front", From 89471094ce01278b1994be31c9d554fd8dd4bd74 Mon Sep 17 00:00:00 2001 From: Faustino Kialungila Date: Tue, 5 Apr 2022 21:48:59 +0200 Subject: [PATCH 07/29] fix: Copy to clipboard all text nodes as text (#5014) * fix: Copy to clipboard all text nodes as text * fix: support copying text even if there are selected elements that are no text * patch: makes paragraphs betwen texts of each element * patch: allow copying text for bound text --- src/actions/actionClipboard.tsx | 20 ++++++++++++++------ src/components/App.tsx | 14 ++++++-------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index cceaf3535..473c56e32 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -4,9 +4,8 @@ import { copyTextToSystemClipboard, copyToClipboard } from "../clipboard"; import { actionDeleteSelected } from "./actionDeleteSelected"; import { getSelectedElements } from "../scene/selection"; import { exportCanvas } from "../data/index"; -import { getNonDeletedElements } from "../element"; +import { getNonDeletedElements, isTextElement } from "../element"; import { t } from "../i18n"; -import { ExcalidrawTextElement } from "../element/types"; export const actionCopy = register({ name: "copy", @@ -131,10 +130,19 @@ export const actionCopyAsPng = register({ export const copyAllTextNodesAsText = register({ name: "copyAllTextNodesAsText", trackEvent: { category: "element" }, - perform: (elements) => { - const text = ( - getNonDeletedElements(elements) as ExcalidrawTextElement[] - ).reduce((acc, element) => `${acc}${element.text}\n`, ""); + perform: (elements, appState) => { + const selectedElements = getSelectedElements( + getNonDeletedElements(elements), + appState, + true, + ); + + const text = selectedElements.reduce((acc, element) => { + if (isTextElement(element)) { + return `${acc}${element.text}\n\n`; + } + return acc; + }, ""); copyTextToSystemClipboard(text); return { commitToHistory: false, diff --git a/src/components/App.tsx b/src/components/App.tsx index a59a45d47..c9a141f39 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -5476,7 +5476,10 @@ class App extends React.Component { const elements = this.scene.getElements(); - const isTextNodesOnly = elements.every((element) => isTextElement(element)); + const selectedElements = getSelectedElements( + this.scene.getElements(), + this.state, + ); const options: ContextMenuOption[] = []; if (probablySupportsClipboardBlob && elements.length > 0) { @@ -5487,11 +5490,7 @@ class App extends React.Component { options.push(actionCopyAsSvg); } - if ( - probablySupportsClipboardWriteText && - elements.length > 0 && - isTextNodesOnly - ) { + if (probablySupportsClipboardWriteText && selectedElements.length > 0) { options.push(copyAllTextNodesAsText); } if (type === "canvas") { @@ -5538,8 +5537,7 @@ class App extends React.Component { elements.length > 0 && actionCopyAsSvg, probablySupportsClipboardWriteText && - elements.length > 0 && - isTextNodesOnly && + selectedElements.length > 0 && copyAllTextNodesAsText, ((probablySupportsClipboardBlob && elements.length > 0) || (probablySupportsClipboardWriteText && elements.length > 0)) && From 77d789ed8e68308468b65b5f0b990dfc4286a556 Mon Sep 17 00:00:00 2001 From: David Luzar Date: Tue, 5 Apr 2022 23:11:00 +0200 Subject: [PATCH 08/29] fix: more copyText fixes (#5016) --- src/actions/actionClipboard.tsx | 32 ++++++++++++++++++++++---------- src/actions/index.ts | 2 +- src/actions/types.ts | 2 +- src/components/App.tsx | 12 ++++++++---- src/locales/en.json | 2 +- 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/src/actions/actionClipboard.tsx b/src/actions/actionClipboard.tsx index 473c56e32..5d92290e2 100644 --- a/src/actions/actionClipboard.tsx +++ b/src/actions/actionClipboard.tsx @@ -1,6 +1,10 @@ import { CODES, KEYS } from "../keys"; import { register } from "./register"; -import { copyTextToSystemClipboard, copyToClipboard } from "../clipboard"; +import { + copyTextToSystemClipboard, + copyToClipboard, + probablySupportsClipboardWriteText, +} from "../clipboard"; import { actionDeleteSelected } from "./actionDeleteSelected"; import { getSelectedElements } from "../scene/selection"; import { exportCanvas } from "../data/index"; @@ -127,8 +131,8 @@ export const actionCopyAsPng = register({ keyTest: (event) => event.code === CODES.C && event.altKey && event.shiftKey, }); -export const copyAllTextNodesAsText = register({ - name: "copyAllTextNodesAsText", +export const copyText = register({ + name: "copyText", trackEvent: { category: "element" }, perform: (elements, appState) => { const selectedElements = getSelectedElements( @@ -137,16 +141,24 @@ export const copyAllTextNodesAsText = register({ true, ); - const text = selectedElements.reduce((acc, element) => { - if (isTextElement(element)) { - return `${acc}${element.text}\n\n`; - } - return acc; - }, ""); + const text = selectedElements + .reduce((acc: string[], element) => { + if (isTextElement(element)) { + acc.push(element.text); + } + return acc; + }, []) + .join("\n\n"); copyTextToSystemClipboard(text); return { commitToHistory: false, }; }, - contextItemLabel: "labels.copyAllTextNodesAsText", + contextItemPredicate: (elements, appState) => { + return ( + probablySupportsClipboardWriteText && + getSelectedElements(elements, appState, true).some(isTextElement) + ); + }, + contextItemLabel: "labels.copyText", }); diff --git a/src/actions/index.ts b/src/actions/index.ts index 34f842108..ffb20bc3f 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -75,7 +75,7 @@ export { actionCut, actionCopyAsPng, actionCopyAsSvg, - copyAllTextNodesAsText, + copyText, } from "./actionClipboard"; export { actionToggleGridMode } from "./actionToggleGridMode"; diff --git a/src/actions/types.ts b/src/actions/types.ts index fe6044a36..810f69c15 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -41,7 +41,7 @@ export type ActionName = | "paste" | "copyAsPng" | "copyAsSvg" - | "copyAllTextNodesAsText" + | "copyText" | "sendBackward" | "bringForward" | "sendToBack" diff --git a/src/components/App.tsx b/src/components/App.tsx index c9a141f39..6aaca2018 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -11,7 +11,7 @@ import { actionCopy, actionCopyAsPng, actionCopyAsSvg, - copyAllTextNodesAsText, + copyText, actionCopyStyles, actionCut, actionDeleteSelected, @@ -5490,8 +5490,12 @@ class App extends React.Component { options.push(actionCopyAsSvg); } - if (probablySupportsClipboardWriteText && selectedElements.length > 0) { - options.push(copyAllTextNodesAsText); + if ( + type === "element" && + copyText.contextItemPredicate(elements, this.state) && + probablySupportsClipboardWriteText + ) { + options.push(copyText); } if (type === "canvas") { const viewModeOptions = [ @@ -5538,7 +5542,7 @@ class App extends React.Component { actionCopyAsSvg, probablySupportsClipboardWriteText && selectedElements.length > 0 && - copyAllTextNodesAsText, + copyText, ((probablySupportsClipboardBlob && elements.length > 0) || (probablySupportsClipboardWriteText && elements.length > 0)) && separator, diff --git a/src/locales/en.json b/src/locales/en.json index 08eef15ce..5bcaa850a 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -9,7 +9,7 @@ "copy": "Copy", "copyAsPng": "Copy to clipboard as PNG", "copyAsSvg": "Copy to clipboard as SVG", - "copyAllTextNodesAsText": "Copy to clipboard as a single text element", + "copyText": "Copy to clipboard as text", "bringForward": "Bring forward", "sendToBack": "Send to back", "bringToFront": "Bring to front", From cb6b7559b431a7bdc869c268532a4ac772cb388a Mon Sep 17 00:00:00 2001 From: David Luzar Date: Wed, 6 Apr 2022 14:05:09 +0200 Subject: [PATCH 09/29] fix: support copying PNG to clipboard on Safari (#3746) --- src/clipboard.ts | 34 ++++++++++++++++++++++++++++++---- src/data/index.ts | 13 ++++++++++--- src/locales/en.json | 2 +- src/utils.ts | 12 ++++++++++++ 4 files changed, 53 insertions(+), 8 deletions(-) diff --git a/src/clipboard.ts b/src/clipboard.ts index 94a2dfa6b..5b61dae25 100644 --- a/src/clipboard.ts +++ b/src/clipboard.ts @@ -8,6 +8,7 @@ import { SVG_EXPORT_TAG } from "./scene/export"; import { tryParseSpreadsheet, Spreadsheet, VALID_SPREADSHEET } from "./charts"; import { EXPORT_DATA_TYPES, MIME_TYPES } from "./constants"; import { isInitializedImageElement } from "./element/typeChecks"; +import { isPromiseLike } from "./utils"; type ElementsClipboard = { type: typeof EXPORT_DATA_TYPES.excalidrawClipboard; @@ -166,10 +167,35 @@ export const parseClipboard = async ( } }; -export const copyBlobToClipboardAsPng = async (blob: Blob) => { - await navigator.clipboard.write([ - new window.ClipboardItem({ [MIME_TYPES.png]: blob }), - ]); +export const copyBlobToClipboardAsPng = async (blob: Blob | Promise) => { + let promise; + try { + // in Safari so far we need to construct the ClipboardItem synchronously + // (i.e. in the same tick) otherwise browser will complain for lack of + // user intent. Using a Promise ClipboardItem constructor solves this. + // https://bugs.webkit.org/show_bug.cgi?id=222262 + // + // not await so that we can detect whether the thrown error likely relates + // to a lack of support for the Promise ClipboardItem constructor + promise = navigator.clipboard.write([ + new window.ClipboardItem({ + [MIME_TYPES.png]: blob, + }), + ]); + } catch (error: any) { + // if we're using a Promise ClipboardItem, let's try constructing + // with resolution value instead + if (isPromiseLike(blob)) { + await navigator.clipboard.write([ + new window.ClipboardItem({ + [MIME_TYPES.png]: await blob, + }), + ]); + } else { + throw error; + } + } + await promise; }; export const copyTextToSystemClipboard = async (text: string | null) => { diff --git a/src/data/index.ts b/src/data/index.ts index 889664618..5cfaaa978 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -16,7 +16,7 @@ export { loadFromBlob } from "./blob"; export { loadFromJSON, saveAsJSON } from "./json"; export const exportCanvas = async ( - type: ExportType, + type: Omit, elements: readonly NonDeletedExcalidrawElement[], appState: AppState, files: BinaryFiles, @@ -73,10 +73,10 @@ export const exportCanvas = async ( }); tempCanvas.style.display = "none"; document.body.appendChild(tempCanvas); - let blob = await canvasToBlob(tempCanvas); - tempCanvas.remove(); if (type === "png") { + let blob = await canvasToBlob(tempCanvas); + tempCanvas.remove(); if (appState.exportEmbedScene) { blob = await ( await import(/* webpackChunkName: "image" */ "./image") @@ -94,12 +94,19 @@ export const exportCanvas = async ( }); } else if (type === "clipboard") { try { + const blob = canvasToBlob(tempCanvas); await copyBlobToClipboardAsPng(blob); } catch (error: any) { if (error.name === "CANVAS_POSSIBLY_TOO_BIG") { throw error; } throw new Error(t("alerts.couldNotCopyToClipboard")); + } finally { + tempCanvas.remove(); } + } else { + tempCanvas.remove(); + // shouldn't happen + throw new Error("Unsupported export type"); } }; diff --git a/src/locales/en.json b/src/locales/en.json index 5bcaa850a..def36a102 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -161,7 +161,7 @@ "couldNotLoadInvalidFile": "Couldn't load invalid file", "importBackendFailed": "Importing from backend failed.", "cannotExportEmptyCanvas": "Cannot export empty canvas.", - "couldNotCopyToClipboard": "Couldn't copy to clipboard. Try using Chrome browser.", + "couldNotCopyToClipboard": "Couldn't copy to clipboard.", "decryptFailed": "Couldn't decrypt data.", "uploadedSecurly": "The upload has been secured with end-to-end encryption, which means that Excalidraw server and third parties can't read the content.", "loadSceneOverridePrompt": "Loading external drawing will replace your existing content. Do you wish to continue?", diff --git a/src/utils.ts b/src/utils.ts index 7cc08d07b..91b67fd2b 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -625,3 +625,15 @@ export const getFrame = () => { return "iframe"; } }; + +export const isPromiseLike = ( + value: any, +): value is Promise> => { + return ( + !!value && + typeof value === "object" && + "then" in value && + "catch" in value && + "finally" in value + ); +}; From c2fce6d8c4eb0d4fc20a6853925f5e21aaa95eed Mon Sep 17 00:00:00 2001 From: Achille Lacoin Date: Thu, 7 Apr 2022 09:05:44 +0200 Subject: [PATCH 10/29] fix: export serializeLibraryAsJSON from the package (#5017) --- src/packages/excalidraw/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index f56711650..96f4741a2 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -183,6 +183,7 @@ export { exportToBlob, exportToSvg, serializeAsJSON, + serializeLibraryAsJSON, loadLibraryFromBlob, loadFromBlob, getFreeDrawSvgPath, From 327ed0e2d1c8f79fba7a46bcd6e8ee0404a2d0de Mon Sep 17 00:00:00 2001 From: Tom Sherman Date: Thu, 7 Apr 2022 12:43:29 +0100 Subject: [PATCH 11/29] feat: Element locking (#4964) Co-authored-by: dwelle Co-authored-by: Zsolt Viczian --- src/actions/actionSelectAll.ts | 3 +- src/actions/actionToggleLock.ts | 63 +++ src/actions/index.ts | 1 + src/actions/shortcuts.ts | 2 + src/actions/types.ts | 3 +- src/charts.ts | 1 + src/components/App.tsx | 55 ++- src/components/HelpDialog.tsx | 4 + src/data/restore.ts | 1 + src/element/binding.ts | 9 +- src/element/linearElementEditor.ts | 2 +- src/element/newElement.ts | 2 + src/element/transformHandles.ts | 7 + src/element/typeChecks.ts | 11 +- src/element/types.ts | 1 + src/locales/en.json | 10 +- src/scene/comparisons.ts | 2 +- src/scene/selection.ts | 1 + .../__snapshots__/contextmenu.test.tsx.snap | 93 +++++ .../__snapshots__/dragCreate.test.tsx.snap | 5 + src/tests/__snapshots__/move.test.tsx.snap | 6 + .../multiPointCreate.test.tsx.snap | 2 + .../regressionTests.test.tsx.snap | 362 ++++++++++++++++ .../__snapshots__/selection.test.tsx.snap | 5 + src/tests/contextmenu.test.tsx | 43 +- .../data/__snapshots__/restore.test.ts.snap | 9 + src/tests/elementLocking.test.tsx | 388 ++++++++++++++++++ src/tests/fixtures/elementFixture.ts | 1 + src/tests/helpers/api.ts | 11 +- src/tests/helpers/ui.ts | 14 + .../scene/__snapshots__/export.test.ts.snap | 2 +- 31 files changed, 1066 insertions(+), 53 deletions(-) create mode 100644 src/actions/actionToggleLock.ts create mode 100644 src/tests/elementLocking.test.tsx diff --git a/src/actions/actionSelectAll.ts b/src/actions/actionSelectAll.ts index 6b793b933..09c08d72b 100644 --- a/src/actions/actionSelectAll.ts +++ b/src/actions/actionSelectAll.ts @@ -18,7 +18,8 @@ export const actionSelectAll = register({ selectedElementIds: elements.reduce((map, element) => { if ( !element.isDeleted && - !(isTextElement(element) && element.containerId) + !(isTextElement(element) && element.containerId) && + element.locked === false ) { map[element.id] = true; } diff --git a/src/actions/actionToggleLock.ts b/src/actions/actionToggleLock.ts new file mode 100644 index 000000000..0f49e798d --- /dev/null +++ b/src/actions/actionToggleLock.ts @@ -0,0 +1,63 @@ +import { newElementWith } from "../element/mutateElement"; +import { ExcalidrawElement } from "../element/types"; +import { KEYS } from "../keys"; +import { getSelectedElements } from "../scene"; +import { arrayToMap } from "../utils"; +import { register } from "./register"; + +export const actionToggleLock = register({ + name: "toggleLock", + trackEvent: { category: "element" }, + perform: (elements, appState) => { + const selectedElements = getSelectedElements(elements, appState, true); + + if (!selectedElements.length) { + return false; + } + + const operation = getOperation(selectedElements); + const selectedElementsMap = arrayToMap(selectedElements); + + return { + elements: elements.map((element) => { + if (!selectedElementsMap.has(element.id)) { + return element; + } + + return newElementWith(element, { locked: operation === "lock" }); + }), + appState, + commitToHistory: true, + }; + }, + contextItemLabel: (elements, appState) => { + const selected = getSelectedElements(elements, appState, false); + if (selected.length === 1) { + return selected[0].locked + ? "labels.elementLock.unlock" + : "labels.elementLock.lock"; + } + + if (selected.length > 1) { + return getOperation(selected) === "lock" + ? "labels.elementLock.lockAll" + : "labels.elementLock.unlockAll"; + } + + throw new Error( + "Unexpected zero elements to lock/unlock. This should never happen.", + ); + }, + keyTest: (event, appState, elements) => { + return ( + event.key.toLocaleLowerCase() === KEYS.L && + event[KEYS.CTRL_OR_CMD] && + event.shiftKey && + getSelectedElements(elements, appState, false).length > 0 + ); + }, +}); + +const getOperation = ( + elements: readonly ExcalidrawElement[], +): "lock" | "unlock" => (elements.some((el) => !el.locked) ? "lock" : "unlock"); diff --git a/src/actions/index.ts b/src/actions/index.ts index ffb20bc3f..d8ec67635 100644 --- a/src/actions/index.ts +++ b/src/actions/index.ts @@ -84,3 +84,4 @@ export { actionToggleZenMode } from "./actionToggleZenMode"; export { actionToggleStats } from "./actionToggleStats"; export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionLink } from "../element/Hyperlink"; +export { actionToggleLock } from "./actionToggleLock"; diff --git a/src/actions/shortcuts.ts b/src/actions/shortcuts.ts index 2227dc1d9..2d37f20c2 100644 --- a/src/actions/shortcuts.ts +++ b/src/actions/shortcuts.ts @@ -29,6 +29,7 @@ export type ShortcutName = SubtypeOf< | "flipHorizontal" | "flipVertical" | "hyperlink" + | "toggleLock" >; const shortcutMap: Record = { @@ -67,6 +68,7 @@ const shortcutMap: Record = { flipVertical: [getShortcutKey("Shift+V")], viewMode: [getShortcutKey("Alt+R")], hyperlink: [getShortcutKey("CtrlOrCmd+K")], + toggleLock: [getShortcutKey("CtrlOrCmd+Shift+L")], }; export const getShortcutFromShortcutName = (name: ShortcutName) => { diff --git a/src/actions/types.ts b/src/actions/types.ts index 810f69c15..f9cd9186f 100644 --- a/src/actions/types.ts +++ b/src/actions/types.ts @@ -111,7 +111,8 @@ export type ActionName = | "unbindText" | "hyperlink" | "eraser" - | "bindText"; + | "bindText" + | "toggleLock"; export type PanelComponentProps = { elements: readonly ExcalidrawElement[]; diff --git a/src/charts.ts b/src/charts.ts index 1479bce68..4e057b9dc 100644 --- a/src/charts.ts +++ b/src/charts.ts @@ -167,6 +167,7 @@ const commonProps = { strokeStyle: "solid", strokeWidth: 1, verticalAlign: VERTICAL_ALIGN.MIDDLE, + locked: false, } as const; const getChartDimentions = (spreadsheet: Spreadsheet) => { diff --git a/src/components/App.tsx b/src/components/App.tsx index 6aaca2018..ea3969d8d 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -31,6 +31,7 @@ import { actionBindText, actionUngroup, actionLink, + actionToggleLock, } from "../actions"; import { createRedoAction, createUndoAction } from "../actions/actionHistory"; import { ActionManager } from "../actions/manager"; @@ -1134,7 +1135,7 @@ class App extends React.Component { prevState.activeTool !== this.state.activeTool && multiElement != null && isBindingEnabled(this.state) && - isBindingElement(multiElement) + isBindingElement(multiElement, false) ) { maybeBindLinearElement( multiElement, @@ -1546,6 +1547,7 @@ class App extends React.Component { fontFamily: this.state.currentItemFontFamily, textAlign: this.state.currentItemTextAlign, verticalAlign: DEFAULT_VERTICAL_ALIGN, + locked: false, }); this.scene.replaceAllElements([ @@ -2126,12 +2128,14 @@ class App extends React.Component { of all hit elements */ preferSelected?: boolean; includeBoundTextElement?: boolean; + includeLockedElements?: boolean; }, ): NonDeleted | null { const allHitElements = this.getElementsAtPosition( x, y, opts?.includeBoundTextElement, + opts?.includeLockedElements, ); if (allHitElements.length > 1) { if (opts?.preferSelected) { @@ -2164,14 +2168,19 @@ class App extends React.Component { x: number, y: number, includeBoundTextElement: boolean = false, + includeLockedElements: boolean = false, ): NonDeleted[] { - const elements = includeBoundTextElement - ? this.scene.getElements() - : this.scene - .getElements() - .filter( - (element) => !(isTextElement(element) && element.containerId), - ); + const elements = + includeBoundTextElement && includeLockedElements + ? this.scene.getElements() + : this.scene + .getElements() + .filter( + (element) => + (includeLockedElements || !element.locked) && + (includeBoundTextElement || + !(isTextElement(element) && element.containerId)), + ); return getElementsAtPosition(elements, (element) => hitTest(element, this.state, x, y), @@ -2213,7 +2222,7 @@ class App extends React.Component { if (selectedElements.length === 1) { if (isTextElement(selectedElements[0])) { existingTextElement = selectedElements[0]; - } else if (isTextBindableContainer(selectedElements[0])) { + } else if (isTextBindableContainer(selectedElements[0], false)) { container = selectedElements[0]; existingTextElement = getBoundTextElement(container); } @@ -2233,7 +2242,8 @@ class App extends React.Component { this.scene .getElements() .filter( - (ele) => isTextBindableContainer(ele) && !getBoundTextElement(ele), + (ele) => + isTextBindableContainer(ele, false) && !getBoundTextElement(ele), ), sceneX, sceneY, @@ -2291,6 +2301,7 @@ class App extends React.Component { : DEFAULT_VERTICAL_ALIGN, containerId: container?.id ?? undefined, groupIds: container?.groupIds ?? [], + locked: false, }); this.setState({ editingElement: element }); @@ -2597,7 +2608,7 @@ class App extends React.Component { // Hovering with a selected tool or creating new linear element via click // and point const { draggingElement } = this.state; - if (isBindingElement(draggingElement)) { + if (isBindingElement(draggingElement, false)) { this.maybeSuggestBindingsForLinearElementAtCoords( draggingElement, [scenePointer], @@ -2780,7 +2791,8 @@ class App extends React.Component { this.isHittingCommonBoundingBoxOfSelectedElements( scenePointer, selectedElements, - )) + )) && + !hitElement?.locked ) { setCursor(this.canvas, CURSOR_TYPE.MOVE); } else { @@ -2796,6 +2808,10 @@ class App extends React.Component { ) => { const updateElementIds = (elements: ExcalidrawElement[]) => { elements.forEach((element) => { + if (element.locked) { + return; + } + idsToUpdate.push(element.id); if (event.altKey) { if ( @@ -3617,6 +3633,7 @@ class App extends React.Component { opacity: this.state.currentItemOpacity, strokeSharpness: this.state.currentItemLinearStrokeSharpness, simulatePressure: event.pressure === 0.5, + locked: false, }); this.setState((prevState) => ({ @@ -3672,6 +3689,7 @@ class App extends React.Component { roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, strokeSharpness: this.state.currentItemLinearStrokeSharpness, + locked: false, }); return element; @@ -3759,6 +3777,7 @@ class App extends React.Component { strokeSharpness: this.state.currentItemLinearStrokeSharpness, startArrowhead, endArrowhead, + locked: false, }); this.setState((prevState) => ({ selectedElementIds: { @@ -3807,6 +3826,7 @@ class App extends React.Component { roughness: this.state.currentItemRoughness, opacity: this.state.currentItemOpacity, strokeSharpness: this.state.currentItemStrokeSharpness, + locked: false, }); if (element.type === "selection") { @@ -4106,7 +4126,7 @@ class App extends React.Component { }); } - if (isBindingElement(draggingElement)) { + if (isBindingElement(draggingElement, false)) { // When creating a linear element by dragging this.maybeSuggestBindingsForLinearElementAtCoords( draggingElement, @@ -4385,7 +4405,7 @@ class App extends React.Component { } else if (pointerDownState.drag.hasOccurred && !multiElement) { if ( isBindingEnabled(this.state) && - isBindingElement(draggingElement) + isBindingElement(draggingElement, false) ) { maybeBindLinearElement( draggingElement, @@ -5303,7 +5323,10 @@ class App extends React.Component { } const { x, y } = viewportCoordsToSceneCoords(event, this.state); - const element = this.getElementAtPosition(x, y, { preferSelected: true }); + const element = this.getElementAtPosition(x, y, { + preferSelected: true, + includeLockedElements: true, + }); const type = element ? "element" : "canvas"; @@ -5615,6 +5638,8 @@ class App extends React.Component { (maybeFlipHorizontal || maybeFlipVertical) && separator, actionLink.contextItemPredicate(elements, this.state) && actionLink, actionDuplicateSelection, + actionToggleLock, + separator, actionDeleteSelected, ], top, diff --git a/src/components/HelpDialog.tsx b/src/components/HelpDialog.tsx index 66e90d55a..34d24f483 100644 --- a/src/components/HelpDialog.tsx +++ b/src/components/HelpDialog.tsx @@ -363,6 +363,10 @@ export const HelpDialog = ({ onClose }: { onClose?: () => void }) => { getShortcutKey(`Alt+${t("helpDialog.drag")}`), ]} /> + - isBindableElement(element) && bindingBorderTest(element, pointerCoords), + isBindableElement(element, false) && + bindingBorderTest(element, pointerCoords), ); return hoveredElement as NonDeleted | null; }; @@ -456,13 +457,13 @@ export const getEligibleElementsForBinding = ( ): SuggestedBinding[] => { const includedElementIds = new Set(elements.map(({ id }) => id)); return elements.flatMap((element) => - isBindingElement(element) + isBindingElement(element, false) ? (getElligibleElementsForBindingElement( element as NonDeleted, ).filter( (element) => !includedElementIds.has(element.id), ) as SuggestedBinding[]) - : isBindableElement(element) + : isBindableElement(element, false) ? getElligibleElementsForBindableElementAndWhere(element).filter( (binding) => !includedElementIds.has(binding[0].id), ) @@ -508,7 +509,7 @@ const getElligibleElementsForBindableElementAndWhere = ( return Scene.getScene(bindableElement)! .getElements() .map((element) => { - if (!isBindingElement(element)) { + if (!isBindingElement(element, false)) { return null; } const canBindStart = isLinearElementEligibleForNewBindingByBindable( diff --git a/src/element/linearElementEditor.ts b/src/element/linearElementEditor.ts index 51ddc4926..b2566381b 100644 --- a/src/element/linearElementEditor.ts +++ b/src/element/linearElementEditor.ts @@ -205,7 +205,7 @@ export class LinearElementEditor { ); // suggest bindings for first and last point if selected - if (isBindingElement(element)) { + if (isBindingElement(element, false)) { const coords: { x: number; y: number }[] = []; const firstSelectedIndex = selectedPointsIndices[0]; diff --git a/src/element/newElement.ts b/src/element/newElement.ts index 11a0f23dd..234e06c39 100644 --- a/src/element/newElement.ts +++ b/src/element/newElement.ts @@ -56,6 +56,7 @@ const _newElementBase = ( strokeSharpness, boundElements = null, link = null, + locked, ...rest }: ElementConstructorOpts & Omit, "type">, ) => { @@ -83,6 +84,7 @@ const _newElementBase = ( boundElements, updated: getUpdatedTimestamp(), link, + locked, }; return element; }; diff --git a/src/element/transformHandles.ts b/src/element/transformHandles.ts index 4350891b1..f93f74d8a 100644 --- a/src/element/transformHandles.ts +++ b/src/element/transformHandles.ts @@ -222,6 +222,13 @@ export const getTransformHandles = ( zoom: Zoom, pointerType: PointerType = "mouse", ): TransformHandles => { + // so that when locked element is selected (especially when you toggle lock + // via keyboard) the locked element is visually distinct, indicating + // you can't move/resize + if (element.locked) { + return {}; + } + let omitSides: { [T in TransformHandleType]?: boolean } = {}; if ( element.type === "arrow" || diff --git a/src/element/typeChecks.ts b/src/element/typeChecks.ts index 58d9492fd..fdebca875 100644 --- a/src/element/typeChecks.ts +++ b/src/element/typeChecks.ts @@ -70,8 +70,13 @@ export const isLinearElementType = ( export const isBindingElement = ( element?: ExcalidrawElement | null, + includeLocked = true, ): element is ExcalidrawLinearElement => { - return element != null && isBindingElementType(element.type); + return ( + element != null && + (!element.locked || includeLocked === true) && + isBindingElementType(element.type) + ); }; export const isBindingElementType = ( @@ -82,9 +87,11 @@ export const isBindingElementType = ( export const isBindableElement = ( element: ExcalidrawElement | null, + includeLocked = true, ): element is ExcalidrawBindableElement => { return ( element != null && + (!element.locked || includeLocked === true) && (element.type === "rectangle" || element.type === "diamond" || element.type === "ellipse" || @@ -95,9 +102,11 @@ export const isBindableElement = ( export const isTextBindableContainer = ( element: ExcalidrawElement | null, + includeLocked = true, ): element is ExcalidrawTextContainer => { return ( element != null && + (!element.locked || includeLocked === true) && (element.type === "rectangle" || element.type === "diamond" || element.type === "ellipse" || diff --git a/src/element/types.ts b/src/element/types.ts index f7177e44c..0834efd6c 100644 --- a/src/element/types.ts +++ b/src/element/types.ts @@ -55,6 +55,7 @@ type _ExcalidrawElementBase = Readonly<{ /** epoch (ms) timestamp of last element update */ updated: number; link: string | null; + locked: boolean; }>; export type ExcalidrawSelectionElement = _ExcalidrawElementBase & { diff --git a/src/locales/en.json b/src/locales/en.json index def36a102..7e69365c1 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -113,6 +113,12 @@ "edit": "Edit link", "create": "Create link", "label": "Link" + }, + "elementLock": { + "lock": "Lock", + "unlock": "Unlock", + "lockAll": "Lock all", + "unlockAll": "Unlock all" } }, "buttons": { @@ -292,7 +298,8 @@ "title": "Help", "view": "View", "zoomToFit": "Zoom to fit all elements", - "zoomToSelection": "Zoom to selection" + "zoomToSelection": "Zoom to selection", + "toggleElementLock": "Lock/unlock selection" }, "clearCanvasDialog": { "title": "Clear canvas" @@ -336,7 +343,6 @@ "noteItems": "Each library item must have its own name so it's filterable. The following library items will be included:", "atleastOneLibItem": "Please select at least one library item to get started" }, - "publishSuccessDialog": { "title": "Library submitted", "content": "Thank you {{authorName}}. Your library has been submitted for review. You can track the status", diff --git a/src/scene/comparisons.ts b/src/scene/comparisons.ts index 57b3672d6..d94be0a69 100644 --- a/src/scene/comparisons.ts +++ b/src/scene/comparisons.ts @@ -91,5 +91,5 @@ export const getTextBindableContainerAtPosition = ( break; } } - return isTextBindableContainer(hitElement) ? hitElement : null; + return isTextBindableContainer(hitElement, false) ? hitElement : null; }; diff --git a/src/scene/selection.ts b/src/scene/selection.ts index a5abf7e43..39d3518b1 100644 --- a/src/scene/selection.ts +++ b/src/scene/selection.ts @@ -17,6 +17,7 @@ export const getElementsWithinSelection = ( getElementBounds(element); return ( + element.locked === false && element.type !== "selection" && !isBoundToContainer(element) && selectionX1 <= elementX1 && diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index 7d5029a9f..9451f7dc4 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -94,6 +94,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -149,6 +150,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -268,6 +270,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -296,6 +299,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -351,6 +355,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -390,6 +395,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -415,6 +421,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -454,6 +461,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -479,6 +487,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -598,6 +607,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -626,6 +636,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -681,6 +692,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -720,6 +732,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -745,6 +758,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -784,6 +798,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -809,6 +824,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -928,6 +944,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -983,6 +1000,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1100,6 +1118,7 @@ Object { "id": "id0", "isDeleted": true, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1155,6 +1174,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1192,6 +1212,7 @@ Object { "id": "id0", "isDeleted": true, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1311,6 +1332,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1339,6 +1361,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -1394,6 +1417,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1433,6 +1457,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1458,6 +1483,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -1585,6 +1611,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1615,6 +1642,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -1670,6 +1698,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1709,6 +1738,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1734,6 +1764,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -1779,6 +1810,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -1806,6 +1838,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -1925,6 +1958,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 60, "roughness": 2, "seed": 1278240551, @@ -1953,6 +1987,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 60, "roughness": 2, "seed": 400692809, @@ -2008,6 +2043,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2047,6 +2083,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2072,6 +2109,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2111,6 +2149,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2136,6 +2175,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2175,6 +2215,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2200,6 +2241,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2239,6 +2281,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2264,6 +2307,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2303,6 +2347,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2328,6 +2373,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2367,6 +2413,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2392,6 +2439,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2431,6 +2479,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2456,6 +2505,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 2, "seed": 400692809, @@ -2495,6 +2545,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2520,6 +2571,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 60, "roughness": 2, "seed": 400692809, @@ -2559,6 +2611,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 60, "roughness": 2, "seed": 1278240551, @@ -2584,6 +2637,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 60, "roughness": 2, "seed": 400692809, @@ -2703,6 +2757,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2731,6 +2786,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2786,6 +2842,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2825,6 +2882,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -2850,6 +2908,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2889,6 +2948,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -2914,6 +2974,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3033,6 +3094,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3061,6 +3123,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3116,6 +3179,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3155,6 +3219,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3180,6 +3245,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3219,6 +3285,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3244,6 +3311,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3367,6 +3435,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -3395,6 +3464,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -3450,6 +3520,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -3489,6 +3560,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -3514,6 +3586,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -3559,6 +3632,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -3586,6 +3660,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -3627,6 +3702,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -3652,6 +3728,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -3777,6 +3854,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3805,6 +3883,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3860,6 +3939,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3899,6 +3979,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3924,6 +4005,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -4053,6 +4135,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4083,6 +4166,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -4138,6 +4222,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4177,6 +4262,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4202,6 +4288,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -4248,6 +4335,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4275,6 +4363,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -4582,6 +4671,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -4610,6 +4700,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4638,6 +4729,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -4693,6 +4785,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, diff --git a/src/tests/__snapshots__/dragCreate.test.tsx.snap b/src/tests/__snapshots__/dragCreate.test.tsx.snap index 92848d4d5..ef7b8a685 100644 --- a/src/tests/__snapshots__/dragCreate.test.tsx.snap +++ b/src/tests/__snapshots__/dragCreate.test.tsx.snap @@ -16,6 +16,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -58,6 +59,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -88,6 +90,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -119,6 +122,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -161,6 +165,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, diff --git a/src/tests/__snapshots__/move.test.tsx.snap b/src/tests/__snapshots__/move.test.tsx.snap index 7971a53c4..68a733837 100644 --- a/src/tests/__snapshots__/move.test.tsx.snap +++ b/src/tests/__snapshots__/move.test.tsx.snap @@ -11,6 +11,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -39,6 +40,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -67,6 +69,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -100,6 +103,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -133,6 +137,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -168,6 +173,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ diff --git a/src/tests/__snapshots__/multiPointCreate.test.tsx.snap b/src/tests/__snapshots__/multiPointCreate.test.tsx.snap index 8fa46f1fe..2289ec869 100644 --- a/src/tests/__snapshots__/multiPointCreate.test.tsx.snap +++ b/src/tests/__snapshots__/multiPointCreate.test.tsx.snap @@ -17,6 +17,7 @@ Object { 110, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -67,6 +68,7 @@ Object { 110, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 9b22aab36..d8566f5ce 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -105,6 +105,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -135,6 +136,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -165,6 +167,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -220,6 +223,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -259,6 +263,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -284,6 +289,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -323,6 +329,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -348,6 +355,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -373,6 +381,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -417,6 +426,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -444,6 +454,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -471,6 +482,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -608,6 +620,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -638,6 +651,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -668,6 +682,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -723,6 +738,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -762,6 +778,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -787,6 +804,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -826,6 +844,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -851,6 +870,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -876,6 +896,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -919,6 +940,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -946,6 +968,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -973,6 +996,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -1096,6 +1120,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1127,6 +1152,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1157,6 +1183,7 @@ Object { "id": "id7", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -1212,6 +1239,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1251,6 +1279,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1276,6 +1305,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1322,6 +1352,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1349,6 +1380,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1391,6 +1423,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1418,6 +1451,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1459,6 +1493,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1486,6 +1521,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1511,6 +1547,7 @@ Object { "id": "id7", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -1559,6 +1596,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1587,6 +1625,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1614,6 +1653,7 @@ Object { "id": "id7", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -1657,6 +1697,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1685,6 +1726,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1712,6 +1754,7 @@ Object { "id": "id7", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -1755,6 +1798,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1783,6 +1827,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -1810,6 +1855,7 @@ Object { "id": "id7", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -1932,6 +1978,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -1987,6 +2034,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2027,6 +2075,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2154,6 +2203,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -2184,6 +2234,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2214,6 +2265,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -2269,6 +2321,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2308,6 +2361,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2333,6 +2387,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -2372,6 +2427,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2397,6 +2453,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -2422,6 +2479,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -2466,6 +2524,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -2493,6 +2552,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2520,6 +2580,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -2642,6 +2703,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -2670,6 +2732,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2725,6 +2788,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2765,6 +2829,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -2790,6 +2855,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2909,6 +2975,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -2964,6 +3031,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3086,6 +3154,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3114,6 +3183,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3142,6 +3212,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 2019559783, @@ -3197,6 +3268,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3236,6 +3308,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3261,6 +3334,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3300,6 +3374,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3325,6 +3400,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3350,6 +3426,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 2019559783, @@ -3390,6 +3467,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -3415,6 +3493,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 453191, @@ -3440,6 +3519,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 2019559783, @@ -3559,6 +3639,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3614,6 +3695,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3653,6 +3735,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3692,6 +3775,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3814,6 +3898,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3869,6 +3954,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -3909,6 +3995,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4033,6 +4120,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4088,6 +4176,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4128,6 +4217,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4169,6 +4259,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4291,6 +4382,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4319,6 +4411,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4374,6 +4467,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4413,6 +4507,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4438,6 +4533,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4561,6 +4657,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4589,6 +4686,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4617,6 +4715,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -4672,6 +4771,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4711,6 +4811,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4736,6 +4837,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4775,6 +4877,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -4800,6 +4903,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -4825,6 +4929,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -4884,6 +4989,7 @@ Object { "id": "id3", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 2019559783, @@ -4950,6 +5056,7 @@ Object { "id": "id3", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 2019559783, @@ -4994,6 +5101,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5022,6 +5130,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5077,6 +5186,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5116,6 +5226,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5141,6 +5252,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5200,6 +5312,7 @@ Object { "id": "id3", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 2019559783, @@ -5286,6 +5399,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5314,6 +5428,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5369,6 +5484,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5408,6 +5524,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5433,6 +5550,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5492,6 +5610,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5556,6 +5675,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5600,6 +5720,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5655,6 +5776,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5714,6 +5836,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -5798,6 +5921,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5853,6 +5977,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -5974,6 +6099,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6004,6 +6130,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6034,6 +6161,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -6089,6 +6217,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6128,6 +6257,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6153,6 +6283,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6192,6 +6323,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6217,6 +6349,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6242,6 +6375,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -6287,6 +6421,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6314,6 +6449,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6341,6 +6477,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -6467,6 +6604,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6495,6 +6633,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6550,6 +6689,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6589,6 +6729,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6614,6 +6755,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6656,6 +6798,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6681,6 +6824,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6800,6 +6944,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -6828,6 +6973,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -6856,6 +7002,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -6887,6 +7034,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -6930,6 +7078,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -6976,6 +7125,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7026,6 +7176,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7074,6 +7225,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7149,6 +7301,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7188,6 +7341,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7213,6 +7367,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -7252,6 +7407,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7277,6 +7433,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -7302,6 +7459,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -7341,6 +7499,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7366,6 +7525,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -7391,6 +7551,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -7419,6 +7580,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7470,6 +7632,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7495,6 +7658,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -7520,6 +7684,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -7548,6 +7713,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7588,6 +7754,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7639,6 +7806,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7664,6 +7832,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -7689,6 +7858,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -7717,6 +7887,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7757,6 +7928,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7800,6 +7972,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7851,6 +8024,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -7876,6 +8050,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -7901,6 +8076,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -7929,6 +8105,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -7969,6 +8146,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8012,6 +8190,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8067,6 +8246,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -8092,6 +8272,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -8117,6 +8298,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -8145,6 +8327,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8185,6 +8368,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8228,6 +8412,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8275,6 +8460,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8326,6 +8512,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -8351,6 +8538,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -8376,6 +8564,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -8404,6 +8593,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8444,6 +8634,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8487,6 +8678,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8534,6 +8726,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8589,6 +8782,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -8614,6 +8808,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -8639,6 +8834,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -8667,6 +8863,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8707,6 +8904,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8750,6 +8948,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8797,6 +8996,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8842,6 +9042,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -8986,6 +9187,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9014,6 +9216,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -9042,6 +9245,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -9097,6 +9301,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9136,6 +9341,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9161,6 +9367,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -9200,6 +9407,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9225,6 +9433,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -9250,6 +9459,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -9375,6 +9585,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9403,6 +9614,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -9456,6 +9668,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9495,6 +9708,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9520,6 +9734,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -9643,6 +9858,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9671,6 +9887,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -9727,6 +9944,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9752,6 +9970,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -9876,6 +10095,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9904,6 +10124,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -9960,6 +10181,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -9985,6 +10207,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -10026,6 +10249,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10051,6 +10275,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1278240551, @@ -10170,6 +10395,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10225,6 +10451,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10344,6 +10571,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10399,6 +10627,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10518,6 +10747,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10573,6 +10803,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -10695,6 +10926,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -10765,6 +10997,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -10899,6 +11132,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -10969,6 +11203,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11104,6 +11339,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11183,6 +11419,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11325,6 +11562,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11395,6 +11633,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11526,6 +11765,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -11581,6 +11821,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -11703,6 +11944,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11773,6 +12015,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -11904,6 +12147,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -11959,6 +12203,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12078,6 +12323,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12133,6 +12379,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12256,6 +12503,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -12335,6 +12583,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -12487,6 +12736,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 915032327, @@ -12517,6 +12767,7 @@ Object { "id": "id1_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 747212839, @@ -12547,6 +12798,7 @@ Object { "id": "id2_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 760410951, @@ -12577,6 +12829,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12607,6 +12860,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -12637,6 +12891,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -12692,6 +12947,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12731,6 +12987,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12756,6 +13013,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -12795,6 +13053,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12820,6 +13079,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -12845,6 +13105,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -12891,6 +13152,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -12918,6 +13180,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -12945,6 +13208,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -12992,6 +13256,7 @@ Object { "id": "id0_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 915032327, @@ -13019,6 +13284,7 @@ Object { "id": "id1_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 747212839, @@ -13046,6 +13312,7 @@ Object { "id": "id2_copy", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 760410951, @@ -13073,6 +13340,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13100,6 +13368,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -13127,6 +13396,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -13250,6 +13520,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13278,6 +13549,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -13333,6 +13605,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13372,6 +13645,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13397,6 +13671,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -13731,6 +14006,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13786,6 +14062,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13914,6 +14191,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -13942,6 +14220,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -13997,6 +14276,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14036,6 +14316,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14061,6 +14342,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14104,6 +14386,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14129,6 +14412,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14248,6 +14532,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14303,6 +14588,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14342,6 +14628,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14475,6 +14762,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14506,6 +14794,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14537,6 +14826,7 @@ Object { "id": "id5", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1014066025, @@ -14568,6 +14858,7 @@ Object { "id": "id6", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -14623,6 +14914,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14662,6 +14954,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14687,6 +14980,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14733,6 +15027,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14760,6 +15055,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14801,6 +15097,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14828,6 +15125,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14853,6 +15151,7 @@ Object { "id": "id5", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1014066025, @@ -14894,6 +15193,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -14921,6 +15221,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -14946,6 +15247,7 @@ Object { "id": "id5", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1014066025, @@ -14971,6 +15273,7 @@ Object { "id": "id6", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -15017,6 +15320,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15044,6 +15348,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15071,6 +15376,7 @@ Object { "id": "id5", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1014066025, @@ -15098,6 +15404,7 @@ Object { "id": "id6", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -15145,6 +15452,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15173,6 +15481,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15201,6 +15510,7 @@ Object { "id": "id5", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1014066025, @@ -15229,6 +15539,7 @@ Object { "id": "id6", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 400692809, @@ -15457,6 +15768,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15488,6 +15800,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15519,6 +15832,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -15574,6 +15888,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15613,6 +15928,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15638,6 +15954,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15677,6 +15994,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15702,6 +16020,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15727,6 +16046,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -15772,6 +16092,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15799,6 +16120,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15826,6 +16148,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -15869,6 +16192,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -15896,6 +16220,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15923,6 +16248,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -15968,6 +16294,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -15996,6 +16323,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16024,6 +16352,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -16070,6 +16399,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -16098,6 +16428,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16126,6 +16457,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -16185,6 +16517,7 @@ Object { "id": "id4", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1116226695, @@ -16253,6 +16586,7 @@ Object { "id": "id4", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 1116226695, @@ -16297,6 +16631,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16325,6 +16660,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -16353,6 +16689,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -16408,6 +16745,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16447,6 +16785,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16472,6 +16811,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -16511,6 +16851,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16536,6 +16877,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -16561,6 +16903,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -16620,6 +16963,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -16686,6 +17030,7 @@ Object { "id": "id2", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 401146281, @@ -16730,6 +17075,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16758,6 +17104,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -16813,6 +17160,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16852,6 +17200,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -16877,6 +17226,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -17103,6 +17453,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -17131,6 +17482,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -17165,6 +17517,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -17220,6 +17573,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -17245,6 +17599,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -17276,6 +17631,7 @@ Object { 20, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -17331,6 +17687,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -17356,6 +17713,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, @@ -17387,6 +17745,7 @@ Object { 10, ], "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -17451,6 +17810,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -17490,6 +17850,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -17515,6 +17876,7 @@ Object { "id": "id1", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 449462985, diff --git a/src/tests/__snapshots__/selection.test.tsx.snap b/src/tests/__snapshots__/selection.test.tsx.snap index e89f709da..d3f6ae527 100644 --- a/src/tests/__snapshots__/selection.test.tsx.snap +++ b/src/tests/__snapshots__/selection.test.tsx.snap @@ -14,6 +14,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -57,6 +58,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -97,6 +99,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -125,6 +128,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, @@ -153,6 +157,7 @@ Object { "id": "id0", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "roughness": 1, "seed": 337897, diff --git a/src/tests/contextmenu.test.tsx b/src/tests/contextmenu.test.tsx index 4dfcbb3c7..8f5f3263f 100644 --- a/src/tests/contextmenu.test.tsx +++ b/src/tests/contextmenu.test.tsx @@ -36,10 +36,6 @@ const checkpoint = (name: string) => { const mouse = new Pointer("mouse"); -const queryContextMenu = () => { - return GlobalTestState.renderResult.container.querySelector(".context-menu"); -}; - // Unmount ReactDOM from root ReactDOM.unmountComponentAtNode(document.getElementById("root")!); @@ -83,7 +79,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const contextMenuOptions = contextMenu?.querySelectorAll(".context-menu li"); const expectedShortcutNames: ShortcutName[] = [ @@ -113,7 +109,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const contextMenuOptions = contextMenu?.querySelectorAll(".context-menu li"); const expectedShortcutNames: ShortcutName[] = [ @@ -129,6 +125,7 @@ describe("contextMenu element", () => { "bringToFront", "duplicateSelection", "hyperlink", + "toggleLock", ]; expect(contextMenu).not.toBeNull(); @@ -166,7 +163,7 @@ describe("contextMenu element", () => { clientX: 100, clientY: 100, }); - expect(queryContextMenu()).not.toBeNull(); + expect(UI.queryContextMenu()).not.toBeNull(); expect(API.getSelectedElement().id).toBe(rect1.id); // higher z-index @@ -176,7 +173,7 @@ describe("contextMenu element", () => { clientX: 100, clientY: 100, }); - expect(queryContextMenu()).not.toBeNull(); + expect(UI.queryContextMenu()).not.toBeNull(); expect(API.getSelectedElement().id).toBe(rect2.id); }); @@ -201,7 +198,7 @@ describe("contextMenu element", () => { clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const contextMenuOptions = contextMenu?.querySelectorAll(".context-menu li"); const expectedShortcutNames: ShortcutName[] = [ @@ -215,6 +212,7 @@ describe("contextMenu element", () => { "sendToBack", "bringToFront", "duplicateSelection", + "toggleLock", ]; expect(contextMenu).not.toBeNull(); @@ -251,7 +249,7 @@ describe("contextMenu element", () => { clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const contextMenuOptions = contextMenu?.querySelectorAll(".context-menu li"); const expectedShortcutNames: ShortcutName[] = [ @@ -265,6 +263,7 @@ describe("contextMenu element", () => { "sendToBack", "bringToFront", "duplicateSelection", + "toggleLock", ]; expect(contextMenu).not.toBeNull(); @@ -286,7 +285,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); expect(copiedStyles).toBe("{}"); fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!); expect(copiedStyles).not.toBe("{}"); @@ -328,7 +327,7 @@ describe("contextMenu element", () => { clientX: 40, clientY: 40, }); - let contextMenu = queryContextMenu(); + let contextMenu = UI.queryContextMenu(); fireEvent.click(queryByText(contextMenu as HTMLElement, "Copy styles")!); const secondRect = JSON.parse(copiedStyles); expect(secondRect.id).toBe(h.elements[1].id); @@ -340,7 +339,7 @@ describe("contextMenu element", () => { clientX: 10, clientY: 10, }); - contextMenu = queryContextMenu(); + contextMenu = UI.queryContextMenu(); fireEvent.click(queryByText(contextMenu as HTMLElement, "Paste styles")!); const firstRect = API.getSelectedElement(); @@ -364,7 +363,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); fireEvent.click(queryAllByText(contextMenu as HTMLElement, "Delete")[0]); expect(API.getSelectedElements()).toHaveLength(0); expect(h.elements[0].isDeleted).toBe(true); @@ -380,7 +379,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); fireEvent.click(queryByText(contextMenu as HTMLElement, "Add to library")!); await waitFor(() => { @@ -401,7 +400,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); fireEvent.click(queryByText(contextMenu as HTMLElement, "Duplicate")!); expect(h.elements).toHaveLength(2); const { id: _id0, seed: _seed0, x: _x0, y: _y0, ...rect1 } = h.elements[0]; @@ -424,7 +423,7 @@ describe("contextMenu element", () => { clientX: 40, clientY: 40, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Send backward")!); expect(elementsBefore[0].id).toEqual(h.elements[1].id); @@ -446,7 +445,7 @@ describe("contextMenu element", () => { clientX: 10, clientY: 10, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring forward")!); expect(elementsBefore[0].id).toEqual(h.elements[1].id); @@ -468,7 +467,7 @@ describe("contextMenu element", () => { clientX: 40, clientY: 40, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Send to back")!); expect(elementsBefore[1].id).toEqual(h.elements[0].id); @@ -489,7 +488,7 @@ describe("contextMenu element", () => { clientX: 10, clientY: 10, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); const elementsBefore = h.elements; fireEvent.click(queryByText(contextMenu as HTMLElement, "Bring to front")!); expect(elementsBefore[0].id).toEqual(h.elements[1].id); @@ -514,7 +513,7 @@ describe("contextMenu element", () => { clientX: 1, clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); fireEvent.click( queryByText(contextMenu as HTMLElement, "Group selection")!, ); @@ -547,7 +546,7 @@ describe("contextMenu element", () => { clientY: 1, }); - const contextMenu = queryContextMenu(); + const contextMenu = UI.queryContextMenu(); expect(contextMenu).not.toBeNull(); fireEvent.click( queryByText(contextMenu as HTMLElement, "Ungroup selection")!, diff --git a/src/tests/data/__snapshots__/restore.test.ts.snap b/src/tests/data/__snapshots__/restore.test.ts.snap index 870b12165..b06493154 100644 --- a/src/tests/data/__snapshots__/restore.test.ts.snap +++ b/src/tests/data/__snapshots__/restore.test.ts.snap @@ -14,6 +14,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -58,6 +59,7 @@ Object { "id": "1", "isDeleted": false, "link": null, + "locked": false, "opacity": 10, "roughness": 2, "seed": Any, @@ -90,6 +92,7 @@ Object { "id": "2", "isDeleted": false, "link": null, + "locked": false, "opacity": 10, "roughness": 2, "seed": Any, @@ -122,6 +125,7 @@ Object { "id": "3", "isDeleted": false, "link": null, + "locked": false, "opacity": 10, "roughness": 2, "seed": Any, @@ -151,6 +155,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [], "pressures": Array [], @@ -185,6 +190,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -228,6 +234,7 @@ Object { "isDeleted": false, "lastCommittedPoint": null, "link": null, + "locked": false, "opacity": 100, "points": Array [ Array [ @@ -272,6 +279,7 @@ Object { "id": "id-text01", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "originalText": "text", "roughness": 1, @@ -308,6 +316,7 @@ Object { "id": "id-text01", "isDeleted": false, "link": null, + "locked": false, "opacity": 100, "originalText": "test", "roughness": 1, diff --git a/src/tests/elementLocking.test.tsx b/src/tests/elementLocking.test.tsx new file mode 100644 index 000000000..f23e2d214 --- /dev/null +++ b/src/tests/elementLocking.test.tsx @@ -0,0 +1,388 @@ +import ReactDOM from "react-dom"; +import ExcalidrawApp from "../excalidraw-app"; +import { render } from "../tests/test-utils"; +import { Keyboard, Pointer, UI } from "../tests/helpers/ui"; +import { KEYS } from "../keys"; +import { API } from "../tests/helpers/api"; +import { actionSelectAll } from "../actions"; +import { t } from "../i18n"; +import { mutateElement } from "../element/mutateElement"; + +ReactDOM.unmountComponentAtNode(document.getElementById("root")!); + +const mouse = new Pointer("mouse"); +const h = window.h; + +describe("element locking", () => { + beforeEach(async () => { + await render(); + h.elements = []; + }); + + it("click-selecting a locked element is disabled", () => { + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + h.elements = [lockedRectangle]; + + mouse.clickAt(50, 50); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("box-selecting a locked element is disabled", () => { + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + x: 100, + y: 100, + }); + + h.elements = [lockedRectangle]; + + mouse.downAt(50, 50); + mouse.moveTo(250, 250); + mouse.upAt(250, 250); + expect(API.getSelectedElements().length).toBe(0); + }); + + it("dragging a locked element is disabled", () => { + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + h.elements = [lockedRectangle]; + + mouse.downAt(50, 50); + mouse.moveTo(100, 100); + mouse.upAt(100, 100); + expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 })); + }); + + it("you can drag element that's below a locked element", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + h.elements = [rectangle, lockedRectangle]; + + mouse.downAt(50, 50); + mouse.moveTo(100, 100); + mouse.upAt(100, 100); + expect(lockedRectangle).toEqual(expect.objectContaining({ x: 0, y: 0 })); + expect(rectangle).toEqual(expect.objectContaining({ x: 50, y: 50 })); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + }); + + it("selectAll shouldn't select locked elements", () => { + h.elements = [ + API.createElement({ type: "rectangle" }), + API.createElement({ type: "rectangle", locked: true }), + ]; + h.app.actionManager.executeAction(actionSelectAll); + expect(API.getSelectedElements().length).toBe(1); + }); + + it("clicking on a locked element should select the unlocked element beneath it", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + h.elements = [rectangle, lockedRectangle]; + expect(API.getSelectedElements().length).toBe(0); + mouse.clickAt(50, 50); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + }); + + it("right-clicking on a locked element should select it & open its contextMenu", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + h.elements = [rectangle, lockedRectangle]; + expect(API.getSelectedElements().length).toBe(0); + mouse.rightClickAt(50, 50); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(lockedRectangle.id); + + const contextMenu = UI.queryContextMenu(); + expect(contextMenu).not.toBeNull(); + expect( + contextMenu?.querySelector( + `li[data-testid="toggleLock"] .context-menu-option__label`, + ), + ).toHaveTextContent(t("labels.elementLock.unlock")); + }); + + it("right-clicking on element covered by locked element should ignore the locked element", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + }); + + h.elements = [rectangle, lockedRectangle]; + API.setSelectedElements([rectangle]); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + mouse.rightClickAt(50, 50); + expect(API.getSelectedElements().length).toBe(1); + expect(API.getSelectedElement().id).toBe(rectangle.id); + + const contextMenu = UI.queryContextMenu(); + expect(contextMenu).not.toBeNull(); + }); + + it("selecting a group selects all elements including locked ones", () => { + const rectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + groupIds: ["g1"], + }); + const lockedRectangle = API.createElement({ + type: "rectangle", + width: 100, + backgroundColor: "red", + fillStyle: "solid", + locked: true, + groupIds: ["g1"], + x: 200, + y: 200, + }); + + h.elements = [rectangle, lockedRectangle]; + + mouse.clickAt(250, 250); + expect(API.getSelectedElements().length).toBe(0); + + mouse.clickAt(50, 50); + expect(API.getSelectedElements().length).toBe(2); + }); + + it("should ignore locked text element in center of container on ENTER", () => { + const container = API.createElement({ + type: "rectangle", + width: 100, + }); + const textSize = 20; + const text = API.createElement({ + type: "text", + text: "ola", + x: container.width / 2 - textSize / 2, + y: container.height / 2 - textSize / 2, + width: textSize, + height: textSize, + containerId: container.id, + locked: true, + }); + h.elements = [container, text]; + API.setSelectedElements([container]); + Keyboard.keyPress(KEYS.ENTER); + expect(h.state.editingElement?.id).not.toBe(text.id); + expect(h.state.editingElement?.id).toBe(h.elements[2].id); + }); + + it("should ignore locked text under cursor when clicked with text tool", () => { + const text = API.createElement({ + type: "text", + text: "ola", + x: 60, + y: 0, + width: 100, + height: 100, + locked: true, + }); + h.elements = [text]; + UI.clickTool("text"); + mouse.clickAt(text.x + 50, text.y + 50); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).not.toBe(null); + expect(h.state.editingElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(2); + expect(h.state.editingElement?.id).toBe(h.elements[1].id); + }); + + it("should ignore text under cursor when double-clicked with selection tool", () => { + const text = API.createElement({ + type: "text", + text: "ola", + x: 60, + y: 0, + width: 100, + height: 100, + locked: true, + }); + h.elements = [text]; + UI.clickTool("selection"); + mouse.doubleClickAt(text.x + 50, text.y + 50); + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).not.toBe(null); + expect(h.state.editingElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(2); + expect(h.state.editingElement?.id).toBe(h.elements[1].id); + }); + + it("locking should include bound text", () => { + const container = API.createElement({ + type: "rectangle", + width: 100, + }); + const textSize = 20; + const text = API.createElement({ + type: "text", + text: "ola", + x: container.width / 2 - textSize / 2, + y: container.height / 2 - textSize / 2, + width: textSize, + height: textSize, + containerId: container.id, + }); + mutateElement(container, { + boundElements: [{ id: text.id, type: "text" }], + }); + + h.elements = [container, text]; + + UI.clickTool("selection"); + mouse.clickAt(container.x + 10, container.y + 10); + Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => { + Keyboard.keyPress(KEYS.L); + }); + + expect(h.elements).toEqual([ + expect.objectContaining({ + id: container.id, + locked: true, + }), + expect.objectContaining({ + id: text.id, + locked: true, + }), + ]); + }); + + it("bound text shouldn't be editable via double-click", () => { + const container = API.createElement({ + type: "rectangle", + width: 100, + locked: true, + }); + const textSize = 20; + const text = API.createElement({ + type: "text", + text: "ola", + x: container.width / 2 - textSize / 2, + y: container.height / 2 - textSize / 2, + width: textSize, + height: textSize, + containerId: container.id, + locked: true, + }); + mutateElement(container, { + boundElements: [{ id: text.id, type: "text" }], + }); + h.elements = [container, text]; + + UI.clickTool("selection"); + mouse.doubleClickAt(container.width / 2, container.height / 2); + + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).not.toBe(null); + expect(h.state.editingElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(3); + expect(h.state.editingElement?.id).toBe(h.elements[2].id); + }); + + it("bound text shouldn't be editable via text tool", () => { + const container = API.createElement({ + type: "rectangle", + width: 100, + locked: true, + }); + const textSize = 20; + const text = API.createElement({ + type: "text", + text: "ola", + x: container.width / 2 - textSize / 2, + y: container.height / 2 - textSize / 2, + width: textSize, + height: textSize, + containerId: container.id, + locked: true, + }); + mutateElement(container, { + boundElements: [{ id: text.id, type: "text" }], + }); + h.elements = [container, text]; + + UI.clickTool("text"); + mouse.clickAt(container.width / 2, container.height / 2); + + const editor = document.querySelector( + ".excalidraw-textEditorContainer > textarea", + ) as HTMLTextAreaElement; + expect(editor).not.toBe(null); + expect(h.state.editingElement?.id).not.toBe(text.id); + expect(h.elements.length).toBe(3); + expect(h.state.editingElement?.id).toBe(h.elements[2].id); + }); +}); diff --git a/src/tests/fixtures/elementFixture.ts b/src/tests/fixtures/elementFixture.ts index eca71256d..ea5980ca1 100644 --- a/src/tests/fixtures/elementFixture.ts +++ b/src/tests/fixtures/elementFixture.ts @@ -23,6 +23,7 @@ const elementBase: Omit = { boundElements: null, updated: 1, link: null, + locked: false, }; export const rectangleFixture: ExcalidrawElement = { diff --git a/src/tests/helpers/api.ts b/src/tests/helpers/api.ts index 62f9e9283..8944bfdb0 100644 --- a/src/tests/helpers/api.ts +++ b/src/tests/helpers/api.ts @@ -15,6 +15,7 @@ import path from "path"; import { getMimeType } from "../../data/blob"; import { newFreeDrawElement } from "../../element/newElement"; import { Point } from "../../types"; +import { getSelectedElements } from "../../scene/selection"; const readFile = util.promisify(fs.readFile); @@ -30,10 +31,10 @@ export class API { }); }; - static getSelectedElements = (): ExcalidrawElement[] => { - return h.elements.filter( - (element) => h.state.selectedElementIds[element.id], - ); + static getSelectedElements = ( + includeBoundTextElement: boolean = false, + ): ExcalidrawElement[] => { + return getSelectedElements(h.elements, h.state, includeBoundTextElement); }; static getSelectedElement = (): ExcalidrawElement => { @@ -100,6 +101,7 @@ export class API { ? ExcalidrawTextElement["containerId"] : never; points?: T extends "arrow" | "line" ? readonly Point[] : never; + locked?: boolean; }): T extends "arrow" | "line" ? ExcalidrawLinearElement : T extends "freedraw" @@ -125,6 +127,7 @@ export class API { roughness: rest.roughness ?? appState.currentItemRoughness, opacity: rest.opacity ?? appState.currentItemOpacity, boundElements: rest.boundElements ?? null, + locked: rest.locked ?? false, }; switch (type) { case "rectangle": diff --git a/src/tests/helpers/ui.ts b/src/tests/helpers/ui.ts index 243077803..a8b284d72 100644 --- a/src/tests/helpers/ui.ts +++ b/src/tests/helpers/ui.ts @@ -179,6 +179,14 @@ export class Pointer { this.upAt(); } + rightClickAt(x: number, y: number) { + fireEvent.contextMenu(GlobalTestState.canvas, { + button: 2, + clientX: x, + clientY: y, + }); + } + doubleClickAt(x: number, y: number) { this.moveTo(x, y); fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent()); @@ -309,4 +317,10 @@ export class UI { Keyboard.codePress(CODES.G); }); } + + static queryContextMenu = () => { + return GlobalTestState.renderResult.container.querySelector( + ".context-menu", + ); + }; } diff --git a/src/tests/scene/__snapshots__/export.test.ts.snap b/src/tests/scene/__snapshots__/export.test.ts.snap index c0290f757..61eb9d704 100644 --- a/src/tests/scene/__snapshots__/export.test.ts.snap +++ b/src/tests/scene/__snapshots__/export.test.ts.snap @@ -93,7 +93,7 @@ exports[`exportToSvg with elements that have a link 1`] = ` exports[`exportToSvg with exportEmbedScene 1`] = ` " - eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1STU9cdTAwMDMhXHUwMDEwvfdXbPDapLtrv+ytWmNMjFx1MDAxZXpoovFAl9mFlFx1MDAwMlx1MDAwNbZcdTAwMWZp+t9cdTAwMDXaLrrx5lVcdTAwMGUk83hvZph5x06SIHtQgCZcdIJ9gTkjXHUwMDFh71DX41vQhknhnvJcdTAwMTBcdTAwMWJZ61wiMKm1atLrcelcdTAwMDRUXHUwMDFhe+ZcdTAwMDOHNVxia1x1MDAxY+PDxUlyXGa3e2HEq7ZcdTAwMGK9eZuWKyZIvinWo5fZ9Ok9SFx1MDAwM2nvOP2s38RcdTAwMDdf+HbUxDtGLHVYlqZcckaBVdS2QCwq7tuMiLFaruBBcql9IzdpOLH0XHUwMDEyXHUwMDE3q0rLWpDIyVx1MDAwNlx1MDAxOC/LyClcdTAwMTnnc3vg51x1MDAwMeCC1lx1MDAxYVCrwuLaYlx1MDAwYm90RrpcdTAwMDFHlStZUVx1MDAwMcb80EiFXHUwMDBiZlx1MDAwZq1f+f7UM1x00/1s56dYq0tcdTAwMWVkfPCtM1x1MDAwMFx1MDAxMlL1s+FgdJeOm5e43yxP2+irXHUwMDE0YddZNlx1MDAxZadpP1x1MDAxZlxyXHUwMDFiXHUwMDA2MzO3alx1MDAxYtKWmFx1MDAxYohz9CN8jDZcdTAwMTA1581jrVxiPoviVzlcdTAwMTOrNu9qR8LwWlxuglx1MDAwMn7q/jvq31F/dFx1MDAxNHDOlIGLo9xcdTAwMWR+jbBSc+tcdTAwMTI5ytlfaMtgd//LXHUwMDA2y3C8PvjRb1x1MDAxMHxXx1Pn9Fx1MDAwNbeWWs0ifQ== + eyJ2ZXJzaW9uIjoiMSIsImVuY29kaW5nIjoiYnN0cmluZyIsImNvbXByZXNzZWQiOnRydWUsImVuY29kZWQiOiJ4nO1SPW/CMFx1MDAxMN35XHUwMDE1kbtcIpGk4aNstFRVpapcdTAwMWRcdTAwMTiQWnUw8YVYMbaxXHUwMDFkPoT477VccsRtxNpcclx1MDAwZpbu+b278907dKJcYpm9XHUwMDA0NI5cdTAwMTDscswoUXiLulx1MDAwZd+A0lRw+5T6WIta5Z5ZXHUwMDFhI8e9XHUwMDFlXHUwMDEzVlBcbm1OfGCwXHUwMDAybrRlfNk4ilx1MDAwZf62L5Q41Wau1lx1MDAxZpOiopyk63w1fJtOXj691JN2lpMlWVx1MDAxM+9d4fthXHUwMDEzbykxpcWSOG6wXHUwMDEy6LI0LVx1MDAxMPMlc21cdTAwMDZEXHUwMDFiJSp4XHUwMDEyTCjXyF3sTyi9wHm1VKLmJHCSPsaLXCJwXG7K2Mzs2WlcdTAwMDA4L2tcdTAwMDWoVWF+abGFNzot7ICDypZcXJZcdTAwMWO0/qNcdTAwMTFcdTAwMTLn1Oxbv3L9yVfip/vdzl9iJc95kHbBr85cdTAwMDCIT5Ulg/7wIVx1MDAxZTUvYb9JXHUwMDFht9F3wf2uk2Q0iuMsXHUwMDFkXHUwMDBlXHUwMDFhXHUwMDA21VO7auPTXHUwMDE2mGlcYnN0I3xcdTAwMGU24DVjzWMtXHQ+icJXXHUwMDE55VWbZ11VXcl9cSmheCU4QVx1MDAxZT92b0a7XHUwMDE57X+MXHUwMDA2jFGp4Ww0e/thICzlzNj8lnKyXHUwMDFk2lDYPl5ZbOGP03ubusWCa/Zw7Fx1MDAxY39cdTAwMDCLqmbvIn0=