diff --git a/src/actions/actionBoundText.tsx b/src/actions/actionBoundText.tsx index c8a1faac..06f2acda 100644 --- a/src/actions/actionBoundText.tsx +++ b/src/actions/actionBoundText.tsx @@ -31,6 +31,7 @@ import { } from "../element/types"; import { getSelectedElements } from "../scene"; import { AppState } from "../types"; +import { Mutable } from "../utility-types"; import { getFontString } from "../utils"; import { register } from "./register"; @@ -211,7 +212,7 @@ export const actionWrapTextInContainer = register({ appState, ); let updatedElements: readonly ExcalidrawElement[] = elements.slice(); - const containerIds: AppState["selectedElementIds"] = {}; + const containerIds: Mutable = {}; for (const textElement of selectedElements) { if (isTextElement(textElement)) { diff --git a/src/actions/actionDuplicateSelection.tsx b/src/actions/actionDuplicateSelection.tsx index ae2d6f7b..181e70e7 100644 --- a/src/actions/actionDuplicateSelection.tsx +++ b/src/actions/actionDuplicateSelection.tsx @@ -274,6 +274,7 @@ const duplicateElements = ( ), }, getNonDeletedElements(finalElements), + appState, ), }; }; diff --git a/src/actions/actionFinalize.tsx b/src/actions/actionFinalize.tsx index 3508de0a..99c8d8cf 100644 --- a/src/actions/actionFinalize.tsx +++ b/src/actions/actionFinalize.tsx @@ -125,13 +125,6 @@ export const actionFinalize = register({ { x, y }, ); } - - if ( - !appState.activeTool.locked && - appState.activeTool.type !== "freedraw" - ) { - appState.selectedElementIds[multiPointElement.id] = true; - } } if ( diff --git a/src/actions/actionGroup.tsx b/src/actions/actionGroup.tsx index 5f22e3ae..ddd1a655 100644 --- a/src/actions/actionGroup.tsx +++ b/src/actions/actionGroup.tsx @@ -218,6 +218,7 @@ export const actionUngroup = register({ const updateAppState = selectGroupsForSelectedElements( { ...appState, selectedGroupIds: {} }, getNonDeletedElements(nextElements), + appState, ); frames.forEach((frame) => { @@ -232,9 +233,18 @@ export const actionUngroup = register({ }); // remove binded text elements from selection - boundTextElementIds.forEach( - (id) => (updateAppState.selectedElementIds[id] = false), + updateAppState.selectedElementIds = Object.entries( + updateAppState.selectedElementIds, + ).reduce( + (acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => { + if (selected && !boundTextElementIds.includes(id)) { + acc[id] = true; + } + return acc; + }, + {}, ); + return { appState: updateAppState, elements: nextElements, diff --git a/src/actions/actionSelectAll.ts b/src/actions/actionSelectAll.ts index 40e7f041..6ba78b93 100644 --- a/src/actions/actionSelectAll.ts +++ b/src/actions/actionSelectAll.ts @@ -41,6 +41,7 @@ export const actionSelectAll = register({ selectedElementIds, }, getNonDeletedElements(elements), + appState, ), commitToHistory: true, }; diff --git a/src/components/App.tsx b/src/components/App.tsx index 3ccfa124..887392e2 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -315,7 +315,10 @@ import { updateFrameMembershipOfSelectedElements, isElementInFrame, } from "../frame"; -import { excludeElementsInFramesFromSelection } from "../scene/selection"; +import { + excludeElementsInFramesFromSelection, + makeNextSelectedElementIds, +} from "../scene/selection"; import { actionPaste } from "../actions/actionClipboard"; import { actionRemoveAllElementsFromFrame, @@ -1353,6 +1356,7 @@ class App extends React.Component { this.scene.destroy(); this.library.destroy(); clearTimeout(touchTimeout); + isSomeElementSelected.clearCache(); touchTimeout = 0; } @@ -1825,7 +1829,7 @@ class App extends React.Component { if (event.touches.length === 2) { this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), }); } }; @@ -1835,7 +1839,10 @@ class App extends React.Component { if (event.touches.length > 0) { this.setState({ previousSelectedElementIds: {}, - selectedElementIds: this.state.previousSelectedElementIds, + selectedElementIds: makeNextSelectedElementIds( + this.state.previousSelectedElementIds, + this.state, + ), }); } else { gesture.pointers.clear(); @@ -1895,7 +1902,14 @@ class App extends React.Component { const imageElement = this.createImageElement({ sceneX, sceneY }); this.insertImageElement(imageElement, file); this.initializeImageDimensions(imageElement); - this.setState({ selectedElementIds: { [imageElement.id]: true } }); + this.setState({ + selectedElementIds: makeNextSelectedElementIds( + { + [imageElement.id]: true, + }, + this.state, + ), + }); return; } @@ -2032,6 +2046,7 @@ class App extends React.Component { selectedGroupIds: {}, }, this.scene.getNonDeletedElements(), + this.state, ), () => { if (opts.files) { @@ -2130,8 +2145,9 @@ class App extends React.Component { } this.setState({ - selectedElementIds: Object.fromEntries( - textElements.map((el) => [el.id, true]), + selectedElementIds: makeNextSelectedElementIds( + Object.fromEntries(textElements.map((el) => [el.id, true])), + this.state, ), }); @@ -2749,7 +2765,7 @@ class App extends React.Component { } else { setCursorForShape(this.canvas, this.state); this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, editingGroupId: null, }); @@ -2794,7 +2810,7 @@ class App extends React.Component { if (nextActiveTool.type !== "selection") { this.setState({ activeTool: nextActiveTool, - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, editingGroupId: null, }); @@ -2831,7 +2847,7 @@ class App extends React.Component { // elements by mistake while zooming if (this.isTouchScreenMultiTouchGesture()) { this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), }); } gesture.initialScale = this.state.zoom.value; @@ -2876,7 +2892,10 @@ class App extends React.Component { if (this.isTouchScreenMultiTouchGesture()) { this.setState({ previousSelectedElementIds: {}, - selectedElementIds: this.state.previousSelectedElementIds, + selectedElementIds: makeNextSelectedElementIds( + this.state.previousSelectedElementIds, + this.state, + ), }); } gesture.initialScale = null; @@ -2941,10 +2960,13 @@ class App extends React.Component { ? element.containerId : element.id; this.setState((prevState) => ({ - selectedElementIds: { - ...prevState.selectedElementIds, - [elementIdToSelect]: true, - }, + selectedElementIds: makeNextSelectedElementIds( + { + ...prevState.selectedElementIds, + [elementIdToSelect]: true, + }, + prevState, + ), })); } if (isDeleted) { @@ -2980,7 +3002,7 @@ class App extends React.Component { private deselectElements() { this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, editingGroupId: null, }); @@ -3291,6 +3313,7 @@ class App extends React.Component { selectedGroupIds: {}, }, this.scene.getNonDeletedElements(), + prevState, ), ); return; @@ -3998,12 +4021,15 @@ class App extends React.Component { editingElement: null, startBoundElement: null, suggestedBindings: [], - selectedElementIds: Object.keys(this.state.selectedElementIds) - .filter((key) => key !== element.id) - .reduce((obj: { [id: string]: boolean }, key) => { - obj[key] = this.state.selectedElementIds[key]; - return obj; - }, {}), + selectedElementIds: makeNextSelectedElementIds( + Object.keys(this.state.selectedElementIds) + .filter((key) => key !== element.id) + .reduce((obj: { [id: string]: true }, key) => { + obj[key] = this.state.selectedElementIds[key]; + return obj; + }, {}), + this.state, + ), }, }); return; @@ -4472,7 +4498,7 @@ class App extends React.Component { private clearSelectionIfNotUsingSelection = (): void => { if (this.state.activeTool.type !== "selection") { this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, editingGroupId: null, }); @@ -4604,9 +4630,12 @@ class App extends React.Component { if (this.state.editingLinearElement) { this.setState({ - selectedElementIds: { - [this.state.editingLinearElement.elementId]: true, - }, + selectedElementIds: makeNextSelectedElementIds( + { + [this.state.editingLinearElement.elementId]: true, + }, + this.state, + ), }); // If we click on something } else if (hitElement != null) { @@ -4634,7 +4663,7 @@ class App extends React.Component { !isElementInGroup(hitElement, this.state.editingGroupId) ) { this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, editingGroupId: null, }); @@ -4650,7 +4679,7 @@ class App extends React.Component { !pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements ) { this.setState((prevState) => { - const nextSelectedElementIds = { + const nextSelectedElementIds: { [id: string]: true } = { ...prevState.selectedElementIds, [hitElement.id]: true, }; @@ -4668,13 +4697,13 @@ class App extends React.Component { previouslySelectedElements, hitElement.id, ).forEach((element) => { - nextSelectedElementIds[element.id] = false; + delete nextSelectedElementIds[element.id]; }); } else if (hitElement.frameId) { // if hitElement is in a frame and its frame has been selected // disable selection for the given element if (nextSelectedElementIds[hitElement.frameId]) { - nextSelectedElementIds[hitElement.id] = false; + delete nextSelectedElementIds[hitElement.id]; } } else { // hitElement is neither a frame nor an element in a frame @@ -4704,7 +4733,7 @@ class App extends React.Component { framesInGroups.has(element.frameId) ) { // deselect element and groups containing the element - nextSelectedElementIds[element.id] = false; + delete nextSelectedElementIds[element.id]; element.groupIds .flatMap((gid) => getElementsInGroup( @@ -4712,10 +4741,9 @@ class App extends React.Component { gid, ), ) - .forEach( - (element) => - (nextSelectedElementIds[element.id] = false), - ); + .forEach((element) => { + delete nextSelectedElementIds[element.id]; + }); } }); } @@ -4728,6 +4756,7 @@ class App extends React.Component { showHyperlinkPopup: hitElement.link ? "info" : false, }, this.scene.getNonDeletedElements(), + prevState, ); }); pointerDownState.hit.wasAddedToSelection = true; @@ -4844,12 +4873,18 @@ class App extends React.Component { frameId: topLayerFrame ? topLayerFrame.id : null, }); - this.setState((prevState) => ({ - selectedElementIds: { + this.setState((prevState) => { + const nextSelectedElementIds = { ...prevState.selectedElementIds, - [element.id]: false, - }, - })); + }; + delete nextSelectedElementIds[element.id]; + return { + selectedElementIds: makeNextSelectedElementIds( + nextSelectedElementIds, + prevState, + ), + }; + }); const pressures = element.simulatePressure ? element.pressures @@ -4945,10 +4980,13 @@ class App extends React.Component { } this.setState((prevState) => ({ - selectedElementIds: { - ...prevState.selectedElementIds, - [multiElement.id]: true, - }, + selectedElementIds: makeNextSelectedElementIds( + { + ...prevState.selectedElementIds, + [multiElement.id]: true, + }, + prevState, + ), })); // clicking outside commit zone → update reference for last committed // point @@ -4999,12 +5037,18 @@ class App extends React.Component { locked: false, frameId: topLayerFrame ? topLayerFrame.id : null, }); - this.setState((prevState) => ({ - selectedElementIds: { + this.setState((prevState) => { + const nextSelectedElementIds = { ...prevState.selectedElementIds, - [element.id]: false, - }, - })); + }; + delete nextSelectedElementIds[element.id]; + return { + selectedElementIds: makeNextSelectedElementIds( + nextSelectedElementIds, + prevState, + ), + }; + }); mutateElement(element, { points: [...element.points, [0, 0]], }); @@ -5378,15 +5422,16 @@ class App extends React.Component { const oldIdToDuplicatedId = new Map(); const hitElement = pointerDownState.hit.element; const elements = this.scene.getElementsIncludingDeleted(); - const selectedElementIds: Array = + const selectedElementIds = new Set( getSelectedElements(elements, this.state, { includeBoundTextElement: true, includeElementsInFrames: true, - }).map((element) => element.id); + }).map((element) => element.id), + ); for (const element of elements) { if ( - selectedElementIds.includes(element.id) || + selectedElementIds.has(element.id) || // case: the state.selectedElementIds might not have been // updated yet by the time this mousemove event is fired (element.id === hitElement?.id && @@ -5524,14 +5569,9 @@ class App extends React.Component { }, }, this.scene.getNonDeletedElements(), + prevState, ), ); - } else { - this.setState({ - selectedElementIds: {}, - selectedGroupIds: {}, - editingGroupId: null, - }); } } // box-select line editor points @@ -5547,28 +5587,29 @@ class App extends React.Component { elements, draggingElement, ); - this.setState((prevState) => - selectGroupsForSelectedElements( + this.setState((prevState) => { + const nextSelectedElementIds = elementsWithinSelection.reduce( + (acc: Record, element) => { + acc[element.id] = true; + return acc; + }, + {}, + ); + + if (pointerDownState.hit.element) { + // if using ctrl/cmd, select the hitElement only if we + // haven't box-selected anything else + if (!elementsWithinSelection.length) { + nextSelectedElementIds[pointerDownState.hit.element.id] = true; + } else { + delete nextSelectedElementIds[pointerDownState.hit.element.id]; + } + } + + return selectGroupsForSelectedElements( { ...prevState, - selectedElementIds: { - ...prevState.selectedElementIds, - ...elementsWithinSelection.reduce( - (acc: Record, element) => { - acc[element.id] = true; - return acc; - }, - {}, - ), - ...(pointerDownState.hit.element - ? { - // if using ctrl/cmd, select the hitElement only if we - // haven't box-selected anything else - [pointerDownState.hit.element.id]: - !elementsWithinSelection.length, - } - : null), - }, + selectedElementIds: nextSelectedElementIds, showHyperlinkPopup: elementsWithinSelection.length === 1 && elementsWithinSelection[0].link @@ -5585,8 +5626,9 @@ class App extends React.Component { : null, }, this.scene.getNonDeletedElements(), - ), - ); + prevState, + ); + }); } } }); @@ -5780,7 +5822,12 @@ class App extends React.Component { try { this.initializeImageDimensions(imageElement); this.setState( - { selectedElementIds: { [imageElement.id]: true } }, + { + selectedElementIds: makeNextSelectedElementIds( + { [imageElement.id]: true }, + this.state, + ), + }, () => { this.actionManager.executeAction(actionFinalize); }, @@ -5844,10 +5891,13 @@ class App extends React.Component { activeTool: updateActiveTool(this.state, { type: "selection", }), - selectedElementIds: { - ...prevState.selectedElementIds, - [draggingElement.id]: true, - }, + selectedElementIds: makeNextSelectedElementIds( + { + ...prevState.selectedElementIds, + [draggingElement.id]: true, + }, + prevState, + ), selectedLinearElement: new LinearElementEditor( draggingElement, this.scene, @@ -6141,31 +6191,37 @@ class App extends React.Component { if (childEvent.shiftKey && !this.state.editingLinearElement) { if (this.state.selectedElementIds[hitElement.id]) { if (isSelectedViaGroup(this.state, hitElement)) { - // We want to unselect all groups hitElement is part of - // as well as all elements that are part of the groups - // hitElement is part of - const idsOfSelectedElementsThatAreInGroups = hitElement.groupIds - .flatMap((groupId) => - getElementsInGroup( - this.scene.getNonDeletedElements(), - groupId, - ), - ) - .map((element) => ({ [element.id]: false })) - .reduce((prevId, acc) => ({ ...prevId, ...acc }), {}); + this.setState((_prevState) => { + const nextSelectedElementIds = { + ..._prevState.selectedElementIds, + }; - this.setState((_prevState) => ({ - selectedGroupIds: { - ..._prevState.selectedElementIds, - ...hitElement.groupIds - .map((gId) => ({ [gId]: false })) - .reduce((prev, acc) => ({ ...prev, ...acc }), {}), - }, - selectedElementIds: { - ..._prevState.selectedElementIds, - ...idsOfSelectedElementsThatAreInGroups, - }, - })); + // We want to unselect all groups hitElement is part of + // as well as all elements that are part of the groups + // hitElement is part of + for (const groupedElement of hitElement.groupIds.flatMap( + (groupId) => + getElementsInGroup( + this.scene.getNonDeletedElements(), + groupId, + ), + )) { + delete nextSelectedElementIds[groupedElement.id]; + } + + return { + selectedGroupIds: { + ..._prevState.selectedElementIds, + ...hitElement.groupIds + .map((gId) => ({ [gId]: false })) + .reduce((prev, acc) => ({ ...prev, ...acc }), {}), + }, + selectedElementIds: makeNextSelectedElementIds( + nextSelectedElementIds, + _prevState, + ), + }; + }); // if not gragging a linear element point (outside editor) } else if (!this.state.selectedLinearElement?.isDragging) { // remove element from selection while @@ -6174,8 +6230,8 @@ class App extends React.Component { this.setState((prevState) => { const newSelectedElementIds = { ...prevState.selectedElementIds, - [hitElement!.id]: false, }; + delete newSelectedElementIds[hitElement!.id]; const newSelectedElements = getSelectedElements( this.scene.getNonDeletedElements(), { ...prevState, selectedElementIds: newSelectedElementIds }, @@ -6196,6 +6252,7 @@ class App extends React.Component { : prevState.selectedLinearElement, }, this.scene.getNonDeletedElements(), + prevState, ); }); } @@ -6206,21 +6263,23 @@ class App extends React.Component { // when hitElement is part of a selected frame, deselect the frame // to avoid frame and containing elements selected simultaneously this.setState((prevState) => { - const nextSelectedElementIds = { + const nextSelectedElementIds: { + [id: string]: true; + } = { ...prevState.selectedElementIds, [hitElement.id]: true, - // deselect the frame - [hitElement.frameId!]: false, }; + // deselect the frame + delete nextSelectedElementIds[hitElement.frameId!]; // deselect groups containing the frame (this.scene.getElement(hitElement.frameId!)?.groupIds ?? []) .flatMap((gid) => getElementsInGroup(this.scene.getNonDeletedElements(), gid), ) - .forEach( - (element) => (nextSelectedElementIds[element.id] = false), - ); + .forEach((element) => { + delete nextSelectedElementIds[element.id]; + }); return selectGroupsForSelectedElements( { @@ -6229,15 +6288,19 @@ class App extends React.Component { showHyperlinkPopup: hitElement.link ? "info" : false, }, this.scene.getNonDeletedElements(), + prevState, ); }); } else { // add element to selection while keeping prev elements selected this.setState((_prevState) => ({ - selectedElementIds: { - ..._prevState.selectedElementIds, - [hitElement!.id]: true, - }, + selectedElementIds: makeNextSelectedElementIds( + { + ..._prevState.selectedElementIds, + [hitElement!.id]: true, + }, + _prevState, + ), })); } } else { @@ -6255,6 +6318,7 @@ class App extends React.Component { : prevState.selectedLinearElement, }, this.scene.getNonDeletedElements(), + prevState, ), })); } @@ -6279,7 +6343,7 @@ class App extends React.Component { } else { // Deselect selected elements this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), selectedGroupIds: {}, editingGroupId: null, }); @@ -6290,13 +6354,17 @@ class App extends React.Component { if ( !activeTool.locked && activeTool.type !== "freedraw" && - draggingElement + draggingElement && + draggingElement.type !== "selection" ) { this.setState((prevState) => ({ - selectedElementIds: { - ...prevState.selectedElementIds, - [draggingElement.id]: true, - }, + selectedElementIds: makeNextSelectedElementIds( + { + ...prevState.selectedElementIds, + [draggingElement.id]: true, + }, + prevState, + ), })); } @@ -6610,7 +6678,10 @@ class App extends React.Component { this.initializeImageDimensions(imageElement); this.setState( { - selectedElementIds: { [imageElement.id]: true }, + selectedElementIds: makeNextSelectedElementIds( + { [imageElement.id]: true }, + this.state, + ), }, () => { this.actionManager.executeAction(actionFinalize); @@ -6837,7 +6908,7 @@ class App extends React.Component { private clearSelection(hitElement: ExcalidrawElement | null): void { this.setState((prevState) => ({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, prevState), selectedGroupIds: {}, // Continue editing the same group if the user selected a different // element from it @@ -6849,7 +6920,7 @@ class App extends React.Component { : null, })); this.setState({ - selectedElementIds: {}, + selectedElementIds: makeNextSelectedElementIds({}, this.state), previousSelectedElementIds: this.state.selectedElementIds, }); } @@ -6918,7 +6989,12 @@ class App extends React.Component { const imageElement = this.createImageElement({ sceneX, sceneY }); this.insertImageElement(imageElement, file); this.initializeImageDimensions(imageElement); - this.setState({ selectedElementIds: { [imageElement.id]: true } }); + this.setState({ + selectedElementIds: makeNextSelectedElementIds( + { [imageElement.id]: true }, + this.state, + ), + }); return; } @@ -7043,6 +7119,7 @@ class App extends React.Component { : null, }, this.scene.getNonDeletedElements(), + this.state, ) : this.state), showHyperlinkPopup: false, diff --git a/src/excalidraw-app/debug.ts b/src/excalidraw-app/debug.ts new file mode 100644 index 00000000..6e439f1c --- /dev/null +++ b/src/excalidraw-app/debug.ts @@ -0,0 +1,135 @@ +declare global { + interface Window { + debug: typeof Debug; + } +} + +const lessPrecise = (num: number, precision = 5) => + parseFloat(num.toPrecision(precision)); + +const getAvgFrameTime = (times: number[]) => + lessPrecise(times.reduce((a, b) => a + b) / times.length); + +const getFps = (frametime: number) => lessPrecise(1000 / frametime); + +export class Debug { + public static DEBUG_LOG_TIMES = true; + + private static TIMES_AGGR: Record = + {}; + private static TIMES_AVG: Record< + string, + { t: number; times: number[]; avg: number | null } + > = {}; + private static LAST_DEBUG_LOG_CALL = 0; + private static DEBUG_LOG_INTERVAL_ID: null | number = null; + + private static setupInterval = () => { + if (Debug.DEBUG_LOG_INTERVAL_ID === null) { + console.info("%c(starting perf recording)", "color: lime"); + Debug.DEBUG_LOG_INTERVAL_ID = window.setInterval(Debug.debugLogger, 1000); + } + Debug.LAST_DEBUG_LOG_CALL = Date.now(); + }; + + private static debugLogger = () => { + if ( + Date.now() - Debug.LAST_DEBUG_LOG_CALL > 600 && + Debug.DEBUG_LOG_INTERVAL_ID !== null + ) { + window.clearInterval(Debug.DEBUG_LOG_INTERVAL_ID); + Debug.DEBUG_LOG_INTERVAL_ID = null; + for (const [name, { avg }] of Object.entries(Debug.TIMES_AVG)) { + if (avg != null) { + console.info( + `%c${name} run avg: ${avg}ms (${getFps(avg)} fps)`, + "color: blue", + ); + } + } + console.info("%c(stopping perf recording)", "color: red"); + Debug.TIMES_AGGR = {}; + Debug.TIMES_AVG = {}; + return; + } + if (Debug.DEBUG_LOG_TIMES) { + for (const [name, { t, times }] of Object.entries(Debug.TIMES_AGGR)) { + if (times.length) { + console.info( + name, + lessPrecise(times.reduce((a, b) => a + b)), + times.sort((a, b) => a - b).map((x) => lessPrecise(x)), + ); + Debug.TIMES_AGGR[name] = { t, times: [] }; + } + } + for (const [name, { t, times, avg }] of Object.entries(Debug.TIMES_AVG)) { + if (times.length) { + const avgFrameTime = getAvgFrameTime(times); + console.info(name, `${avgFrameTime}ms (${getFps(avgFrameTime)} fps)`); + Debug.TIMES_AVG[name] = { + t, + times: [], + avg: + avg != null ? getAvgFrameTime([avg, avgFrameTime]) : avgFrameTime, + }; + } + } + } + }; + + public static logTime = (time?: number, name = "default") => { + Debug.setupInterval(); + const now = performance.now(); + const { t, times } = (Debug.TIMES_AGGR[name] = Debug.TIMES_AGGR[name] || { + t: 0, + times: [], + }); + if (t) { + times.push(time != null ? time : now - t); + } + Debug.TIMES_AGGR[name].t = now; + }; + public static logTimeAverage = (time?: number, name = "default") => { + Debug.setupInterval(); + const now = performance.now(); + const { t, times } = (Debug.TIMES_AVG[name] = Debug.TIMES_AVG[name] || { + t: 0, + times: [], + }); + if (t) { + times.push(time != null ? time : now - t); + } + Debug.TIMES_AVG[name].t = now; + }; + + private static logWrapper = + (type: "logTime" | "logTimeAverage") => + (fn: (...args: T) => R, name = "default") => { + return (...args: T) => { + const t0 = performance.now(); + const ret = fn(...args); + Debug.logTime(performance.now() - t0, name); + return ret; + }; + }; + + public static logTimeWrap = Debug.logWrapper("logTime"); + public static logTimeAverageWrap = Debug.logWrapper("logTimeAverage"); + + public static perfWrap = ( + fn: (...args: T) => R, + name = "default", + ) => { + return (...args: T) => { + // eslint-disable-next-line no-console + console.time(name); + const ret = fn(...args); + // eslint-disable-next-line no-console + console.timeEnd(name); + return ret; + }; + }; +} + +window.debug = Debug; diff --git a/src/groups.ts b/src/groups.ts index eda01373..a3bf134e 100644 --- a/src/groups.ts +++ b/src/groups.ts @@ -2,6 +2,7 @@ import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types"; import { AppState } from "./types"; import { getSelectedElements } from "./scene"; import { getBoundTextElement } from "./element/textElement"; +import { makeNextSelectedElementIds } from "./scene/selection"; export const selectGroup = ( groupId: GroupId, @@ -67,13 +68,21 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] => export const selectGroupsForSelectedElements = ( appState: AppState, elements: readonly NonDeleted[], + prevAppState: AppState, ): AppState => { let nextAppState: AppState = { ...appState, selectedGroupIds: {} }; const selectedElements = getSelectedElements(elements, appState); if (!selectedElements.length) { - return { ...nextAppState, editingGroupId: null }; + return { + ...nextAppState, + editingGroupId: null, + selectedElementIds: makeNextSelectedElementIds( + nextAppState.selectedElementIds, + prevAppState, + ), + }; } for (const selectedElement of selectedElements) { @@ -91,6 +100,11 @@ export const selectGroupsForSelectedElements = ( } } + nextAppState.selectedElementIds = makeNextSelectedElementIds( + nextAppState.selectedElementIds, + prevAppState, + ); + return nextAppState; }; diff --git a/src/scene/selection.test.ts b/src/scene/selection.test.ts new file mode 100644 index 00000000..644d2129 --- /dev/null +++ b/src/scene/selection.test.ts @@ -0,0 +1,35 @@ +import { makeNextSelectedElementIds } from "./selection"; + +describe("makeNextSelectedElementIds", () => { + const _makeNextSelectedElementIds = ( + selectedElementIds: { [id: string]: true }, + prevSelectedElementIds: { [id: string]: true }, + expectUpdated: boolean, + ) => { + const ret = makeNextSelectedElementIds(selectedElementIds, { + selectedElementIds: prevSelectedElementIds, + }); + expect(ret === selectedElementIds).toBe(expectUpdated); + }; + it("should return prevState selectedElementIds if no change", () => { + _makeNextSelectedElementIds({}, {}, false); + _makeNextSelectedElementIds({ 1: true }, { 1: true }, false); + _makeNextSelectedElementIds( + { 1: true, 2: true }, + { 1: true, 2: true }, + false, + ); + }); + it("should return new selectedElementIds if changed", () => { + // _makeNextSelectedElementIds({ 1: true }, { 1: false }, true); + _makeNextSelectedElementIds({ 1: true }, {}, true); + _makeNextSelectedElementIds({}, { 1: true }, true); + _makeNextSelectedElementIds({ 1: true }, { 2: true }, true); + _makeNextSelectedElementIds({ 1: true }, { 1: true, 2: true }, true); + _makeNextSelectedElementIds( + { 1: true, 2: true }, + { 1: true, 3: true }, + true, + ); + }); +}); diff --git a/src/scene/selection.ts b/src/scene/selection.ts index 5b8cb35b..bbb629d3 100644 --- a/src/scene/selection.ts +++ b/src/scene/selection.ts @@ -10,6 +10,7 @@ import { getContainingFrame, getFrameElements, } from "../frame"; +import { isShallowEqual } from "../utils"; /** * Frames and their containing elements are not to be selected at the same time. @@ -88,11 +89,41 @@ export const getElementsWithinSelection = ( return elementsInSelection; }; -export const isSomeElementSelected = ( - elements: readonly NonDeletedExcalidrawElement[], - appState: Pick, -): boolean => - elements.some((element) => appState.selectedElementIds[element.id]); +// FIXME move this into the editor instance to keep utility methods stateless +export const isSomeElementSelected = (function () { + let lastElements: readonly NonDeletedExcalidrawElement[] | null = null; + let lastSelectedElementIds: AppState["selectedElementIds"] | null = null; + let isSelected: boolean | null = null; + + const ret = ( + elements: readonly NonDeletedExcalidrawElement[], + appState: Pick, + ): boolean => { + if ( + isSelected != null && + elements === lastElements && + appState.selectedElementIds === lastSelectedElementIds + ) { + return isSelected; + } + + isSelected = elements.some( + (element) => appState.selectedElementIds[element.id], + ); + lastElements = elements; + lastSelectedElementIds = appState.selectedElementIds; + + return isSelected; + }; + + ret.clearCache = () => { + lastElements = null; + lastSelectedElementIds = null; + isSelected = null; + }; + + return ret; +})(); /** * Returns common attribute (picked by `getAttribute` callback) of selected @@ -161,3 +192,18 @@ export const getTargetElements = ( : getSelectedElements(elements, appState, { includeBoundTextElement: true, }); + +/** + * returns prevState's selectedElementids if no change from previous, so as to + * retain reference identity for memoization + */ +export const makeNextSelectedElementIds = ( + nextSelectedElementIds: AppState["selectedElementIds"], + prevState: Pick, +) => { + if (isShallowEqual(prevState.selectedElementIds, nextSelectedElementIds)) { + return prevState.selectedElementIds; + } + + return nextSelectedElementIds; +}; diff --git a/src/tests/__snapshots__/contextmenu.test.tsx.snap b/src/tests/__snapshots__/contextmenu.test.tsx.snap index e2ed78b3..4c67a4c7 100644 --- a/src/tests/__snapshots__/contextmenu.test.tsx.snap +++ b/src/tests/__snapshots__/contextmenu.test.tsx.snap @@ -2179,7 +2179,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object { @@ -2413,7 +2412,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "selectedGroupIds": Object { "id3": true, @@ -4171,7 +4169,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -4399,7 +4396,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "selectedGroupIds": Object { "id3": true, @@ -4479,7 +4475,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -4892,7 +4887,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -4901,8 +4895,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -5469,7 +5461,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -5478,8 +5469,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object { @@ -5713,8 +5702,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedGroupIds": Object { "id4": true, diff --git a/src/tests/__snapshots__/regressionTests.test.tsx.snap b/src/tests/__snapshots__/regressionTests.test.tsx.snap index 588dff76..2da3ccc2 100644 --- a/src/tests/__snapshots__/regressionTests.test.tsx.snap +++ b/src/tests/__snapshots__/regressionTests.test.tsx.snap @@ -65,9 +65,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, - "id4": true, - "id6": true, }, "resizingElement": null, "scrollX": 0, @@ -76,7 +73,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id7": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object { @@ -443,8 +439,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id3": true, - "id4": true, }, "selectedGroupIds": Object { "id5": true, @@ -618,29 +612,20 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, - "id5": true, }, "resizingElement": null, "scrollX": 0, "scrollY": 0, "scrolledOutside": false, "selectedElementIds": Object { - "id0": false, "id1": true, - "id2": false, - "id3": true, - "id5": true, - "id6": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object { "id0": true, "id1": true, "id2": true, - "id3": true, "id4": false, - "id5": true, }, "selectedLinearElement": null, "selectionElement": null, @@ -1003,7 +988,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id3": true, }, "selectedGroupIds": Object { "id4": true, @@ -1179,7 +1163,6 @@ Object { "scrollY": 0, "scrolledOutside": false, "selectedElementIds": Object { - "id12": true, "id7": true, }, "selectedElementsAreBeingDragged": false, @@ -1448,8 +1431,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedGroupIds": Object { "id4": true, @@ -1528,7 +1509,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id5": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -1712,8 +1692,6 @@ Object { "id0": true, "id1": true, "id7": true, - "id8": true, - "id9": true, }, "selectedGroupIds": Object { "id10": true, @@ -1825,7 +1803,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id11": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -1934,7 +1911,6 @@ Object { "editingLinearElement": null, "name": "Untitled-201933152653", "selectedElementIds": Object { - "id12": true, "id7": true, }, "selectedGroupIds": Object {}, @@ -2116,7 +2092,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -2239,7 +2214,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -2347,7 +2321,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -2356,8 +2329,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id3": true, - "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object { @@ -2724,8 +2695,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id3": true, - "id4": true, }, "selectedGroupIds": Object { "id5": true, @@ -2904,7 +2873,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -3059,7 +3027,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -3394,7 +3361,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id1": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -3754,7 +3720,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id1": true, - "id3": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -4247,7 +4212,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -4370,7 +4334,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -4478,7 +4441,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id1": true, }, "resizingElement": null, "scrollX": 0, @@ -4486,8 +4448,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id1": true, - "id2": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -4610,7 +4570,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -4654,8 +4613,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id1": true, - "id2": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -4770,7 +4727,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id2": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -5069,7 +5025,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -5522,7 +5477,6 @@ Object { "previousSelectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -5875,7 +5829,6 @@ Object { "previousSelectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -6423,9 +6376,7 @@ Object { "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "selectedElementIds": Object { - "id1": true, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, @@ -7157,7 +7108,6 @@ Object { "previousSelectedElementIds": Object { "id0": true, "id1": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -7166,8 +7116,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -7395,8 +7343,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -7536,9 +7482,7 @@ Object { "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "selectedElementIds": Object { - "id7": false, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, @@ -9539,9 +9483,7 @@ Object { "editingGroupId": null, "editingLinearElement": null, "name": "Untitled-201933152653", - "selectedElementIds": Object { - "id7": false, - }, + "selectedElementIds": Object {}, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", }, @@ -9948,7 +9890,6 @@ Object { "previousSelectedElementIds": Object { "id0": true, "id2": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -9956,7 +9897,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id1": true, - "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -10380,7 +10320,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -10389,8 +10328,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -10681,7 +10618,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -10689,7 +10625,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id1": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -10801,7 +10736,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id2": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -10938,7 +10872,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id2": true, }, "resizingElement": null, "scrollX": 0, @@ -10946,8 +10879,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id2": true, - "id3": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -11059,7 +10990,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id2": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -11132,8 +11062,6 @@ Object { "name": "Untitled-201933152653", "selectedElementIds": Object { "id0": true, - "id2": true, - "id3": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -12334,9 +12262,7 @@ Object { "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "selectedElementIds": Object { - "id0": false, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, @@ -12435,9 +12361,7 @@ Object { "editingGroupId": null, "editingLinearElement": null, "name": "Untitled-201933152653", - "selectedElementIds": Object { - "id0": false, - }, + "selectedElementIds": Object {}, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", }, @@ -13439,9 +13363,7 @@ Object { "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "selectedElementIds": Object { - "id0": false, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, @@ -13540,9 +13462,7 @@ Object { "editingGroupId": null, "editingLinearElement": null, "name": "Untitled-201933152653", - "selectedElementIds": Object { - "id0": false, - }, + "selectedElementIds": Object {}, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", }, @@ -13864,7 +13784,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -13874,8 +13793,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, - "id5": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object { @@ -14347,7 +14264,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, }, "selectedGroupIds": Object { "id4": true, @@ -14459,8 +14375,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, - "id5": true, }, "selectedGroupIds": Object { "id4": true, @@ -14727,7 +14641,6 @@ Object { "pendingImageElementId": null, "previousSelectedElementIds": Object { "id0": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -14735,7 +14648,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id1": true, - "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -15029,9 +14941,7 @@ Object { "scrollX": -2.916666666666668, "scrollY": 0, "scrolledOutside": false, - "selectedElementIds": Object { - "id0": true, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, @@ -15261,10 +15171,7 @@ Object { "scrollX": 0, "scrollY": 0, "scrolledOutside": false, - "selectedElementIds": Object { - "id0": false, - "id1": true, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, @@ -15451,8 +15358,6 @@ Object { "previousSelectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -15461,9 +15366,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, - "id4": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -15691,9 +15593,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, - "id4": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -15832,7 +15731,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -15842,7 +15740,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id5": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -16204,7 +16101,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id3": true, }, "selectedGroupIds": Object { "id4": true, @@ -16316,7 +16212,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id5": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -16487,7 +16382,6 @@ Object { "scrolledOutside": false, "selectedElementIds": Object { "id0": true, - "id1": true, }, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, @@ -16728,7 +16622,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id11": true, "id5": true, "id6": true, }, @@ -17036,8 +16929,6 @@ Object { "selectedElementIds": Object { "id0": true, "id1": true, - "id2": true, - "id3": true, }, "selectedGroupIds": Object { "id4": true, @@ -17356,8 +17247,6 @@ Object { "selectedElementIds": Object { "id5": true, "id6": true, - "id7": true, - "id8": true, }, "selectedGroupIds": Object { "id9": true, @@ -18309,7 +18198,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id4": true, }, "selectedGroupIds": Object {}, "viewBackgroundColor": "#ffffff", @@ -18418,7 +18306,6 @@ Object { "selectedElementIds": Object { "id0": true, "id2": true, - "id4": true, }, "selectedGroupIds": Object { "id5": true, @@ -18532,7 +18419,6 @@ Object { "id0": true, "id1": true, "id2": true, - "id7": true, }, "selectedGroupIds": Object { "id3": true, @@ -18737,7 +18623,6 @@ Object { "previousSelectedElementIds": Object { "id1": true, "id2": true, - "id3": true, }, "resizingElement": null, "scrollX": 0, @@ -19552,9 +19437,7 @@ Object { "scrollX": 10, "scrollY": -10, "scrolledOutside": false, - "selectedElementIds": Object { - "id0": true, - }, + "selectedElementIds": Object {}, "selectedElementsAreBeingDragged": false, "selectedGroupIds": Object {}, "selectedLinearElement": null, diff --git a/src/tests/flip.test.tsx b/src/tests/flip.test.tsx index 091d1c73..c90c0783 100644 --- a/src/tests/flip.test.tsx +++ b/src/tests/flip.test.tsx @@ -430,7 +430,10 @@ describe("arrow", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); h.app.scene.replaceAllElements([line]); - h.app.state.selectedElementIds[line.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [line.id]: true, + }; mutateElement(line, { angle: originalAngle, }); @@ -446,7 +449,10 @@ describe("arrow", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("arrow"); h.app.scene.replaceAllElements([line]); - h.app.state.selectedElementIds[line.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [line.id]: true, + }; mutateElement(line, { angle: originalAngle, }); @@ -616,7 +622,10 @@ describe("line", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("line"); h.app.scene.replaceAllElements([line]); - h.app.state.selectedElementIds[line.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [line.id]: true, + }; mutateElement(line, { angle: originalAngle, }); @@ -632,7 +641,10 @@ describe("line", () => { const expectedAngle = (7 * Math.PI) / 4; const line = createLinearElementWithCurveInsideMinMaxPoints("line"); h.app.scene.replaceAllElements([line]); - h.app.state.selectedElementIds[line.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [line.id]: true, + }; mutateElement(line, { angle: originalAngle, }); @@ -659,14 +671,20 @@ describe("freedraw", () => { it("flips an unrotated drawing horizontally correctly", async () => { const draw = createAndReturnOneDraw(); // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [draw.id]: true, + }; await checkHorizontalFlip(); }); it("flips an unrotated drawing vertically correctly", async () => { const draw = createAndReturnOneDraw(); // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [draw.id]: true, + }; await checkVerticalFlip(); }); @@ -676,7 +694,10 @@ describe("freedraw", () => { const draw = createAndReturnOneDraw(originalAngle); // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [draw.id]: true, + }; await checkRotatedHorizontalFlip(expectedAngle); }); @@ -687,7 +708,10 @@ describe("freedraw", () => { const draw = createAndReturnOneDraw(originalAngle); // select draw, since not done automatically - h.state.selectedElementIds[draw.id] = true; + h.state.selectedElementIds = { + ...h.state.selectedElementIds, + [draw.id]: true, + }; await checkRotatedVerticalFlip(expectedAngle); }); diff --git a/src/tests/zindex.test.tsx b/src/tests/zindex.test.tsx index a59af77c..de3292af 100644 --- a/src/tests/zindex.test.tsx +++ b/src/tests/zindex.test.tsx @@ -89,6 +89,7 @@ const populateElements = ( ...selectGroupsForSelectedElements( { ...h.state, ...appState, selectedElementIds }, h.elements, + h.state, ), ...appState, selectedElementIds, diff --git a/src/types.ts b/src/types.ts index fd00578b..bb641088 100644 --- a/src/types.ts +++ b/src/types.ts @@ -181,8 +181,8 @@ export type AppState = { defaultSidebarDockedPreference: boolean; lastPointerDownWith: PointerType; - selectedElementIds: { [id: string]: boolean }; - previousSelectedElementIds: { [id: string]: boolean }; + selectedElementIds: Readonly<{ [id: string]: true }>; + previousSelectedElementIds: { [id: string]: true }; selectedElementsAreBeingDragged: boolean; shouldCacheIgnoreZoom: boolean; toast: { message: string; closable?: boolean; duration?: number } | null;