feat: cache most of element selection (#6747)

This commit is contained in:
David Luzar 2023-07-17 01:09:44 +02:00 committed by GitHub
parent 2e46e27490
commit 9f76f8677b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 452 additions and 3755 deletions

View File

@ -1,6 +1,4 @@
import { register } from "./register"; import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { deepCopyElement } from "../element/newElement"; import { deepCopyElement } from "../element/newElement";
import { randomId } from "../random"; import { randomId } from "../random";
import { t } from "../i18n"; import { t } from "../i18n";
@ -9,14 +7,11 @@ export const actionAddToLibrary = register({
name: "addToLibrary", name: "addToLibrary",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
if (selectedElements.some((element) => element.type === "image")) { if (selectedElements.some((element) => element.type === "image")) {
return { return {
commitToHistory: false, commitToHistory: false,

View File

@ -13,19 +13,18 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n"; import { t } from "../i18n";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const alignActionsPredicate = ( const alignActionsPredicate = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
_: unknown,
app: AppClassProperties,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
return ( return (
selectedElements.length > 1 && selectedElements.length > 1 &&
// TODO enable aligning frames when implemented properly // TODO enable aligning frames when implemented properly
@ -36,12 +35,10 @@ const alignActionsPredicate = (
const alignSelectedElements = ( const alignSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
app: AppClassProperties,
alignment: Alignment, alignment: Alignment,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
const updatedElements = alignElements(selectedElements, alignment); const updatedElements = alignElements(selectedElements, alignment);
@ -50,6 +47,7 @@ const alignSelectedElements = (
return updateFrameMembershipOfSelectedElements( return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element), elements.map((element) => updatedElementsMap.get(element.id) || element),
appState, appState,
app,
); );
}; };
@ -57,10 +55,10 @@ export const actionAlignTop = register({
name: "alignTop", name: "alignTop",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "start", position: "start",
axis: "y", axis: "y",
}), }),
@ -69,9 +67,9 @@ export const actionAlignTop = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignTopIcon} icon={AlignTopIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -88,10 +86,10 @@ export const actionAlignBottom = register({
name: "alignBottom", name: "alignBottom",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "end", position: "end",
axis: "y", axis: "y",
}), }),
@ -100,9 +98,9 @@ export const actionAlignBottom = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignBottomIcon} icon={AlignBottomIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -119,10 +117,10 @@ export const actionAlignLeft = register({
name: "alignLeft", name: "alignLeft",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "start", position: "start",
axis: "x", axis: "x",
}), }),
@ -131,9 +129,9 @@ export const actionAlignLeft = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignLeftIcon} icon={AlignLeftIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -150,10 +148,10 @@ export const actionAlignRight = register({
name: "alignRight", name: "alignRight",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "end", position: "end",
axis: "x", axis: "x",
}), }),
@ -162,9 +160,9 @@ export const actionAlignRight = register({
}, },
keyTest: (event) => keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT, event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={AlignRightIcon} icon={AlignRightIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -181,19 +179,19 @@ export const actionAlignVerticallyCentered = register({
name: "alignVerticallyCentered", name: "alignVerticallyCentered",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "center", position: "center",
axis: "y", axis: "y",
}), }),
commitToHistory: true, commitToHistory: true,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={CenterVerticallyIcon} icon={CenterVerticallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -208,19 +206,19 @@ export const actionAlignHorizontallyCentered = register({
name: "alignHorizontallyCentered", name: "alignHorizontallyCentered",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: alignActionsPredicate, predicate: alignActionsPredicate,
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: alignSelectedElements(elements, appState, { elements: alignSelectedElements(elements, appState, app, {
position: "center", position: "center",
axis: "x", axis: "x",
}), }),
commitToHistory: true, commitToHistory: true,
}; };
}, },
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!alignActionsPredicate(elements, appState)} hidden={!alignActionsPredicate(elements, appState, null, app)}
type="button" type="button"
icon={CenterHorizontallyIcon} icon={CenterHorizontallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}

View File

@ -4,7 +4,7 @@ import {
VERTICAL_ALIGN, VERTICAL_ALIGN,
TEXT_ALIGN, TEXT_ALIGN,
} from "../constants"; } from "../constants";
import { getNonDeletedElements, isTextElement, newElement } from "../element"; import { isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement"; import { mutateElement } from "../element/mutateElement";
import { import {
computeBoundTextPosition, computeBoundTextPosition,
@ -29,7 +29,6 @@ import {
ExcalidrawTextContainer, ExcalidrawTextContainer,
ExcalidrawTextElement, ExcalidrawTextElement,
} from "../element/types"; } from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
import { getFontString } from "../utils"; import { getFontString } from "../utils";
@ -39,16 +38,13 @@ export const actionUnbindText = register({
name: "unbindText", name: "unbindText",
contextItemLabel: "labels.unbindText", contextItemLabel: "labels.unbindText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
return selectedElements.some((element) => hasBoundTextElement(element)); return selectedElements.some((element) => hasBoundTextElement(element));
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element); const boundTextElement = getBoundTextElement(element);
if (boundTextElement) { if (boundTextElement) {
@ -93,8 +89,8 @@ export const actionBindText = register({
name: "bindText", name: "bindText",
contextItemLabel: "labels.bindText", contextItemLabel: "labels.bindText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 2) { if (selectedElements.length === 2) {
const textElement = const textElement =
@ -117,11 +113,8 @@ export const actionBindText = register({
} }
return false; return false;
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
let textElement: ExcalidrawTextElement; let textElement: ExcalidrawTextElement;
let container: ExcalidrawTextContainer; let container: ExcalidrawTextContainer;
@ -201,16 +194,13 @@ export const actionWrapTextInContainer = register({
name: "wrapTextInContainer", name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText", contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
const areTextElements = selectedElements.every((el) => isTextElement(el)); const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements; return selectedElements.length > 0 && areTextElements;
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
let updatedElements: readonly ExcalidrawElement[] = elements.slice(); let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: Mutable<AppState["selectedElementIds"]> = {}; const containerIds: Mutable<AppState["selectedElementIds"]> = {};

View File

@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getNormalizedZoom, getSelectedElements } from "../scene"; import { getNormalizedZoom } from "../scene";
import { centerScrollOn } from "../scene/scroll"; import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom"; import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types"; import { AppState, NormalizedZoomValue } from "../types";
@ -302,11 +302,8 @@ export const zoomToFit = ({
export const actionZoomToFitSelectionInViewport = register({ export const actionZoomToFitSelectionInViewport = register({
name: "zoomToFitSelectionInViewport", name: "zoomToFitSelectionInViewport",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
return zoomToFit({ return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements, targetElements: selectedElements.length ? selectedElements : elements,
appState, appState,
@ -325,11 +322,8 @@ export const actionZoomToFitSelectionInViewport = register({
export const actionZoomToFitSelection = register({ export const actionZoomToFitSelection = register({
name: "zoomToFitSelection", name: "zoomToFitSelection",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
return zoomToFit({ return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements, targetElements: selectedElements.length ? selectedElements : elements,
appState, appState,

View File

@ -7,7 +7,6 @@ import {
probablySupportsClipboardWriteText, probablySupportsClipboardWriteText,
} from "../clipboard"; } from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected"; import { actionDeleteSelected } from "./actionDeleteSelected";
import { getSelectedElements } from "../scene/selection";
import { exportCanvas } from "../data/index"; import { exportCanvas } from "../data/index";
import { getNonDeletedElements, isTextElement } from "../element"; import { getNonDeletedElements, isTextElement } from "../element";
import { t } from "../i18n"; import { t } from "../i18n";
@ -16,7 +15,8 @@ export const actionCopy = register({
name: "copy", name: "copy",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const elementsToCopy = getSelectedElements(elements, appState, { const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}); });
@ -75,14 +75,11 @@ export const actionCopyAsSvg = register({
commitToHistory: false, commitToHistory: false,
}; };
} }
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
try { try {
await exportCanvas( await exportCanvas(
"clipboard-svg", "clipboard-svg",
@ -122,14 +119,11 @@ export const actionCopyAsPng = register({
commitToHistory: false, commitToHistory: false,
}; };
} }
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
try { try {
await exportCanvas( await exportCanvas(
"clipboard", "clipboard",
@ -177,14 +171,11 @@ export const actionCopyAsPng = register({
export const copyText = register({ export const copyText = register({
name: "copyText", name: "copyText",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, });
);
const text = selectedElements const text = selectedElements
.reduce((acc: string[], element) => { .reduce((acc: string[], element) => {
@ -199,12 +190,15 @@ export const copyText = register({
commitToHistory: false, commitToHistory: false,
}; };
}, },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
return ( return (
probablySupportsClipboardWriteText && probablySupportsClipboardWriteText &&
getSelectedElements(elements, appState, { app.scene
.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
}).some(isTextElement) })
.some(isTextElement)
); );
}, },
contextItemLabel: "labels.copyText", contextItemLabel: "labels.copyText",

View File

@ -9,19 +9,13 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame"; import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n"; import { t } from "../i18n";
import { CODES, KEYS } from "../keys"; import { CODES, KEYS } from "../keys";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils"; import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
const enableActionGroup = ( const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
elements: readonly ExcalidrawElement[], const selectedElements = app.scene.getSelectedElements(appState);
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return ( return (
selectedElements.length > 1 && selectedElements.length > 1 &&
// TODO enable distributing frames when implemented properly // TODO enable distributing frames when implemented properly
@ -32,12 +26,10 @@ const enableActionGroup = (
const distributeSelectedElements = ( const distributeSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
app: AppClassProperties,
distribution: Distribution, distribution: Distribution,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements(appState);
getNonDeletedElements(elements),
appState,
);
const updatedElements = distributeElements(selectedElements, distribution); const updatedElements = distributeElements(selectedElements, distribution);
@ -46,16 +38,17 @@ const distributeSelectedElements = (
return updateFrameMembershipOfSelectedElements( return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element), elements.map((element) => updatedElementsMap.get(element.id) || element),
appState, appState,
app,
); );
}; };
export const distributeHorizontally = register({ export const distributeHorizontally = register({
name: "distributeHorizontally", name: "distributeHorizontally",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: distributeSelectedElements(elements, appState, { elements: distributeSelectedElements(elements, appState, app, {
space: "between", space: "between",
axis: "x", axis: "x",
}), }),
@ -64,9 +57,9 @@ export const distributeHorizontally = register({
}, },
keyTest: (event) => keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H, !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(appState, app)}
type="button" type="button"
icon={DistributeHorizontallyIcon} icon={DistributeHorizontallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -82,10 +75,10 @@ export const distributeHorizontally = register({
export const distributeVertically = register({ export const distributeVertically = register({
name: "distributeVertically", name: "distributeVertically",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
appState, appState,
elements: distributeSelectedElements(elements, appState, { elements: distributeSelectedElements(elements, appState, app, {
space: "between", space: "between",
axis: "y", axis: "y",
}), }),
@ -94,9 +87,9 @@ export const distributeVertically = register({
}, },
keyTest: (event) => keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V, !event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(appState, app)}
type="button" type="button"
icon={DistributeVerticallyIcon} icon={DistributeVerticallyIcon}
onClick={() => updateData(null)} onClick={() => updateData(null)}

View File

@ -275,6 +275,7 @@ const duplicateElements = (
}, },
getNonDeletedElements(finalElements), getNonDeletedElements(finalElements),
appState, appState,
null,
), ),
}; };
}; };

View File

@ -1,7 +1,6 @@
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { arrayToMap } from "../utils"; import { arrayToMap } from "../utils";
import { register } from "./register"; import { register } from "./register";
@ -11,14 +10,15 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
export const actionToggleElementLock = register({ export const actionToggleElementLock = register({
name: "toggleElementLock", name: "toggleElementLock",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
return !selectedElements.some( return !selectedElements.some(
(element) => element.locked && element.frameId, (element) => element.locked && element.frameId,
); );
}, },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState, { const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}); });
@ -46,8 +46,9 @@ export const actionToggleElementLock = register({
commitToHistory: true, commitToHistory: true,
}; };
}, },
contextItemLabel: (elements, appState) => { contextItemLabel: (elements, appState, app) => {
const selected = getSelectedElements(elements, appState, { const selected = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false, includeBoundTextElement: false,
}); });
if (selected.length === 1 && selected[0].type !== "frame") { if (selected.length === 1 && selected[0].type !== "frame") {
@ -60,12 +61,13 @@ export const actionToggleElementLock = register({
? "labels.elementLock.lockAll" ? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll"; : "labels.elementLock.unlockAll";
}, },
keyTest: (event, appState, elements) => { keyTest: (event, appState, elements, app) => {
return ( return (
event.key.toLocaleLowerCase() === KEYS.L && event.key.toLocaleLowerCase() === KEYS.L &&
event[KEYS.CTRL_OR_CMD] && event[KEYS.CTRL_OR_CMD] &&
event.shiftKey && event.shiftKey &&
getSelectedElements(elements, appState, { app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: false, includeBoundTextElement: false,
}).length > 0 }).length > 0
); );

View File

@ -17,11 +17,12 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
export const actionFlipHorizontal = register({ export const actionFlipHorizontal = register({
name: "flipHorizontal", name: "flipHorizontal",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
elements: updateFrameMembershipOfSelectedElements( elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "horizontal"), flipSelectedElements(elements, appState, "horizontal"),
appState, appState,
app,
), ),
appState, appState,
commitToHistory: true, commitToHistory: true,
@ -34,11 +35,12 @@ export const actionFlipHorizontal = register({
export const actionFlipVertical = register({ export const actionFlipVertical = register({
name: "flipVertical", name: "flipVertical",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
return { return {
elements: updateFrameMembershipOfSelectedElements( elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "vertical"), flipSelectedElements(elements, appState, "vertical"),
appState, appState,
app,
), ),
appState, appState,
commitToHistory: true, commitToHistory: true,

View File

@ -3,19 +3,12 @@ import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame"; import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame"; import { getFrameElements } from "../frame";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getSelectedElements } from "../scene"; import { AppClassProperties, AppState } from "../types";
import { AppState } from "../types";
import { setCursorForShape, updateActiveTool } from "../utils"; import { setCursorForShape, updateActiveTool } from "../utils";
import { register } from "./register"; import { register } from "./register";
const isSingleFrameSelected = ( const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
elements: readonly ExcalidrawElement[], const selectedElements = app.scene.getSelectedElements(appState);
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return selectedElements.length === 1 && selectedElements[0].type === "frame"; return selectedElements.length === 1 && selectedElements[0].type === "frame";
}; };
@ -23,11 +16,8 @@ const isSingleFrameSelected = (
export const actionSelectAllElementsInFrame = register({ export const actionSelectAllElementsInFrame = register({
name: "selectAllElementsInFrame", name: "selectAllElementsInFrame",
trackEvent: { category: "canvas" }, trackEvent: { category: "canvas" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedFrame = getSelectedElements( const selectedFrame = app.scene.getSelectedElements(appState)[0];
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") { if (selectedFrame && selectedFrame.type === "frame") {
const elementsInFrame = getFrameElements( const elementsInFrame = getFrameElements(
@ -55,17 +45,15 @@ export const actionSelectAllElementsInFrame = register({
}; };
}, },
contextItemLabel: "labels.selectAllElementsInFrame", contextItemLabel: "labels.selectAllElementsInFrame",
predicate: (elements, appState) => isSingleFrameSelected(elements, appState), predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
}); });
export const actionRemoveAllElementsFromFrame = register({ export const actionRemoveAllElementsFromFrame = register({
name: "removeAllElementsFromFrame", name: "removeAllElementsFromFrame",
trackEvent: { category: "history" }, trackEvent: { category: "history" },
perform: (elements, appState) => { perform: (elements, appState, _, app) => {
const selectedFrame = getSelectedElements( const selectedFrame = app.scene.getSelectedElements(appState)[0];
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") { if (selectedFrame && selectedFrame.type === "frame") {
return { return {
@ -87,7 +75,8 @@ export const actionRemoveAllElementsFromFrame = register({
}; };
}, },
contextItemLabel: "labels.removeAllElementsFromFrame", contextItemLabel: "labels.removeAllElementsFromFrame",
predicate: (elements, appState) => isSingleFrameSelected(elements, appState), predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
}); });
export const actionupdateFrameRendering = register({ export const actionupdateFrameRendering = register({

View File

@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register"; import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons"; import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { isSomeElementSelected } from "../scene";
import { import {
getSelectedGroupIds, getSelectedGroupIds,
selectGroup, selectGroup,
@ -22,7 +22,7 @@ import {
ExcalidrawFrameElement, ExcalidrawFrameElement,
ExcalidrawTextElement, ExcalidrawTextElement,
} from "../element/types"; } from "../element/types";
import { AppState } from "../types"; import { AppClassProperties, AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks"; import { isBoundToContainer } from "../element/typeChecks";
import { import {
getElementsInResizingFrame, getElementsInResizingFrame,
@ -51,14 +51,12 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
const enableActionGroup = ( const enableActionGroup = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
app: AppClassProperties,
) => { ) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, });
);
return ( return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements) selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
); );
@ -68,13 +66,10 @@ export const actionGroup = register({
name: "group", name: "group",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const selectedElements = getSelectedElements( const selectedElements = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, });
);
if (selectedElements.length < 2) { if (selectedElements.length < 2) {
// nothing to group // nothing to group
return { appState, elements, commitToHistory: false }; return { appState, elements, commitToHistory: false };
@ -164,12 +159,13 @@ export const actionGroup = register({
}; };
}, },
contextItemLabel: "labels.group", contextItemLabel: "labels.group",
predicate: (elements, appState) => enableActionGroup(elements, appState), predicate: (elements, appState, _, app) =>
enableActionGroup(elements, appState, app),
keyTest: (event) => keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G, !event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
PanelComponent: ({ elements, appState, updateData }) => ( PanelComponent: ({ elements, appState, updateData, app }) => (
<ToolButton <ToolButton
hidden={!enableActionGroup(elements, appState)} hidden={!enableActionGroup(elements, appState, app)}
type="button" type="button"
icon={<GroupIcon theme={appState.theme} />} icon={<GroupIcon theme={appState.theme} />}
onClick={() => updateData(null)} onClick={() => updateData(null)}
@ -191,7 +187,7 @@ export const actionUngroup = register({
let nextElements = [...elements]; let nextElements = [...elements];
const selectedElements = getSelectedElements(nextElements, appState); const selectedElements = app.scene.getSelectedElements(appState);
const frames = selectedElements const frames = selectedElements
.filter((element) => element.frameId) .filter((element) => element.frameId)
.map((element) => .map((element) =>
@ -219,6 +215,7 @@ export const actionUngroup = register({
{ ...appState, selectedGroupIds: {} }, { ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements), getNonDeletedElements(nextElements),
appState, appState,
null,
); );
frames.forEach((frame) => { frames.forEach((frame) => {

View File

@ -1,8 +1,6 @@
import { getNonDeletedElements } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import { ExcalidrawLinearElement } from "../element/types"; import { ExcalidrawLinearElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { register } from "./register"; import { register } from "./register";
export const actionToggleLinearEditor = register({ export const actionToggleLinearEditor = register({
@ -10,21 +8,18 @@ export const actionToggleLinearEditor = register({
trackEvent: { trackEvent: {
category: "element", category: "element",
}, },
predicate: (elements, appState) => { predicate: (elements, appState, _, app) => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
return true; return true;
} }
return false; return false;
}, },
perform(elements, appState, _, app) { perform(elements, appState, _, app) {
const selectedElement = getSelectedElements( const selectedElement = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, })[0] as ExcalidrawLinearElement;
)[0] as ExcalidrawLinearElement;
const editingLinearElement = const editingLinearElement =
appState.editingLinearElement?.elementId === selectedElement.id appState.editingLinearElement?.elementId === selectedElement.id
@ -38,14 +33,11 @@ export const actionToggleLinearEditor = register({
commitToHistory: false, commitToHistory: false,
}; };
}, },
contextItemLabel: (elements, appState) => { contextItemLabel: (elements, appState, app) => {
const selectedElement = getSelectedElements( const selectedElement = app.scene.getSelectedElements({
getNonDeletedElements(elements), selectedElementIds: appState.selectedElementIds,
appState,
{
includeBoundTextElement: true, includeBoundTextElement: true,
}, })[0] as ExcalidrawLinearElement;
)[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement.id return appState.editingLinearElement?.elementId === selectedElement.id
? "labels.lineEditor.exit" ? "labels.lineEditor.exit"
: "labels.lineEditor.edit"; : "labels.lineEditor.edit";

View File

@ -42,6 +42,7 @@ export const actionSelectAll = register({
}, },
getNonDeletedElements(elements), getNonDeletedElements(elements),
appState, appState,
app,
), ),
commitToHistory: true, commitToHistory: true,
}; };

View File

@ -90,6 +90,7 @@ export class ActionManager {
event, event,
this.getAppState(), this.getAppState(),
this.getElementsIncludingDeleted(), this.getElementsIncludingDeleted(),
this.app,
), ),
); );
@ -168,6 +169,7 @@ export class ActionManager {
appState={this.getAppState()} appState={this.getAppState()}
updateData={updateData} updateData={updateData}
appProps={this.app.props} appProps={this.app.props}
app={this.app}
data={data} data={data}
/> />
); );

View File

@ -130,6 +130,7 @@ export type PanelComponentProps = {
updateData: (formData?: any) => void; updateData: (formData?: any) => void;
appProps: ExcalidrawProps; appProps: ExcalidrawProps;
data?: Record<string, any>; data?: Record<string, any>;
app: AppClassProperties;
}; };
export interface Action { export interface Action {
@ -141,12 +142,14 @@ export interface Action {
event: React.KeyboardEvent | KeyboardEvent, event: React.KeyboardEvent | KeyboardEvent,
appState: AppState, appState: AppState,
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
app: AppClassProperties,
) => boolean; ) => boolean;
contextItemLabel?: contextItemLabel?:
| string | string
| (( | ((
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>, appState: Readonly<AppState>,
app: AppClassProperties,
) => string); ) => string);
predicate?: ( predicate?: (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],

View File

@ -798,10 +798,7 @@ class App extends React.Component<AppProps, AppState> {
}; };
public render() { public render() {
const selectedElement = getSelectedElements( const selectedElement = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const { renderTopRightUI, renderCustomStats } = this.props; const { renderTopRightUI, renderCustomStats } = this.props;
return ( return (
@ -858,6 +855,7 @@ class App extends React.Component<AppProps, AppState> {
!this.state.zenModeEnabled && !this.state.zenModeEnabled &&
!this.scene.getElementsIncludingDeleted().length !this.scene.getElementsIncludingDeleted().length
} }
app={this}
> >
{this.props.children} {this.props.children}
</LayerUI> </LayerUI>
@ -963,10 +961,7 @@ class App extends React.Component<AppProps, AppState> {
const shouldUpdateStrokeColor = const shouldUpdateStrokeColor =
(type === "background" && event.altKey) || (type === "background" && event.altKey) ||
(type === "stroke" && !event.altKey); (type === "stroke" && !event.altKey);
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getElementsIncludingDeleted(),
this.state,
);
if ( if (
!selectedElements.length || !selectedElements.length ||
this.state.activeTool.type !== "selection" this.state.activeTool.type !== "selection"
@ -2046,6 +2041,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
this,
), ),
() => { () => {
if (opts.files) { if (opts.files) {
@ -2610,14 +2606,11 @@ class App extends React.Component<AppProps, AppState> {
offsetY = step; offsetY = step;
} }
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements({
this.scene.getNonDeletedElements(), selectedElementIds: this.state.selectedElementIds,
this.state,
{
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}, });
);
selectedElements.forEach((element) => { selectedElements.forEach((element) => {
mutateElement(element, { mutateElement(element, {
@ -2634,10 +2627,7 @@ class App extends React.Component<AppProps, AppState> {
event.preventDefault(); event.preventDefault();
} else if (event.key === KEYS.ENTER) { } else if (event.key === KEYS.ENTER) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
const selectedElement = selectedElements[0]; const selectedElement = selectedElements[0];
if (event[KEYS.CTRL_OR_CMD]) { if (event[KEYS.CTRL_OR_CMD]) {
@ -2713,10 +2703,7 @@ class App extends React.Component<AppProps, AppState> {
!event.altKey && !event.altKey &&
!event[KEYS.CTRL_OR_CMD] !event[KEYS.CTRL_OR_CMD]
) { ) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if ( if (
this.state.activeTool.type === "selection" && this.state.activeTool.type === "selection" &&
!selectedElements.length !selectedElements.length
@ -2788,10 +2775,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true }); this.setState({ isBindingEnabled: true });
} }
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
isBindingEnabled(this.state) isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements) ? bindOrUnbindSelectedElements(selectedElements)
: unbindLinearElements(selectedElements); : unbindLinearElements(selectedElements);
@ -3141,10 +3125,7 @@ class App extends React.Component<AppProps, AppState> {
} }
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null; let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
if (isTextElement(selectedElements[0])) { if (isTextElement(selectedElements[0])) {
@ -3274,10 +3255,7 @@ class App extends React.Component<AppProps, AppState> {
return; return;
} }
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) { if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if ( if (
@ -3328,6 +3306,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
), ),
); );
return; return;
@ -3704,7 +3683,7 @@ class App extends React.Component<AppProps, AppState> {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if ( if (
selectedElements.length === 1 && selectedElements.length === 1 &&
!isOverScrollBar && !isOverScrollBar &&
@ -4407,10 +4386,7 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLElement>, event: React.PointerEvent<HTMLElement>,
): PointerDownState { ): PointerDownState {
const origin = viewportCoordsToSceneCoords(event, this.state); const origin = viewportCoordsToSceneCoords(event, this.state);
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements); const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
return { return {
@ -4528,7 +4504,7 @@ class App extends React.Component<AppProps, AppState> {
): boolean => { ): boolean => {
if (this.state.activeTool.type === "selection") { if (this.state.activeTool.type === "selection") {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const selectedElements = getSelectedElements(elements, this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithTransformHandleType = const elementWithTransformHandleType =
getElementWithTransformHandleType( getElementWithTransformHandleType(
@ -4771,6 +4747,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
pointerDownState.hit.wasAddedToSelection = true; pointerDownState.hit.wasAddedToSelection = true;
@ -5198,7 +5175,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.offset === null) { if (pointerDownState.drag.offset === null) {
pointerDownState.drag.offset = tupleToCoors( pointerDownState.drag.offset = tupleToCoors(
getDragOffsetXY( getDragOffsetXY(
getSelectedElements(this.scene.getNonDeletedElements(), this.state), this.scene.getSelectedElements(this.state),
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
), ),
@ -5361,10 +5338,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) && pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
!isSelectingPointsInLineEditor !isSelectingPointsInLineEditor
) { ) {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.every((element) => element.locked)) { if (selectedElements.every((element) => element.locked)) {
return; return;
@ -5435,14 +5409,18 @@ class App extends React.Component<AppProps, AppState> {
const groupIdMap = new Map(); const groupIdMap = new Map();
const oldIdToDuplicatedId = new Map(); const oldIdToDuplicatedId = new Map();
const hitElement = pointerDownState.hit.element; const hitElement = pointerDownState.hit.element;
const elements = this.scene.getElementsIncludingDeleted();
const selectedElementIds = new Set( const selectedElementIds = new Set(
getSelectedElements(elements, this.state, { this.scene
.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
includeBoundTextElement: true, includeBoundTextElement: true,
includeElementsInFrames: true, includeElementsInFrames: true,
}).map((element) => element.id), })
.map((element) => element.id),
); );
const elements = this.scene.getNonDeletedElements();
for (const element of elements) { for (const element of elements) {
if ( if (
selectedElementIds.has(element.id) || selectedElementIds.has(element.id) ||
@ -5584,6 +5562,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
), ),
); );
} }
@ -5641,6 +5620,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
} }
@ -5740,10 +5720,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit?.element?.id !== pointerDownState.hit?.element?.id !==
this.state.selectedLinearElement.elementId this.state.selectedLinearElement.elementId
) { ) {
const selectedELements = getSelectedElements( const selectedELements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles // set selectedLinearElement to null if there is more than one element selected since we don't want to show linear element handles
if (selectedELements.length > 1) { if (selectedELements.length > 1) {
this.setState({ selectedLinearElement: null }); this.setState({ selectedLinearElement: null });
@ -5985,10 +5962,7 @@ class App extends React.Component<AppProps, AppState> {
const topLayerFrame = const topLayerFrame =
this.getTopLayerFrameAtSceneCoords(sceneCoords); this.getTopLayerFrameAtSceneCoords(sceneCoords);
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
let nextElements = this.scene.getElementsIncludingDeleted(); let nextElements = this.scene.getElementsIncludingDeleted();
const updateGroupIdsAfterEditingGroup = ( const updateGroupIdsAfterEditingGroup = (
@ -6067,6 +6041,7 @@ class App extends React.Component<AppProps, AppState> {
nextElements = updateFrameMembershipOfSelectedElements( nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
this.state, this.state,
this,
); );
this.scene.replaceAllElements(nextElements); this.scene.replaceAllElements(nextElements);
@ -6111,12 +6086,12 @@ class App extends React.Component<AppProps, AppState> {
let nextElements = updateFrameMembershipOfSelectedElements( let nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
this.state, this.state,
this,
); );
const selectedFrames = getSelectedElements( const selectedFrames = this.scene
this.scene.getElementsIncludingDeleted(), .getSelectedElements(this.state)
this.state, .filter(
).filter(
(element) => element.type === "frame", (element) => element.type === "frame",
) as ExcalidrawFrameElement[]; ) as ExcalidrawFrameElement[];
@ -6143,10 +6118,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.selectedLinearElement?.elementId !== hitElement?.id && this.state.selectedLinearElement?.elementId !== hitElement?.id &&
isLinearElement(hitElement) isLinearElement(hitElement)
) { ) {
const selectedELements = getSelectedElements( const selectedELements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement when no other element selected except // set selectedLinearElement when no other element selected except
// the one we've hit // the one we've hit
if (selectedELements.length === 1) { if (selectedELements.length === 1) {
@ -6248,7 +6220,7 @@ class App extends React.Component<AppProps, AppState> {
delete newSelectedElementIds[hitElement!.id]; delete newSelectedElementIds[hitElement!.id];
const newSelectedElements = getSelectedElements( const newSelectedElements = getSelectedElements(
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
{ ...prevState, selectedElementIds: newSelectedElementIds }, { selectedElementIds: newSelectedElementIds },
); );
return selectGroupsForSelectedElements( return selectGroupsForSelectedElements(
@ -6267,6 +6239,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
} }
@ -6303,6 +6276,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
); );
}); });
} else { } else {
@ -6333,6 +6307,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
prevState, prevState,
this,
), ),
})); }));
} }
@ -6392,9 +6367,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
(isBindingEnabled(this.state) (isBindingEnabled(this.state)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements
: unbindLinearElements)( : unbindLinearElements)(this.scene.getSelectedElements(this.state));
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
);
} }
if (!activeTool.locked && activeTool.type !== "freedraw") { if (!activeTool.locked && activeTool.type !== "freedraw") {
@ -7101,10 +7074,7 @@ class App extends React.Component<AppProps, AppState> {
includeLockedElements: true, includeLockedElements: true,
}); });
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const isHittignCommonBoundBox = const isHittignCommonBoundBox =
this.isHittingCommonBoundingBoxOfSelectedElements( this.isHittingCommonBoundingBoxOfSelectedElements(
{ x, y }, { x, y },
@ -7134,6 +7104,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
this.state, this.state,
this,
) )
: this.state), : this.state),
showHyperlinkPopup: false, showHyperlinkPopup: false,
@ -7221,10 +7192,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState, pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent, event: MouseEvent | KeyboardEvent,
): boolean => { ): boolean => {
const selectedElements = getSelectedElements( const selectedElements = this.scene.getSelectedElements(this.state);
this.scene.getNonDeletedElements(),
this.state,
);
const selectedFrames = selectedElements.filter( const selectedFrames = selectedElements.filter(
(element) => element.type === "frame", (element) => element.type === "frame",
) as ExcalidrawFrameElement[]; ) as ExcalidrawFrameElement[];

View File

@ -82,7 +82,9 @@ export const ContextMenu = React.memo(
let label = ""; let label = "";
if (item.contextItemLabel) { if (item.contextItemLabel) {
if (typeof item.contextItemLabel === "function") { if (typeof item.contextItemLabel === "function") {
label = t(item.contextItemLabel(elements, appState)); label = t(
item.contextItemLabel(elements, appState, actionManager.app),
);
} else { } else {
label = t(item.contextItemLabel); label = t(item.contextItemLabel);
} }

View File

@ -1,7 +1,5 @@
import { t } from "../i18n"; import { t } from "../i18n";
import { NonDeletedExcalidrawElement } from "../element/types"; import { AppClassProperties, Device, UIAppState } from "../types";
import { getSelectedElements } from "../scene";
import { Device, UIAppState } from "../types";
import { import {
isImageElement, isImageElement,
isLinearElement, isLinearElement,
@ -15,17 +13,12 @@ import "./HintViewer.scss";
interface HintViewerProps { interface HintViewerProps {
appState: UIAppState; appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean; isMobile: boolean;
device: Device; device: Device;
app: AppClassProperties;
} }
const getHints = ({ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
appState,
elements,
isMobile,
device,
}: HintViewerProps) => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState; const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null; const multiMode = appState.multiElement !== null;
@ -55,7 +48,7 @@ const getHints = ({
return t("hints.placeImage"); return t("hints.placeImage");
} }
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app.scene.getSelectedElements(appState);
if ( if (
isResizing && isResizing &&
@ -115,15 +108,15 @@ const getHints = ({
export const HintViewer = ({ export const HintViewer = ({
appState, appState,
elements,
isMobile, isMobile,
device, device,
app,
}: HintViewerProps) => { }: HintViewerProps) => {
let hint = getHints({ let hint = getHints({
appState, appState,
elements,
isMobile, isMobile,
device, device,
app,
}); });
if (!hint) { if (!hint) {
return null; return null;

View File

@ -72,6 +72,7 @@ interface LayerUIProps {
onExportImage: AppClassProperties["onExportImage"]; onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
children?: React.ReactNode; children?: React.ReactNode;
app: AppClassProperties;
} }
const DefaultMainMenu: React.FC<{ const DefaultMainMenu: React.FC<{
@ -127,6 +128,7 @@ const LayerUI = ({
onExportImage, onExportImage,
renderWelcomeScreen, renderWelcomeScreen,
children, children,
app,
}: LayerUIProps) => { }: LayerUIProps) => {
const device = useDevice(); const device = useDevice();
const tunnels = useInitializeTunnels(); const tunnels = useInitializeTunnels();
@ -240,9 +242,9 @@ const LayerUI = ({
> >
<HintViewer <HintViewer
appState={appState} appState={appState}
elements={elements}
isMobile={device.isMobile} isMobile={device.isMobile}
device={device} device={device}
app={app}
/> />
{heading} {heading}
<Stack.Row gap={1}> <Stack.Row gap={1}>
@ -401,6 +403,7 @@ const LayerUI = ({
)} )}
{device.isMobile && ( {device.isMobile && (
<MobileMenu <MobileMenu
app={app}
appState={appState} appState={appState}
elements={elements} elements={elements}
actionManager={actionManager} actionManager={actionManager}

View File

@ -1,5 +1,11 @@
import React from "react"; import React from "react";
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types"; import {
AppClassProperties,
AppState,
Device,
ExcalidrawProps,
UIAppState,
} from "../types";
import { ActionManager } from "../actions/manager"; import { ActionManager } from "../actions/manager";
import { t } from "../i18n"; import { t } from "../i18n";
import Stack from "./Stack"; import Stack from "./Stack";
@ -41,6 +47,7 @@ type MobileMenuProps = {
renderSidebars: () => JSX.Element | null; renderSidebars: () => JSX.Element | null;
device: Device; device: Device;
renderWelcomeScreen: boolean; renderWelcomeScreen: boolean;
app: AppClassProperties;
}; };
export const MobileMenu = ({ export const MobileMenu = ({
@ -58,6 +65,7 @@ export const MobileMenu = ({
renderSidebars, renderSidebars,
device, device,
renderWelcomeScreen, renderWelcomeScreen,
app,
}: MobileMenuProps) => { }: MobileMenuProps) => {
const { const {
WelcomeScreenCenterTunnel, WelcomeScreenCenterTunnel,
@ -119,9 +127,9 @@ export const MobileMenu = ({
</Section> </Section>
<HintViewer <HintViewer
appState={appState} appState={appState}
elements={elements}
isMobile={true} isMobile={true}
device={device} device={device}
app={app}
/> />
</FixedSideContainer> </FixedSideContainer>
); );

View File

@ -16,7 +16,7 @@ import {
} from "./element/textElement"; } from "./element/textElement";
import { arrayToMap, findIndex } from "./utils"; import { arrayToMap, findIndex } from "./utils";
import { mutateElement } from "./element/mutateElement"; import { mutateElement } from "./element/mutateElement";
import { AppState } from "./types"; import { AppClassProperties, AppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene"; import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { isFrameElement } from "./element"; import { isFrameElement } from "./element";
import { moveOneRight } from "./zindex"; import { moveOneRight } from "./zindex";
@ -571,8 +571,13 @@ export const replaceAllElementsInFrame = (
export const updateFrameMembershipOfSelectedElements = ( export const updateFrameMembershipOfSelectedElements = (
allElements: ExcalidrawElementsIncludingDeleted, allElements: ExcalidrawElementsIncludingDeleted,
appState: AppState, appState: AppState,
app: AppClassProperties,
) => { ) => {
const selectedElements = getSelectedElements(allElements, appState); const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements: allElements,
});
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements); const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
if (appState.editingGroupId) { if (appState.editingGroupId) {

View File

@ -1,5 +1,10 @@
import { GroupId, ExcalidrawElement, NonDeleted } from "./element/types"; import {
import { AppState } from "./types"; GroupId,
ExcalidrawElement,
NonDeleted,
NonDeletedExcalidrawElement,
} from "./element/types";
import { AppClassProperties, AppState } from "./types";
import { getSelectedElements } from "./scene"; import { getSelectedElements } from "./scene";
import { getBoundTextElement } from "./element/textElement"; import { getBoundTextElement } from "./element/textElement";
import { makeNextSelectedElementIds } from "./scene/selection"; import { makeNextSelectedElementIds } from "./scene/selection";
@ -67,12 +72,23 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
*/ */
export const selectGroupsForSelectedElements = ( export const selectGroupsForSelectedElements = (
appState: AppState, appState: AppState,
elements: readonly NonDeleted<ExcalidrawElement>[], elements: readonly NonDeletedExcalidrawElement[],
prevAppState: AppState, prevAppState: AppState,
/**
* supply null in cases where you don't have access to App instance and
* you don't care about optimizing selectElements retrieval
*/
app: AppClassProperties | null,
): AppState => { ): AppState => {
let nextAppState: AppState = { ...appState, selectedGroupIds: {} }; let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
const selectedElements = getSelectedElements(elements, appState); const selectedElements = app
? app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements,
})
: getSelectedElements(elements, appState);
if (!selectedElements.length) { if (!selectedElements.length) {
return { return {

View File

@ -11,6 +11,9 @@ import {
} from "../element"; } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { isFrameElement } from "../element/typeChecks"; import { isFrameElement } from "../element/typeChecks";
import { getSelectedElements } from "./selection";
import { AppState } from "../types";
import { Assert, SameType } from "../utility-types";
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"]; type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
type ElementKey = ExcalidrawElement | ElementIdKey; type ElementKey = ExcalidrawElement | ElementIdKey;
@ -18,6 +21,31 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => void; type SceneStateCallback = () => void;
type SceneStateCallbackRemover = () => void; type SceneStateCallbackRemover = () => void;
type SelectionHash = string & { __brand: "selectionHash" };
const hashSelectionOpts = (
opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
) => {
const keys = ["includeBoundTextElement", "includeElementsInFrames"] as const;
type HashableKeys = Omit<typeof opts, "selectedElementIds" | "elements">;
// just to ensure we're hashing all expected keys
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type _ = Assert<
SameType<
Required<HashableKeys>,
Pick<Required<HashableKeys>, typeof keys[number]>
>
>;
let hash = "";
for (const key of keys) {
hash += `${key}:${opts[key] ? "1" : "0"}`;
}
return hash as SelectionHash;
};
// ideally this would be a branded type but it'd be insanely hard to work with // ideally this would be a branded type but it'd be insanely hard to work with
// in our codebase // in our codebase
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[]; export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
@ -68,6 +96,15 @@ class Scene {
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = []; private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
private frames: readonly ExcalidrawFrameElement[] = []; private frames: readonly ExcalidrawFrameElement[] = [];
private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>(); private elementsMap = new Map<ExcalidrawElement["id"], ExcalidrawElement>();
private selectedElementsCache: {
selectedElementIds: AppState["selectedElementIds"] | null;
elements: readonly NonDeletedExcalidrawElement[] | null;
cache: Map<SelectionHash, NonDeletedExcalidrawElement[]>;
} = {
selectedElementIds: null,
elements: null,
cache: new Map(),
};
getElementsIncludingDeleted() { getElementsIncludingDeleted() {
return this.elements; return this.elements;
@ -81,6 +118,52 @@ class Scene {
return this.frames; return this.frames;
} }
getSelectedElements(opts: {
// NOTE can be ommitted by making Scene constructor require App instance
selectedElementIds: AppState["selectedElementIds"];
/**
* for specific cases where you need to use elements not from current
* scene state. This in effect will likely result in cache-miss, and
* the cache won't be updated in this case.
*/
elements?: readonly ExcalidrawElement[];
// selection-related options
includeBoundTextElement?: boolean;
includeElementsInFrames?: boolean;
}): NonDeleted<ExcalidrawElement>[] {
const hash = hashSelectionOpts(opts);
const elements = opts?.elements || this.nonDeletedElements;
if (
this.selectedElementsCache.elements === elements &&
this.selectedElementsCache.selectedElementIds === opts.selectedElementIds
) {
const cached = this.selectedElementsCache.cache.get(hash);
if (cached) {
return cached;
}
} else if (opts?.elements == null) {
// if we're operating on latest scene elements and the cache is not
// storing the latest elements, clear the cache
this.selectedElementsCache.cache.clear();
}
const selectedElements = getSelectedElements(
elements,
{ selectedElementIds: opts.selectedElementIds },
opts,
);
// cache only if we're not using custom elements
if (opts?.elements == null) {
this.selectedElementsCache.selectedElementIds = opts.selectedElementIds;
this.selectedElementsCache.elements = this.nonDeletedElements;
this.selectedElementsCache.cache.set(hash, selectedElements);
}
return selectedElements;
}
getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] { getNonDeletedFrames(): readonly NonDeleted<ExcalidrawFrameElement>[] {
return this.nonDeletedFrames; return this.nonDeletedFrames;
} }
@ -168,11 +251,21 @@ class Scene {
} }
destroy() { destroy() {
this.nonDeletedElements = [];
this.elements = [];
this.nonDeletedFrames = [];
this.frames = [];
this.elementsMap.clear();
this.selectedElementsCache.selectedElementIds = null;
this.selectedElementsCache.elements = null;
this.selectedElementsCache.cache.clear();
Scene.sceneMapById.forEach((scene, elementKey) => { Scene.sceneMapById.forEach((scene, elementKey) => {
if (scene === this) { if (scene === this) {
Scene.sceneMapById.delete(elementKey); Scene.sceneMapById.delete(elementKey);
} }
}); });
// done not for memory leaks, but to guard against possible late fires // done not for memory leaks, but to guard against possible late fires
// (I guess?) // (I guess?)
this.callbacks.clear(); this.callbacks.clear();

View File

@ -1527,14 +1527,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -1586,14 +1586,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -4271,14 +4271,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -4303,14 +4303,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 400692809, "versionNonce": 238820263,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@ -4362,14 +4362,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -4405,14 +4405,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -4434,14 +4434,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 2019559783, "versionNonce": 401146281,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@ -4482,14 +4482,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1116226695, "versionNonce": 1150084233,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -4513,14 +4513,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1014066025, "versionNonce": 1116226695,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@ -4557,14 +4557,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 20, "width": 20,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -4586,14 +4586,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 4, "version": 4,
"versionNonce": 400692809, "versionNonce": 238820263,
"width": 20, "width": 20,
"x": 20, "x": 20,
"y": 30, "y": 30,
@ -5585,14 +5585,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1014066025, "versionNonce": 1116226695,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -5619,14 +5619,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 0, "y": 0,
@ -5678,14 +5678,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -5721,14 +5721,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 453191, "versionNonce": 449462985,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -5750,14 +5750,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 2, "version": 2,
"versionNonce": 2019559783, "versionNonce": 401146281,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 0, "y": 0,
@ -5798,14 +5798,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 449462985, "seed": 1278240551,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 1014066025, "versionNonce": 1116226695,
"width": 10, "width": 10,
"x": -10, "x": -10,
"y": 0, "y": 0,
@ -5829,14 +5829,14 @@ Object {
"roundness": Object { "roundness": Object {
"type": 3, "type": 3,
}, },
"seed": 401146281, "seed": 453191,
"strokeColor": "#1e1e1e", "strokeColor": "#1e1e1e",
"strokeStyle": "solid", "strokeStyle": "solid",
"strokeWidth": 1, "strokeWidth": 1,
"type": "rectangle", "type": "rectangle",
"updated": 1, "updated": 1,
"version": 3, "version": 3,
"versionNonce": 238820263, "versionNonce": 1014066025,
"width": 10, "width": 10,
"x": 10, "x": 10,
"y": 0, "y": 0,

File diff suppressed because it is too large Load Diff

View File

@ -90,6 +90,7 @@ const populateElements = (
{ ...h.state, ...appState, selectedElementIds }, { ...h.state, ...appState, selectedElementIds },
h.elements, h.elements,
h.state, h.state,
null,
), ),
...appState, ...appState,
selectedElementIds, selectedElementIds,

View File

@ -47,3 +47,6 @@ export type ForwardRef<T, P = any> = Parameters<
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U> export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
? U ? U
: never; : never;
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
export type Assert<T extends true> = T;