Compare commits

..

1 Commits

Author SHA1 Message Date
dwelle b8da178ebd feat: update eye-dropper icon 2023-07-07 12:22:52 +02:00
39 changed files with 4155 additions and 1471 deletions
+10 -5
View File
@@ -1,4 +1,6 @@
import { register } from "./register";
import { getSelectedElements } from "../scene";
import { getNonDeletedElements } from "../element";
import { deepCopyElement } from "../element/newElement";
import { randomId } from "../random";
import { t } from "../i18n";
@@ -7,11 +9,14 @@ export const actionAddToLibrary = register({
name: "addToLibrary",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
if (selectedElements.some((element) => element.type === "image")) {
return {
commitToHistory: false,
+34 -32
View File
@@ -13,18 +13,19 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { AppClassProperties, AppState } from "../types";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const alignActionsPredicate = (
elements: readonly ExcalidrawElement[],
appState: AppState,
_: unknown,
app: AppClassProperties,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length > 1 &&
// TODO enable aligning frames when implemented properly
@@ -35,10 +36,12 @@ const alignActionsPredicate = (
const alignSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
app: AppClassProperties,
alignment: Alignment,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = alignElements(selectedElements, alignment);
@@ -47,7 +50,6 @@ const alignSelectedElements = (
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app,
);
};
@@ -55,10 +57,10 @@ export const actionAlignTop = register({
name: "alignTop",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "start",
axis: "y",
}),
@@ -67,9 +69,9 @@ export const actionAlignTop = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_UP,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignTopIcon}
onClick={() => updateData(null)}
@@ -86,10 +88,10 @@ export const actionAlignBottom = register({
name: "alignBottom",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "end",
axis: "y",
}),
@@ -98,9 +100,9 @@ export const actionAlignBottom = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_DOWN,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignBottomIcon}
onClick={() => updateData(null)}
@@ -117,10 +119,10 @@ export const actionAlignLeft = register({
name: "alignLeft",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "start",
axis: "x",
}),
@@ -129,9 +131,9 @@ export const actionAlignLeft = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_LEFT,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignLeftIcon}
onClick={() => updateData(null)}
@@ -148,10 +150,10 @@ export const actionAlignRight = register({
name: "alignRight",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "end",
axis: "x",
}),
@@ -160,9 +162,9 @@ export const actionAlignRight = register({
},
keyTest: (event) =>
event[KEYS.CTRL_OR_CMD] && event.shiftKey && event.key === KEYS.ARROW_RIGHT,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={AlignRightIcon}
onClick={() => updateData(null)}
@@ -179,19 +181,19 @@ export const actionAlignVerticallyCentered = register({
name: "alignVerticallyCentered",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "center",
axis: "y",
}),
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={CenterVerticallyIcon}
onClick={() => updateData(null)}
@@ -206,19 +208,19 @@ export const actionAlignHorizontallyCentered = register({
name: "alignHorizontallyCentered",
trackEvent: { category: "element" },
predicate: alignActionsPredicate,
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: alignSelectedElements(elements, appState, app, {
elements: alignSelectedElements(elements, appState, {
position: "center",
axis: "x",
}),
commitToHistory: true,
};
},
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!alignActionsPredicate(elements, appState, null, app)}
hidden={!alignActionsPredicate(elements, appState)}
type="button"
icon={CenterHorizontallyIcon}
onClick={() => updateData(null)}
+24 -15
View File
@@ -4,7 +4,7 @@ import {
VERTICAL_ALIGN,
TEXT_ALIGN,
} from "../constants";
import { isTextElement, newElement } from "../element";
import { getNonDeletedElements, isTextElement, newElement } from "../element";
import { mutateElement } from "../element/mutateElement";
import {
computeBoundTextPosition,
@@ -29,8 +29,8 @@ import {
ExcalidrawTextContainer,
ExcalidrawTextElement,
} from "../element/types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { Mutable } from "../utility-types";
import { getFontString } from "../utils";
import { register } from "./register";
@@ -38,13 +38,16 @@ export const actionUnbindText = register({
name: "unbindText",
contextItemLabel: "labels.unbindText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.some((element) => hasBoundTextElement(element));
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
selectedElements.forEach((element) => {
const boundTextElement = getBoundTextElement(element);
if (boundTextElement) {
@@ -89,8 +92,8 @@ export const actionBindText = register({
name: "bindText",
contextItemLabel: "labels.bindText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 2) {
const textElement =
@@ -113,8 +116,11 @@ export const actionBindText = register({
}
return false;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
let textElement: ExcalidrawTextElement;
let container: ExcalidrawTextContainer;
@@ -194,15 +200,18 @@ export const actionWrapTextInContainer = register({
name: "wrapTextInContainer",
contextItemLabel: "labels.createContainerFromText",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
const areTextElements = selectedElements.every((el) => isTextElement(el));
return selectedElements.length > 0 && areTextElements;
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
let updatedElements: readonly ExcalidrawElement[] = elements.slice();
const containerIds: Mutable<AppState["selectedElementIds"]> = {};
const containerIds: AppState["selectedElementIds"] = {};
for (const textElement of selectedElements) {
if (isTextElement(textElement)) {
+11 -5
View File
@@ -6,7 +6,7 @@ import { getCommonBounds, getNonDeletedElements } from "../element";
import { ExcalidrawElement } from "../element/types";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { getNormalizedZoom } from "../scene";
import { getNormalizedZoom, getSelectedElements } from "../scene";
import { centerScrollOn } from "../scene/scroll";
import { getStateForZoom } from "../scene/zoom";
import { AppState, NormalizedZoomValue } from "../types";
@@ -302,8 +302,11 @@ export const zoomToFit = ({
export const actionZoomToFitSelectionInViewport = register({
name: "zoomToFitSelectionInViewport",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState,
@@ -322,8 +325,11 @@ export const actionZoomToFitSelectionInViewport = register({
export const actionZoomToFitSelection = register({
name: "zoomToFitSelection",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return zoomToFit({
targetElements: selectedElements.length ? selectedElements : elements,
appState,
+30 -24
View File
@@ -7,6 +7,7 @@ import {
probablySupportsClipboardWriteText,
} from "../clipboard";
import { actionDeleteSelected } from "./actionDeleteSelected";
import { getSelectedElements } from "../scene/selection";
import { exportCanvas } from "../data/index";
import { getNonDeletedElements, isTextElement } from "../element";
import { t } from "../i18n";
@@ -15,8 +16,7 @@ export const actionCopy = register({
name: "copy",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const elementsToCopy = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
const elementsToCopy = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
@@ -75,11 +75,14 @@ export const actionCopyAsSvg = register({
commitToHistory: false,
};
}
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
try {
await exportCanvas(
"clipboard-svg",
@@ -119,11 +122,14 @@ export const actionCopyAsPng = register({
commitToHistory: false,
};
}
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
try {
await exportCanvas(
"clipboard",
@@ -171,11 +177,14 @@ export const actionCopyAsPng = register({
export const copyText = register({
name: "copyText",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
});
perform: (elements, appState) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
);
const text = selectedElements
.reduce((acc: string[], element) => {
@@ -190,15 +199,12 @@ export const copyText = register({
commitToHistory: false,
};
},
predicate: (elements, appState, _, app) => {
predicate: (elements, appState) => {
return (
probablySupportsClipboardWriteText &&
app.scene
.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})
.some(isTextElement)
getSelectedElements(elements, appState, {
includeBoundTextElement: true,
}).some(isTextElement)
);
},
contextItemLabel: "labels.copyText",
+22 -15
View File
@@ -9,13 +9,19 @@ import { ExcalidrawElement } from "../element/types";
import { updateFrameMembershipOfSelectedElements } from "../frame";
import { t } from "../i18n";
import { CODES, KEYS } from "../keys";
import { isSomeElementSelected } from "../scene";
import { AppClassProperties, AppState } from "../types";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import { AppState } from "../types";
import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
const selectedElements = app.scene.getSelectedElements(appState);
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return (
selectedElements.length > 1 &&
// TODO enable distributing frames when implemented properly
@@ -26,10 +32,12 @@ const enableActionGroup = (appState: AppState, app: AppClassProperties) => {
const distributeSelectedElements = (
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
app: AppClassProperties,
distribution: Distribution,
) => {
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
const updatedElements = distributeElements(selectedElements, distribution);
@@ -38,17 +46,16 @@ const distributeSelectedElements = (
return updateFrameMembershipOfSelectedElements(
elements.map((element) => updatedElementsMap.get(element.id) || element),
appState,
app,
);
};
export const distributeHorizontally = register({
name: "distributeHorizontally",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: distributeSelectedElements(elements, appState, app, {
elements: distributeSelectedElements(elements, appState, {
space: "between",
axis: "x",
}),
@@ -57,9 +64,9 @@ export const distributeHorizontally = register({
},
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.H,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(appState, app)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={DistributeHorizontallyIcon}
onClick={() => updateData(null)}
@@ -75,10 +82,10 @@ export const distributeHorizontally = register({
export const distributeVertically = register({
name: "distributeVertically",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
appState,
elements: distributeSelectedElements(elements, appState, app, {
elements: distributeSelectedElements(elements, appState, {
space: "between",
axis: "y",
}),
@@ -87,9 +94,9 @@ export const distributeVertically = register({
},
keyTest: (event) =>
!event[KEYS.CTRL_OR_CMD] && event.altKey && event.code === CODES.V,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(appState, app)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={DistributeVerticallyIcon}
onClick={() => updateData(null)}
-2
View File
@@ -274,8 +274,6 @@ const duplicateElements = (
),
},
getNonDeletedElements(finalElements),
appState,
null,
),
};
};
+9 -11
View File
@@ -1,6 +1,7 @@
import { newElementWith } from "../element/mutateElement";
import { ExcalidrawElement } from "../element/types";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { arrayToMap } from "../utils";
import { register } from "./register";
@@ -10,15 +11,14 @@ const shouldLock = (elements: readonly ExcalidrawElement[]) =>
export const actionToggleElementLock = register({
name: "toggleElementLock",
trackEvent: { category: "element" },
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return !selectedElements.some(
(element) => element.locked && element.frameId,
);
},
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
perform: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState, {
includeBoundTextElement: true,
includeElementsInFrames: true,
});
@@ -46,9 +46,8 @@ export const actionToggleElementLock = register({
commitToHistory: true,
};
},
contextItemLabel: (elements, appState, app) => {
const selected = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
contextItemLabel: (elements, appState) => {
const selected = getSelectedElements(elements, appState, {
includeBoundTextElement: false,
});
if (selected.length === 1 && selected[0].type !== "frame") {
@@ -61,13 +60,12 @@ export const actionToggleElementLock = register({
? "labels.elementLock.lockAll"
: "labels.elementLock.unlockAll";
},
keyTest: (event, appState, elements, app) => {
keyTest: (event, appState, elements) => {
return (
event.key.toLocaleLowerCase() === KEYS.L &&
event[KEYS.CTRL_OR_CMD] &&
event.shiftKey &&
app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
getSelectedElements(elements, appState, {
includeBoundTextElement: false,
}).length > 0
);
+7
View File
@@ -125,6 +125,13 @@ export const actionFinalize = register({
{ x, y },
);
}
if (
!appState.activeTool.locked &&
appState.activeTool.type !== "freedraw"
) {
appState.selectedElementIds[multiPointElement.id] = true;
}
}
if (
+2 -4
View File
@@ -17,12 +17,11 @@ import { updateFrameMembershipOfSelectedElements } from "../frame";
export const actionFlipHorizontal = register({
name: "flipHorizontal",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "horizontal"),
appState,
app,
),
appState,
commitToHistory: true,
@@ -35,12 +34,11 @@ export const actionFlipHorizontal = register({
export const actionFlipVertical = register({
name: "flipVertical",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
perform: (elements, appState) => {
return {
elements: updateFrameMembershipOfSelectedElements(
flipSelectedElements(elements, appState, "vertical"),
appState,
app,
),
appState,
commitToHistory: true,
+27 -19
View File
@@ -3,12 +3,19 @@ import { ExcalidrawElement } from "../element/types";
import { removeAllElementsFromFrame } from "../frame";
import { getFrameElements } from "../frame";
import { KEYS } from "../keys";
import { AppClassProperties, AppState } from "../types";
import { getSelectedElements } from "../scene";
import { AppState } from "../types";
import { setCursorForShape, updateActiveTool } from "../utils";
import { register } from "./register";
const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
const selectedElements = app.scene.getSelectedElements(appState);
const isSingleFrameSelected = (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
);
return selectedElements.length === 1 && selectedElements[0].type === "frame";
};
@@ -16,8 +23,11 @@ const isSingleFrameSelected = (appState: AppState, app: AppClassProperties) => {
export const actionSelectAllElementsInFrame = register({
name: "selectAllElementsInFrame",
trackEvent: { category: "canvas" },
perform: (elements, appState, _, app) => {
const selectedFrame = app.scene.getSelectedElements(appState)[0];
perform: (elements, appState) => {
const selectedFrame = getSelectedElements(
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") {
const elementsInFrame = getFrameElements(
@@ -45,15 +55,17 @@ export const actionSelectAllElementsInFrame = register({
};
},
contextItemLabel: "labels.selectAllElementsInFrame",
predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionRemoveAllElementsFromFrame = register({
name: "removeAllElementsFromFrame",
trackEvent: { category: "history" },
perform: (elements, appState, _, app) => {
const selectedFrame = app.scene.getSelectedElements(appState)[0];
perform: (elements, appState) => {
const selectedFrame = getSelectedElements(
getNonDeletedElements(elements),
appState,
)[0];
if (selectedFrame && selectedFrame.type === "frame") {
return {
@@ -75,12 +87,11 @@ export const actionRemoveAllElementsFromFrame = register({
};
},
contextItemLabel: "labels.removeAllElementsFromFrame",
predicate: (elements, appState, _, app) =>
isSingleFrameSelected(appState, app),
predicate: (elements, appState) => isSingleFrameSelected(elements, appState),
});
export const actionupdateFrameRendering = register({
name: "updateFrameRendering",
export const actionToggleFrameRendering = register({
name: "toggleFrameRendering",
viewMode: true,
trackEvent: { category: "canvas" },
perform: (elements, appState) => {
@@ -88,16 +99,13 @@ export const actionupdateFrameRendering = register({
elements,
appState: {
...appState,
frameRendering: {
...appState.frameRendering,
enabled: !appState.frameRendering.enabled,
},
shouldRenderFrames: !appState.shouldRenderFrames,
},
commitToHistory: false,
};
},
contextItemLabel: "labels.updateFrameRendering",
checked: (appState: AppState) => appState.frameRendering.enabled,
contextItemLabel: "labels.toggleFrameRendering",
checked: (appState: AppState) => appState.shouldRenderFrames,
});
export const actionSetFrameAsActiveTool = register({
+22 -29
View File
@@ -4,7 +4,7 @@ import { arrayToMap, getShortcutKey } from "../utils";
import { register } from "./register";
import { UngroupIcon, GroupIcon } from "../components/icons";
import { newElementWith } from "../element/mutateElement";
import { isSomeElementSelected } from "../scene";
import { getSelectedElements, isSomeElementSelected } from "../scene";
import {
getSelectedGroupIds,
selectGroup,
@@ -22,7 +22,7 @@ import {
ExcalidrawFrameElement,
ExcalidrawTextElement,
} from "../element/types";
import { AppClassProperties, AppState } from "../types";
import { AppState } from "../types";
import { isBoundToContainer } from "../element/typeChecks";
import {
getElementsInResizingFrame,
@@ -51,12 +51,14 @@ const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
const enableActionGroup = (
elements: readonly ExcalidrawElement[],
appState: AppState,
app: AppClassProperties,
) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
);
return (
selectedElements.length >= 2 && !allElementsInSameGroup(selectedElements)
);
@@ -66,10 +68,13 @@ export const actionGroup = register({
name: "group",
trackEvent: { category: "element" },
perform: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
});
const selectedElements = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
);
if (selectedElements.length < 2) {
// nothing to group
return { appState, elements, commitToHistory: false };
@@ -159,13 +164,12 @@ export const actionGroup = register({
};
},
contextItemLabel: "labels.group",
predicate: (elements, appState, _, app) =>
enableActionGroup(elements, appState, app),
predicate: (elements, appState) => enableActionGroup(elements, appState),
keyTest: (event) =>
!event.shiftKey && event[KEYS.CTRL_OR_CMD] && event.key === KEYS.G,
PanelComponent: ({ elements, appState, updateData, app }) => (
PanelComponent: ({ elements, appState, updateData }) => (
<ToolButton
hidden={!enableActionGroup(elements, appState, app)}
hidden={!enableActionGroup(elements, appState)}
type="button"
icon={<GroupIcon theme={appState.theme} />}
onClick={() => updateData(null)}
@@ -187,7 +191,7 @@ export const actionUngroup = register({
let nextElements = [...elements];
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(nextElements, appState);
const frames = selectedElements
.filter((element) => element.frameId)
.map((element) =>
@@ -214,8 +218,6 @@ export const actionUngroup = register({
const updateAppState = selectGroupsForSelectedElements(
{ ...appState, selectedGroupIds: {} },
getNonDeletedElements(nextElements),
appState,
null,
);
frames.forEach((frame) => {
@@ -230,18 +232,9 @@ export const actionUngroup = register({
});
// remove binded text elements from selection
updateAppState.selectedElementIds = Object.entries(
updateAppState.selectedElementIds,
).reduce(
(acc: { [key: ExcalidrawElement["id"]]: true }, [id, selected]) => {
if (selected && !boundTextElementIds.includes(id)) {
acc[id] = true;
}
return acc;
},
{},
boundTextElementIds.forEach(
(id) => (updateAppState.selectedElementIds[id] = false),
);
return {
appState: updateAppState,
elements: nextElements,
+19 -11
View File
@@ -1,6 +1,8 @@
import { getNonDeletedElements } from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
import { isLinearElement } from "../element/typeChecks";
import { ExcalidrawLinearElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { register } from "./register";
export const actionToggleLinearEditor = register({
@@ -8,18 +10,21 @@ export const actionToggleLinearEditor = register({
trackEvent: {
category: "element",
},
predicate: (elements, appState, _, app) => {
const selectedElements = app.scene.getSelectedElements(appState);
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
return true;
}
return false;
},
perform(elements, appState, _, app) {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
const selectedElement = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
)[0] as ExcalidrawLinearElement;
const editingLinearElement =
appState.editingLinearElement?.elementId === selectedElement.id
@@ -33,11 +38,14 @@ export const actionToggleLinearEditor = register({
commitToHistory: false,
};
},
contextItemLabel: (elements, appState, app) => {
const selectedElement = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
includeBoundTextElement: true,
})[0] as ExcalidrawLinearElement;
contextItemLabel: (elements, appState) => {
const selectedElement = getSelectedElements(
getNonDeletedElements(elements),
appState,
{
includeBoundTextElement: true,
},
)[0] as ExcalidrawLinearElement;
return appState.editingLinearElement?.elementId === selectedElement.id
? "labels.lineEditor.exit"
: "labels.lineEditor.edit";
-2
View File
@@ -41,8 +41,6 @@ export const actionSelectAll = register({
selectedElementIds,
},
getNonDeletedElements(elements),
appState,
app,
),
commitToHistory: true,
};
-2
View File
@@ -90,7 +90,6 @@ export class ActionManager {
event,
this.getAppState(),
this.getElementsIncludingDeleted(),
this.app,
),
);
@@ -169,7 +168,6 @@ export class ActionManager {
appState={this.getAppState()}
updateData={updateData}
appProps={this.app.props}
app={this.app}
data={data}
/>
);
+1 -4
View File
@@ -119,7 +119,7 @@ export type ActionName =
| "toggleHandTool"
| "selectAllElementsInFrame"
| "removeAllElementsFromFrame"
| "updateFrameRendering"
| "toggleFrameRendering"
| "setFrameAsActiveTool"
| "createContainerFromText"
| "wrapTextInContainer";
@@ -130,7 +130,6 @@ export type PanelComponentProps = {
updateData: (formData?: any) => void;
appProps: ExcalidrawProps;
data?: Record<string, any>;
app: AppClassProperties;
};
export interface Action {
@@ -142,14 +141,12 @@ export interface Action {
event: React.KeyboardEvent | KeyboardEvent,
appState: AppState,
elements: readonly ExcalidrawElement[],
app: AppClassProperties,
) => boolean;
contextItemLabel?:
| string
| ((
elements: readonly ExcalidrawElement[],
appState: Readonly<AppState>,
app: AppClassProperties,
) => string);
predicate?: (
elements: readonly ExcalidrawElement[],
+2 -2
View File
@@ -84,7 +84,7 @@ export const getDefaultAppState = (): Omit<
showStats: false,
startBoundElement: null,
suggestedBindings: [],
frameRendering: { enabled: true, clip: true, name: true, outline: true },
shouldRenderFrames: true,
frameToHighlight: null,
editingFrame: null,
elementsToHighlight: null,
@@ -191,7 +191,7 @@ const APP_STATE_STORAGE_CONF = (<
showStats: { browser: true, export: false, server: false },
startBoundElement: { browser: false, export: false, server: false },
suggestedBindings: { browser: false, export: false, server: false },
frameRendering: { browser: false, export: false, server: false },
shouldRenderFrames: { browser: false, export: false, server: false },
frameToHighlight: { browser: false, export: false, server: false },
editingFrame: { browser: false, export: false, server: false },
elementsToHighlight: { browser: false, export: false, server: false },
+218 -277
View File
@@ -315,10 +315,7 @@ import {
updateFrameMembershipOfSelectedElements,
isElementInFrame,
} from "../frame";
import {
excludeElementsInFramesFromSelection,
makeNextSelectedElementIds,
} from "../scene/selection";
import { excludeElementsInFramesFromSelection } from "../scene/selection";
import { actionPaste } from "../actions/actionClipboard";
import {
actionRemoveAllElementsFromFrame,
@@ -330,7 +327,6 @@ import { activeConfirmDialogAtom } from "./ActiveConfirmDialog";
import { actionWrapTextInContainer } from "../actions/actionBoundText";
import BraveMeasureTextError from "./BraveMeasureTextError";
import { activeEyeDropperAtom } from "./EyeDropper";
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!);
@@ -474,6 +470,8 @@ class App extends React.Component<AppProps, AppState> {
name,
width: window.innerWidth,
height: window.innerHeight,
showHyperlinkPopup: false,
defaultSidebarDockedPreference: false,
};
this.id = nanoid();
@@ -504,7 +502,7 @@ class App extends React.Component<AppProps, AppState> {
setActiveTool: this.setActiveTool,
setCursor: this.setCursor,
resetCursor: this.resetCursor,
updateFrameRendering: this.updateFrameRendering,
toggleFrameRendering: this.toggleFrameRendering,
toggleSidebar: this.toggleSidebar,
} as const;
if (typeof excalidrawRef === "function") {
@@ -650,7 +648,7 @@ class App extends React.Component<AppProps, AppState> {
};
private renderFrameNames = () => {
if (!this.state.frameRendering.enabled || !this.state.frameRendering.name) {
if (!this.state.shouldRenderFrames) {
return null;
}
@@ -798,7 +796,10 @@ class App extends React.Component<AppProps, AppState> {
};
public render() {
const selectedElement = this.scene.getSelectedElements(this.state);
const selectedElement = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
const { renderTopRightUI, renderCustomStats } = this.props;
return (
@@ -855,7 +856,6 @@ class App extends React.Component<AppProps, AppState> {
!this.state.zenModeEnabled &&
!this.scene.getElementsIncludingDeleted().length
}
app={this}
>
{this.props.children}
</LayerUI>
@@ -961,7 +961,10 @@ class App extends React.Component<AppProps, AppState> {
const shouldUpdateStrokeColor =
(type === "background" && event.altKey) ||
(type === "stroke" && !event.altKey);
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
);
if (
!selectedElements.length ||
this.state.activeTool.type !== "selection"
@@ -1350,7 +1353,6 @@ class App extends React.Component<AppProps, AppState> {
this.scene.destroy();
this.library.destroy();
clearTimeout(touchTimeout);
isSomeElementSelected.clearCache();
touchTimeout = 0;
}
@@ -1823,7 +1825,7 @@ class App extends React.Component<AppProps, AppState> {
if (event.touches.length === 2) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
});
}
};
@@ -1833,10 +1835,7 @@ class App extends React.Component<AppProps, AppState> {
if (event.touches.length > 0) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: makeNextSelectedElementIds(
this.state.previousSelectedElementIds,
this.state,
),
selectedElementIds: this.state.previousSelectedElementIds,
});
} else {
gesture.pointers.clear();
@@ -1896,14 +1895,7 @@ class App extends React.Component<AppProps, AppState> {
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[imageElement.id]: true,
},
this.state,
),
});
this.setState({ selectedElementIds: { [imageElement.id]: true } });
return;
}
@@ -2025,7 +2017,7 @@ class App extends React.Component<AppProps, AppState> {
openSidebar:
this.state.openSidebar &&
this.device.canDeviceFitSidebar &&
jotaiStore.get(isSidebarDockedAtom)
this.state.defaultSidebarDockedPreference
? this.state.openSidebar
: null,
selectedElementIds: nextElementsToSelect.reduce(
@@ -2040,8 +2032,6 @@ class App extends React.Component<AppProps, AppState> {
selectedGroupIds: {},
},
this.scene.getNonDeletedElements(),
this.state,
this,
),
() => {
if (opts.files) {
@@ -2140,9 +2130,8 @@ class App extends React.Component<AppProps, AppState> {
}
this.setState({
selectedElementIds: makeNextSelectedElementIds(
Object.fromEntries(textElements.map((el) => [el.id, true])),
this.state,
selectedElementIds: Object.fromEntries(
textElements.map((el) => [el.id, true]),
),
});
@@ -2203,23 +2192,10 @@ class App extends React.Component<AppProps, AppState> {
});
};
updateFrameRendering = (
opts:
| Partial<AppState["frameRendering"]>
| ((
prevState: AppState["frameRendering"],
) => Partial<AppState["frameRendering"]>),
) => {
toggleFrameRendering = () => {
this.setState((prevState) => {
const next =
typeof opts === "function" ? opts(prevState.frameRendering) : opts;
return {
frameRendering: {
enabled: next?.enabled ?? prevState.frameRendering.enabled,
clip: next?.clip ?? prevState.frameRendering.clip,
name: next?.name ?? prevState.frameRendering.name,
outline: next?.outline ?? prevState.frameRendering.outline,
},
shouldRenderFrames: !prevState.shouldRenderFrames,
};
});
};
@@ -2606,11 +2582,14 @@ class App extends React.Component<AppProps, AppState> {
offsetY = step;
}
const selectedElements = this.scene.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
});
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
{
includeBoundTextElement: true,
includeElementsInFrames: true,
},
);
selectedElements.forEach((element) => {
mutateElement(element, {
@@ -2627,7 +2606,10 @@ class App extends React.Component<AppProps, AppState> {
event.preventDefault();
} else if (event.key === KEYS.ENTER) {
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1) {
const selectedElement = selectedElements[0];
if (event[KEYS.CTRL_OR_CMD]) {
@@ -2703,7 +2685,10 @@ class App extends React.Component<AppProps, AppState> {
!event.altKey &&
!event[KEYS.CTRL_OR_CMD]
) {
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
if (
this.state.activeTool.type === "selection" &&
!selectedElements.length
@@ -2764,7 +2749,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
setCursorForShape(this.canvas, this.state);
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -2775,7 +2760,10 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ isBindingEnabled: true });
}
if (isArrowKey(event.key)) {
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements)
: unbindLinearElements(selectedElements);
@@ -2806,7 +2794,7 @@ class App extends React.Component<AppProps, AppState> {
if (nextActiveTool.type !== "selection") {
this.setState({
activeTool: nextActiveTool,
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -2843,7 +2831,7 @@ class App extends React.Component<AppProps, AppState> {
// elements by mistake while zooming
if (this.isTouchScreenMultiTouchGesture()) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
});
}
gesture.initialScale = this.state.zoom.value;
@@ -2888,10 +2876,7 @@ class App extends React.Component<AppProps, AppState> {
if (this.isTouchScreenMultiTouchGesture()) {
this.setState({
previousSelectedElementIds: {},
selectedElementIds: makeNextSelectedElementIds(
this.state.previousSelectedElementIds,
this.state,
),
selectedElementIds: this.state.previousSelectedElementIds,
});
}
gesture.initialScale = null;
@@ -2956,13 +2941,10 @@ class App extends React.Component<AppProps, AppState> {
? element.containerId
: element.id;
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[elementIdToSelect]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[elementIdToSelect]: true,
},
}));
}
if (isDeleted) {
@@ -2998,7 +2980,7 @@ class App extends React.Component<AppProps, AppState> {
private deselectElements() {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -3085,9 +3067,7 @@ class App extends React.Component<AppProps, AppState> {
).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit
const containingFrame = getContainingFrame(element);
return containingFrame &&
this.state.frameRendering.enabled &&
this.state.frameRendering.clip
return containingFrame && this.state.shouldRenderFrames
? isCursorInFrame({ x, y }, containingFrame)
: true;
});
@@ -3125,7 +3105,10 @@ class App extends React.Component<AppProps, AppState> {
}
let existingTextElement: NonDeleted<ExcalidrawTextElement> | null = null;
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1) {
if (isTextElement(selectedElements[0])) {
@@ -3255,7 +3238,10 @@ class App extends React.Component<AppProps, AppState> {
return;
}
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.length === 1 && isLinearElement(selectedElements[0])) {
if (
@@ -3305,8 +3291,6 @@ class App extends React.Component<AppProps, AppState> {
selectedGroupIds: {},
},
this.scene.getNonDeletedElements(),
prevState,
this,
),
);
return;
@@ -3683,7 +3667,7 @@ class App extends React.Component<AppProps, AppState> {
const elements = this.scene.getNonDeletedElements();
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(elements, this.state);
if (
selectedElements.length === 1 &&
!isOverScrollBar &&
@@ -4014,15 +3998,12 @@ class App extends React.Component<AppProps, AppState> {
editingElement: null,
startBoundElement: null,
suggestedBindings: [],
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,
),
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;
}, {}),
},
});
return;
@@ -4386,7 +4367,10 @@ class App extends React.Component<AppProps, AppState> {
event: React.PointerEvent<HTMLElement>,
): PointerDownState {
const origin = viewportCoordsToSceneCoords(event, this.state);
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
const [minX, minY, maxX, maxY] = getCommonBounds(selectedElements);
return {
@@ -4488,7 +4472,7 @@ class App extends React.Component<AppProps, AppState> {
private clearSelectionIfNotUsingSelection = (): void => {
if (this.state.activeTool.type !== "selection") {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -4504,7 +4488,7 @@ class App extends React.Component<AppProps, AppState> {
): boolean => {
if (this.state.activeTool.type === "selection") {
const elements = this.scene.getNonDeletedElements();
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(elements, this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithTransformHandleType =
getElementWithTransformHandleType(
@@ -4620,12 +4604,9 @@ class App extends React.Component<AppProps, AppState> {
if (this.state.editingLinearElement) {
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{
[this.state.editingLinearElement.elementId]: true,
},
this.state,
),
selectedElementIds: {
[this.state.editingLinearElement.elementId]: true,
},
});
// If we click on something
} else if (hitElement != null) {
@@ -4653,7 +4634,7 @@ class App extends React.Component<AppProps, AppState> {
!isElementInGroup(hitElement, this.state.editingGroupId)
) {
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -4669,7 +4650,7 @@ class App extends React.Component<AppProps, AppState> {
!pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements
) {
this.setState((prevState) => {
const nextSelectedElementIds: { [id: string]: true } = {
const nextSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement.id]: true,
};
@@ -4687,13 +4668,13 @@ class App extends React.Component<AppProps, AppState> {
previouslySelectedElements,
hitElement.id,
).forEach((element) => {
delete nextSelectedElementIds[element.id];
nextSelectedElementIds[element.id] = false;
});
} 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]) {
delete nextSelectedElementIds[hitElement.id];
nextSelectedElementIds[hitElement.id] = false;
}
} else {
// hitElement is neither a frame nor an element in a frame
@@ -4723,7 +4704,7 @@ class App extends React.Component<AppProps, AppState> {
framesInGroups.has(element.frameId)
) {
// deselect element and groups containing the element
delete nextSelectedElementIds[element.id];
nextSelectedElementIds[element.id] = false;
element.groupIds
.flatMap((gid) =>
getElementsInGroup(
@@ -4731,9 +4712,10 @@ class App extends React.Component<AppProps, AppState> {
gid,
),
)
.forEach((element) => {
delete nextSelectedElementIds[element.id];
});
.forEach(
(element) =>
(nextSelectedElementIds[element.id] = false),
);
}
});
}
@@ -4746,8 +4728,6 @@ class App extends React.Component<AppProps, AppState> {
showHyperlinkPopup: hitElement.link ? "info" : false,
},
this.scene.getNonDeletedElements(),
prevState,
this,
);
});
pointerDownState.hit.wasAddedToSelection = true;
@@ -4864,18 +4844,12 @@ class App extends React.Component<AppProps, AppState> {
frameId: topLayerFrame ? topLayerFrame.id : null,
});
this.setState((prevState) => {
const nextSelectedElementIds = {
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
};
delete nextSelectedElementIds[element.id];
return {
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
prevState,
),
};
});
[element.id]: false,
},
}));
const pressures = element.simulatePressure
? element.pressures
@@ -4971,13 +4945,10 @@ class App extends React.Component<AppProps, AppState> {
}
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[multiElement.id]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[multiElement.id]: true,
},
}));
// clicking outside commit zone → update reference for last committed
// point
@@ -5028,18 +4999,12 @@ class App extends React.Component<AppProps, AppState> {
locked: false,
frameId: topLayerFrame ? topLayerFrame.id : null,
});
this.setState((prevState) => {
const nextSelectedElementIds = {
this.setState((prevState) => ({
selectedElementIds: {
...prevState.selectedElementIds,
};
delete nextSelectedElementIds[element.id];
return {
selectedElementIds: makeNextSelectedElementIds(
nextSelectedElementIds,
prevState,
),
};
});
[element.id]: false,
},
}));
mutateElement(element, {
points: [...element.points, [0, 0]],
});
@@ -5175,7 +5140,7 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.offset === null) {
pointerDownState.drag.offset = tupleToCoors(
getDragOffsetXY(
this.scene.getSelectedElements(this.state),
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
pointerDownState.origin.x,
pointerDownState.origin.y,
),
@@ -5338,7 +5303,10 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit.hasHitCommonBoundingBoxOfSelectedElements) &&
!isSelectingPointsInLineEditor
) {
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
if (selectedElements.every((element) => element.locked)) {
return;
@@ -5409,21 +5377,16 @@ class App extends React.Component<AppProps, AppState> {
const groupIdMap = new Map();
const oldIdToDuplicatedId = new Map();
const hitElement = pointerDownState.hit.element;
const selectedElementIds = new Set(
this.scene
.getSelectedElements({
selectedElementIds: this.state.selectedElementIds,
includeBoundTextElement: true,
includeElementsInFrames: true,
})
.map((element) => element.id),
);
const elements = this.scene.getNonDeletedElements();
const elements = this.scene.getElementsIncludingDeleted();
const selectedElementIds: Array<ExcalidrawElement["id"]> =
getSelectedElements(elements, this.state, {
includeBoundTextElement: true,
includeElementsInFrames: true,
}).map((element) => element.id);
for (const element of elements) {
if (
selectedElementIds.has(element.id) ||
selectedElementIds.includes(element.id) ||
// case: the state.selectedElementIds might not have been
// updated yet by the time this mousemove event is fired
(element.id === hitElement?.id &&
@@ -5561,10 +5524,14 @@ class App extends React.Component<AppProps, AppState> {
},
},
this.scene.getNonDeletedElements(),
prevState,
this,
),
);
} else {
this.setState({
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
}
}
// box-select line editor points
@@ -5580,29 +5547,28 @@ class App extends React.Component<AppProps, AppState> {
elements,
draggingElement,
);
this.setState((prevState) => {
const nextSelectedElementIds = elementsWithinSelection.reduce(
(acc: Record<ExcalidrawElement["id"], true>, 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(
this.setState((prevState) =>
selectGroupsForSelectedElements(
{
...prevState,
selectedElementIds: nextSelectedElementIds,
selectedElementIds: {
...prevState.selectedElementIds,
...elementsWithinSelection.reduce(
(acc: Record<ExcalidrawElement["id"], true>, 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),
},
showHyperlinkPopup:
elementsWithinSelection.length === 1 &&
elementsWithinSelection[0].link
@@ -5619,10 +5585,8 @@ class App extends React.Component<AppProps, AppState> {
: null,
},
this.scene.getNonDeletedElements(),
prevState,
this,
);
});
),
);
}
}
});
@@ -5720,7 +5684,10 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.hit?.element?.id !==
this.state.selectedLinearElement.elementId
) {
const selectedELements = this.scene.getSelectedElements(this.state);
const selectedELements = getSelectedElements(
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
if (selectedELements.length > 1) {
this.setState({ selectedLinearElement: null });
@@ -5813,12 +5780,7 @@ class App extends React.Component<AppProps, AppState> {
try {
this.initializeImageDimensions(imageElement);
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
},
{ selectedElementIds: { [imageElement.id]: true } },
() => {
this.actionManager.executeAction(actionFinalize);
},
@@ -5882,13 +5844,10 @@ class App extends React.Component<AppProps, AppState> {
activeTool: updateActiveTool(this.state, {
type: "selection",
}),
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
selectedLinearElement: new LinearElementEditor(
draggingElement,
this.scene,
@@ -5962,7 +5921,10 @@ class App extends React.Component<AppProps, AppState> {
const topLayerFrame =
this.getTopLayerFrameAtSceneCoords(sceneCoords);
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
let nextElements = this.scene.getElementsIncludingDeleted();
const updateGroupIdsAfterEditingGroup = (
@@ -6041,7 +6003,6 @@ class App extends React.Component<AppProps, AppState> {
nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
this,
);
this.scene.replaceAllElements(nextElements);
@@ -6086,14 +6047,14 @@ class App extends React.Component<AppProps, AppState> {
let nextElements = updateFrameMembershipOfSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
this,
);
const selectedFrames = this.scene
.getSelectedElements(this.state)
.filter(
(element) => element.type === "frame",
) as ExcalidrawFrameElement[];
const selectedFrames = getSelectedElements(
this.scene.getElementsIncludingDeleted(),
this.state,
).filter(
(element) => element.type === "frame",
) as ExcalidrawFrameElement[];
for (const frame of selectedFrames) {
nextElements = replaceAllElementsInFrame(
@@ -6118,7 +6079,10 @@ class App extends React.Component<AppProps, AppState> {
this.state.selectedLinearElement?.elementId !== hitElement?.id &&
isLinearElement(hitElement)
) {
const selectedELements = this.scene.getSelectedElements(this.state);
const selectedELements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
// set selectedLinearElement when no other element selected except
// the one we've hit
if (selectedELements.length === 1) {
@@ -6177,37 +6141,31 @@ class App extends React.Component<AppProps, AppState> {
if (childEvent.shiftKey && !this.state.editingLinearElement) {
if (this.state.selectedElementIds[hitElement.id]) {
if (isSelectedViaGroup(this.state, hitElement)) {
this.setState((_prevState) => {
const nextSelectedElementIds = {
..._prevState.selectedElementIds,
};
// 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,
// 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) => ({
selectedGroupIds: {
..._prevState.selectedElementIds,
...hitElement.groupIds
.map((gId) => ({ [gId]: false }))
.reduce((prev, acc) => ({ ...prev, ...acc }), {}),
},
selectedElementIds: {
..._prevState.selectedElementIds,
...idsOfSelectedElementsThatAreInGroups,
},
}));
// if not gragging a linear element point (outside editor)
} else if (!this.state.selectedLinearElement?.isDragging) {
// remove element from selection while
@@ -6216,11 +6174,11 @@ class App extends React.Component<AppProps, AppState> {
this.setState((prevState) => {
const newSelectedElementIds = {
...prevState.selectedElementIds,
[hitElement!.id]: false,
};
delete newSelectedElementIds[hitElement!.id];
const newSelectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
{ selectedElementIds: newSelectedElementIds },
{ ...prevState, selectedElementIds: newSelectedElementIds },
);
return selectGroupsForSelectedElements(
@@ -6238,8 +6196,6 @@ class App extends React.Component<AppProps, AppState> {
: prevState.selectedLinearElement,
},
this.scene.getNonDeletedElements(),
prevState,
this,
);
});
}
@@ -6250,23 +6206,21 @@ class App extends React.Component<AppProps, AppState> {
// when hitElement is part of a selected frame, deselect the frame
// to avoid frame and containing elements selected simultaneously
this.setState((prevState) => {
const nextSelectedElementIds: {
[id: string]: true;
} = {
const nextSelectedElementIds = {
...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) => {
delete nextSelectedElementIds[element.id];
});
.forEach(
(element) => (nextSelectedElementIds[element.id] = false),
);
return selectGroupsForSelectedElements(
{
@@ -6275,20 +6229,15 @@ class App extends React.Component<AppProps, AppState> {
showHyperlinkPopup: hitElement.link ? "info" : false,
},
this.scene.getNonDeletedElements(),
prevState,
this,
);
});
} else {
// add element to selection while keeping prev elements selected
this.setState((_prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
..._prevState.selectedElementIds,
[hitElement!.id]: true,
},
_prevState,
),
selectedElementIds: {
..._prevState.selectedElementIds,
[hitElement!.id]: true,
},
}));
}
} else {
@@ -6306,8 +6255,6 @@ class App extends React.Component<AppProps, AppState> {
: prevState.selectedLinearElement,
},
this.scene.getNonDeletedElements(),
prevState,
this,
),
}));
}
@@ -6332,7 +6279,7 @@ class App extends React.Component<AppProps, AppState> {
} else {
// Deselect selected elements
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
selectedGroupIds: {},
editingGroupId: null,
});
@@ -6343,17 +6290,13 @@ class App extends React.Component<AppProps, AppState> {
if (
!activeTool.locked &&
activeTool.type !== "freedraw" &&
draggingElement &&
draggingElement.type !== "selection"
draggingElement
) {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds(
{
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
prevState,
),
selectedElementIds: {
...prevState.selectedElementIds,
[draggingElement.id]: true,
},
}));
}
@@ -6367,7 +6310,9 @@ class App extends React.Component<AppProps, AppState> {
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
(isBindingEnabled(this.state)
? bindOrUnbindSelectedElements
: unbindLinearElements)(this.scene.getSelectedElements(this.state));
: unbindLinearElements)(
getSelectedElements(this.scene.getNonDeletedElements(), this.state),
);
}
if (!activeTool.locked && activeTool.type !== "freedraw") {
@@ -6665,10 +6610,7 @@ class App extends React.Component<AppProps, AppState> {
this.initializeImageDimensions(imageElement);
this.setState(
{
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
selectedElementIds: { [imageElement.id]: true },
},
() => {
this.actionManager.executeAction(actionFinalize);
@@ -6895,7 +6837,7 @@ class App extends React.Component<AppProps, AppState> {
private clearSelection(hitElement: ExcalidrawElement | null): void {
this.setState((prevState) => ({
selectedElementIds: makeNextSelectedElementIds({}, prevState),
selectedElementIds: {},
selectedGroupIds: {},
// Continue editing the same group if the user selected a different
// element from it
@@ -6907,7 +6849,7 @@ class App extends React.Component<AppProps, AppState> {
: null,
}));
this.setState({
selectedElementIds: makeNextSelectedElementIds({}, this.state),
selectedElementIds: {},
previousSelectedElementIds: this.state.selectedElementIds,
});
}
@@ -6976,12 +6918,7 @@ class App extends React.Component<AppProps, AppState> {
const imageElement = this.createImageElement({ sceneX, sceneY });
this.insertImageElement(imageElement, file);
this.initializeImageDimensions(imageElement);
this.setState({
selectedElementIds: makeNextSelectedElementIds(
{ [imageElement.id]: true },
this.state,
),
});
this.setState({ selectedElementIds: { [imageElement.id]: true } });
return;
}
@@ -7074,7 +7011,10 @@ class App extends React.Component<AppProps, AppState> {
includeLockedElements: true,
});
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
const isHittignCommonBoundBox =
this.isHittingCommonBoundingBoxOfSelectedElements(
{ x, y },
@@ -7103,8 +7043,6 @@ class App extends React.Component<AppProps, AppState> {
: null,
},
this.scene.getNonDeletedElements(),
this.state,
this,
)
: this.state),
showHyperlinkPopup: false,
@@ -7192,7 +7130,10 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState: PointerDownState,
event: MouseEvent | KeyboardEvent,
): boolean => {
const selectedElements = this.scene.getSelectedElements(this.state);
const selectedElements = getSelectedElements(
this.scene.getNonDeletedElements(),
this.state,
);
const selectedFrames = selectedElements.filter(
(element) => element.type === "frame",
) as ExcalidrawFrameElement[];
+1 -3
View File
@@ -82,9 +82,7 @@ export const ContextMenu = React.memo(
let label = "";
if (item.contextItemLabel) {
if (typeof item.contextItemLabel === "function") {
label = t(
item.contextItemLabel(elements, appState, actionManager.app),
);
label = t(item.contextItemLabel(elements, appState));
} else {
label = t(item.contextItemLabel);
}
+13 -6
View File
@@ -1,5 +1,7 @@
import { t } from "../i18n";
import { AppClassProperties, Device, UIAppState } from "../types";
import { NonDeletedExcalidrawElement } from "../element/types";
import { getSelectedElements } from "../scene";
import { Device, UIAppState } from "../types";
import {
isImageElement,
isLinearElement,
@@ -13,12 +15,17 @@ import "./HintViewer.scss";
interface HintViewerProps {
appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[];
isMobile: boolean;
device: Device;
app: AppClassProperties;
}
const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
const getHints = ({
appState,
elements,
isMobile,
device,
}: HintViewerProps) => {
const { activeTool, isResizing, isRotating, lastPointerDownWith } = appState;
const multiMode = appState.multiElement !== null;
@@ -48,7 +55,7 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
return t("hints.placeImage");
}
const selectedElements = app.scene.getSelectedElements(appState);
const selectedElements = getSelectedElements(elements, appState);
if (
isResizing &&
@@ -108,15 +115,15 @@ const getHints = ({ appState, isMobile, device, app }: HintViewerProps) => {
export const HintViewer = ({
appState,
elements,
isMobile,
device,
app,
}: HintViewerProps) => {
let hint = getHints({
appState,
elements,
isMobile,
device,
app,
});
if (!hint) {
return null;
+1 -4
View File
@@ -72,7 +72,6 @@ interface LayerUIProps {
onExportImage: AppClassProperties["onExportImage"];
renderWelcomeScreen: boolean;
children?: React.ReactNode;
app: AppClassProperties;
}
const DefaultMainMenu: React.FC<{
@@ -128,7 +127,6 @@ const LayerUI = ({
onExportImage,
renderWelcomeScreen,
children,
app,
}: LayerUIProps) => {
const device = useDevice();
const tunnels = useInitializeTunnels();
@@ -242,9 +240,9 @@ const LayerUI = ({
>
<HintViewer
appState={appState}
elements={elements}
isMobile={device.isMobile}
device={device}
app={app}
/>
{heading}
<Stack.Row gap={1}>
@@ -403,7 +401,6 @@ const LayerUI = ({
)}
{device.isMobile && (
<MobileMenu
app={app}
appState={appState}
elements={elements}
actionManager={actionManager}
+2 -10
View File
@@ -1,11 +1,5 @@
import React from "react";
import {
AppClassProperties,
AppState,
Device,
ExcalidrawProps,
UIAppState,
} from "../types";
import { AppState, Device, ExcalidrawProps, UIAppState } from "../types";
import { ActionManager } from "../actions/manager";
import { t } from "../i18n";
import Stack from "./Stack";
@@ -47,7 +41,6 @@ type MobileMenuProps = {
renderSidebars: () => JSX.Element | null;
device: Device;
renderWelcomeScreen: boolean;
app: AppClassProperties;
};
export const MobileMenu = ({
@@ -65,7 +58,6 @@ export const MobileMenu = ({
renderSidebars,
device,
renderWelcomeScreen,
app,
}: MobileMenuProps) => {
const {
WelcomeScreenCenterTunnel,
@@ -127,9 +119,9 @@ export const MobileMenu = ({
</Section>
<HintViewer
appState={appState}
elements={elements}
isMobile={true}
device={device}
app={app}
/>
</FixedSideContainer>
);
+11 -4
View File
@@ -1620,11 +1620,18 @@ export const alertTriangleIcon = createIcon(
export const eyeDropperIcon = createIcon(
<g strokeWidth={1.25}>
<path stroke="none" d="M0 0h24v24H0z" fill="none"></path>
<path d="M11 7l6 6"></path>
<path d="M4 16l11.7 -11.7a1 1 0 0 1 1.4 0l2.6 2.6a1 1 0 0 1 0 1.4l-11.7 11.7h-4v-4z"></path>
<path d="m9.168 5.833 5 5M9.999 6.667l-6.667 6.666v3.334h3.333L13.332 10M9.999 6.667l3.083-3.084a.833.833 0 0 1 1.167 0l2.166 2.167a.833.833 0 0 1 0 1.167L13.332 10M9.999 6.667 13.332 10" />
<rect
x="13.684"
y="2.976"
width="4.751"
height="5.106"
rx="1"
transform="rotate(45 13.684 2.976)"
fill="currentColor"
/>
</g>,
tablerIconProps,
modifiedTablerIconProps,
);
export const extraToolsIcon = createIcon(
+4 -29
View File
@@ -27,7 +27,6 @@ import { LinearElementEditor } from "./linearElementEditor";
import { arrayToMap, tupleToCoors } from "../utils";
import { KEYS } from "../keys";
import { getBoundTextElement, handleBindTextResize } from "./textElement";
import { getContainingFrame, isPointInFrame } from "../frame";
export type SuggestedBinding =
| NonDeleted<ExcalidrawBindableElement>
@@ -275,18 +274,6 @@ export const getHoveredElementForBinding = (
isBindableElement(element, false) &&
bindingBorderTest(element, pointerCoords),
);
if (hoveredElement) {
const frame = getContainingFrame(hoveredElement);
if (frame) {
if (isPointInFrame(pointerCoords, frame)) {
return hoveredElement as NonDeleted<ExcalidrawBindableElement>;
}
return null;
}
}
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
};
@@ -512,22 +499,10 @@ const getElligibleElementsForBindingElement = (
return [
getElligibleElementForBindingElement(linearElement, "start"),
getElligibleElementForBindingElement(linearElement, "end"),
].filter((element): element is NonDeleted<ExcalidrawBindableElement> => {
if (element != null) {
const frame = getContainingFrame(element);
return frame
? isPointInFrame(
getLinearElementEdgeCoors(linearElement, "start"),
frame,
) ||
isPointInFrame(
getLinearElementEdgeCoors(linearElement, "end"),
frame,
)
: true;
}
return false;
});
].filter(
(element): element is NonDeleted<ExcalidrawBindableElement> =>
element != null,
);
};
const getElligibleElementForBindingElement = (
-135
View File
@@ -1,135 +0,0 @@
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<string, { t: number; times: number[] }> =
{};
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") =>
<T extends any[], R>(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 = <T extends any[], R>(
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;
+2 -17
View File
@@ -1,7 +1,6 @@
import {
getCommonBounds,
getElementAbsoluteCoords,
getElementBounds,
isTextElement,
} from "./element";
import {
@@ -17,7 +16,7 @@ import {
} from "./element/textElement";
import { arrayToMap, findIndex } from "./utils";
import { mutateElement } from "./element/mutateElement";
import { AppClassProperties, AppState } from "./types";
import { AppState } from "./types";
import { getElementsWithinSelection, getSelectedElements } from "./scene";
import { isFrameElement } from "./element";
import { moveOneRight } from "./zindex";
@@ -300,15 +299,6 @@ export const groupsAreCompletelyOutOfFrame = (
);
};
export const isPointInFrame = (
{ x, y }: { x: number; y: number },
frame: ExcalidrawFrameElement,
) => {
const [x1, y1, x2, y2] = getElementBounds(frame);
return x >= x1 && x <= x2 && y >= y1 && y <= y2;
};
// --------------------------- Frame Utils ------------------------------------
/**
@@ -581,13 +571,8 @@ export const replaceAllElementsInFrame = (
export const updateFrameMembershipOfSelectedElements = (
allElements: ExcalidrawElementsIncludingDeleted,
appState: AppState,
app: AppClassProperties,
) => {
const selectedElements = app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements: allElements,
});
const selectedElements = getSelectedElements(allElements, appState);
const elementsToFilter = new Set<ExcalidrawElement>(selectedElements);
if (appState.editingGroupId) {
+5 -35
View File
@@ -1,13 +1,7 @@
import {
GroupId,
ExcalidrawElement,
NonDeleted,
NonDeletedExcalidrawElement,
} from "./element/types";
import { AppClassProperties, AppState } from "./types";
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,
@@ -72,33 +66,14 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
*/
export const selectGroupsForSelectedElements = (
appState: AppState,
elements: readonly NonDeletedExcalidrawElement[],
prevAppState: AppState,
/**
* supply null in cases where you don't have access to App instance and
* you don't care about optimizing selectElements retrieval
*/
app: AppClassProperties | null,
elements: readonly NonDeleted<ExcalidrawElement>[],
): AppState => {
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
const selectedElements = app
? app.scene.getSelectedElements({
selectedElementIds: appState.selectedElementIds,
// supplying elements explicitly in case we're passed non-state elements
elements,
})
: getSelectedElements(elements, appState);
const selectedElements = getSelectedElements(elements, appState);
if (!selectedElements.length) {
return {
...nextAppState,
editingGroupId: null,
selectedElementIds: makeNextSelectedElementIds(
nextAppState.selectedElementIds,
prevAppState,
),
};
return { ...nextAppState, editingGroupId: null };
}
for (const selectedElement of selectedElements) {
@@ -116,11 +91,6 @@ export const selectGroupsForSelectedElements = (
}
}
nextAppState.selectedElementIds = makeNextSelectedElementIds(
nextAppState.selectedElementIds,
prevAppState,
);
return nextAppState;
};
+1 -5
View File
@@ -931,11 +931,7 @@ export const renderElement = (
break;
}
case "frame": {
if (
!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.outline
) {
if (!renderConfig.isExporting && appState.shouldRenderFrames) {
context.save();
context.translate(
element.x + renderConfig.scrollX,
+1 -3
View File
@@ -470,9 +470,7 @@ export const _renderScene = ({
if (
frameId &&
((renderConfig.isExporting && isOnlyExportingSingleFrame(elements)) ||
(!renderConfig.isExporting &&
appState.frameRendering.enabled &&
appState.frameRendering.clip))
(!renderConfig.isExporting && appState.shouldRenderFrames))
) {
context.save();
-93
View File
@@ -11,9 +11,6 @@ import {
} from "../element";
import { LinearElementEditor } from "../element/linearElementEditor";
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 ElementKey = ExcalidrawElement | ElementIdKey;
@@ -21,31 +18,6 @@ type ElementKey = ExcalidrawElement | ElementIdKey;
type SceneStateCallback = () => 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
// in our codebase
export type ExcalidrawElementsIncludingDeleted = readonly ExcalidrawElement[];
@@ -96,15 +68,6 @@ class Scene {
private nonDeletedFrames: readonly NonDeleted<ExcalidrawFrameElement>[] = [];
private frames: readonly ExcalidrawFrameElement[] = [];
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() {
return this.elements;
@@ -118,52 +81,6 @@ class Scene {
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>[] {
return this.nonDeletedFrames;
}
@@ -251,21 +168,11 @@ class Scene {
}
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) => {
if (scene === this) {
Scene.sceneMapById.delete(elementKey);
}
});
// done not for memory leaks, but to guard against possible late fires
// (I guess?)
this.callbacks.clear();
-35
View File
@@ -1,35 +0,0 @@
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,
);
});
});
+5 -51
View File
@@ -10,7 +10,6 @@ import {
getContainingFrame,
getFrameElements,
} from "../frame";
import { isShallowEqual } from "../utils";
/**
* Frames and their containing elements are not to be selected at the same time.
@@ -89,41 +88,11 @@ export const getElementsWithinSelection = (
return elementsInSelection;
};
// 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<AppState, "selectedElementIds">,
): 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;
})();
export const isSomeElementSelected = (
elements: readonly NonDeletedExcalidrawElement[],
appState: Pick<AppState, "selectedElementIds">,
): boolean =>
elements.some((element) => appState.selectedElementIds[element.id]);
/**
* Returns common attribute (picked by `getAttribute` callback) of selected
@@ -192,18 +161,3 @@ 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<AppState, "selectedElementIds">,
) => {
if (isShallowEqual(prevState.selectedElementIds, nextSelectedElementIds)) {
return prevState.selectedElementIds;
}
return nextSelectedElementIds;
};
+66 -138
View File
@@ -314,12 +314,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -359,6 +353,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -506,12 +501,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -548,6 +537,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -704,12 +694,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -746,6 +730,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1076,12 +1061,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1118,6 +1097,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1448,12 +1428,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1490,6 +1464,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1527,14 +1502,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 449462985,
"versionNonce": 453191,
"width": 20,
"x": -10,
"y": 0,
@@ -1586,14 +1561,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 449462985,
"versionNonce": 453191,
"width": 20,
"x": -10,
"y": 0,
@@ -1646,12 +1621,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1686,6 +1655,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -1881,12 +1851,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -1923,6 +1887,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -2181,12 +2146,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -2220,6 +2179,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {
@@ -2228,6 +2188,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -2452,6 +2413,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedGroupIds": Object {
"id3": true,
@@ -2569,12 +2531,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -2611,6 +2567,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -3447,12 +3404,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -3489,6 +3440,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -3819,12 +3771,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -3861,6 +3807,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -4191,12 +4138,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -4230,12 +4171,14 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -4271,14 +4214,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1014066025,
"versionNonce": 238820263,
"width": 20,
"x": -10,
"y": 0,
@@ -4303,14 +4246,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 238820263,
"versionNonce": 400692809,
"width": 20,
"x": 20,
"y": 30,
@@ -4362,14 +4305,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 449462985,
"versionNonce": 453191,
"width": 20,
"x": -10,
"y": 0,
@@ -4405,14 +4348,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 449462985,
"versionNonce": 453191,
"width": 20,
"x": -10,
"y": 0,
@@ -4434,14 +4377,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 401146281,
"versionNonce": 2019559783,
"width": 20,
"x": 20,
"y": 30,
@@ -4456,6 +4399,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedGroupIds": Object {
"id3": true,
@@ -4482,14 +4426,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1150084233,
"versionNonce": 1116226695,
"width": 20,
"x": -10,
"y": 0,
@@ -4513,14 +4457,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1116226695,
"versionNonce": 1014066025,
"width": 20,
"x": 20,
"y": 30,
@@ -4535,6 +4479,7 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
},
"selectedGroupIds": Object {},
"viewBackgroundColor": "#ffffff",
@@ -4557,14 +4502,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 1014066025,
"versionNonce": 238820263,
"width": 20,
"x": -10,
"y": 0,
@@ -4586,14 +4531,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 4,
"versionNonce": 238820263,
"versionNonce": 400692809,
"width": 20,
"x": 20,
"y": 30,
@@ -4922,12 +4867,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -4953,6 +4892,7 @@ Object {
"pendingImageElementId": null,
"previousSelectedElementIds": Object {
"id0": true,
"id2": true,
},
"resizingElement": null,
"scrollX": 0,
@@ -4961,12 +4901,15 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {},
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -5501,12 +5444,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -5532,6 +5469,7 @@ Object {
"pendingImageElementId": null,
"previousSelectedElementIds": Object {
"id0": true,
"id2": true,
},
"resizingElement": null,
"scrollX": 0,
@@ -5540,6 +5478,8 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"selectedElementsAreBeingDragged": false,
"selectedGroupIds": Object {
@@ -5548,6 +5488,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -5585,14 +5526,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1116226695,
"versionNonce": 1014066025,
"width": 10,
"x": -10,
"y": 0,
@@ -5619,14 +5560,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1014066025,
"versionNonce": 238820263,
"width": 10,
"x": 10,
"y": 0,
@@ -5678,14 +5619,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 449462985,
"versionNonce": 453191,
"width": 10,
"x": -10,
"y": 0,
@@ -5721,14 +5662,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 449462985,
"versionNonce": 453191,
"width": 10,
"x": -10,
"y": 0,
@@ -5750,14 +5691,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 2,
"versionNonce": 401146281,
"versionNonce": 2019559783,
"width": 10,
"x": 10,
"y": 0,
@@ -5772,6 +5713,8 @@ Object {
"selectedElementIds": Object {
"id0": true,
"id1": true,
"id2": true,
"id3": true,
},
"selectedGroupIds": Object {
"id4": true,
@@ -5798,14 +5741,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 1278240551,
"seed": 449462985,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1116226695,
"versionNonce": 1014066025,
"width": 10,
"x": -10,
"y": 0,
@@ -5829,14 +5772,14 @@ Object {
"roundness": Object {
"type": 3,
},
"seed": 453191,
"seed": 401146281,
"strokeColor": "#1e1e1e",
"strokeStyle": "solid",
"strokeWidth": 1,
"type": "rectangle",
"updated": 1,
"version": 3,
"versionNonce": 1014066025,
"versionNonce": 238820263,
"width": 10,
"x": 10,
"y": 0,
@@ -6004,12 +5947,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -6044,6 +5981,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -6399,12 +6337,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -6441,6 +6373,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
@@ -6772,12 +6705,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"height": 100,
@@ -6814,6 +6741,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": true,
File diff suppressed because it is too large Load Diff
+8 -32
View File
@@ -430,10 +430,7 @@ describe("arrow", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -449,10 +446,7 @@ describe("arrow", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("arrow");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -622,10 +616,7 @@ describe("line", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -641,10 +632,7 @@ describe("line", () => {
const expectedAngle = (7 * Math.PI) / 4;
const line = createLinearElementWithCurveInsideMinMaxPoints("line");
h.app.scene.replaceAllElements([line]);
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[line.id]: true,
};
h.app.state.selectedElementIds[line.id] = true;
mutateElement(line, {
angle: originalAngle,
});
@@ -671,20 +659,14 @@ describe("freedraw", () => {
it("flips an unrotated drawing horizontally correctly", async () => {
const draw = createAndReturnOneDraw();
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
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 = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkVerticalFlip();
});
@@ -694,10 +676,7 @@ describe("freedraw", () => {
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkRotatedHorizontalFlip(expectedAngle);
});
@@ -708,10 +687,7 @@ describe("freedraw", () => {
const draw = createAndReturnOneDraw(originalAngle);
// select draw, since not done automatically
h.state.selectedElementIds = {
...h.state.selectedElementIds,
[draw.id]: true,
};
h.state.selectedElementIds[draw.id] = true;
await checkRotatedVerticalFlip(expectedAngle);
});
@@ -39,12 +39,6 @@ Object {
"exportScale": 1,
"exportWithDarkMode": false,
"fileHandle": null,
"frameRendering": Object {
"clip": true,
"enabled": true,
"name": true,
"outline": true,
},
"frameToHighlight": null,
"gridSize": null,
"isBindingEnabled": true,
@@ -76,6 +70,7 @@ Object {
"selectedLinearElement": null,
"selectionElement": null,
"shouldCacheIgnoreZoom": false,
"shouldRenderFrames": true,
"showHyperlinkPopup": false,
"showStats": false,
"showWelcomeScreen": false,
-2
View File
@@ -89,8 +89,6 @@ const populateElements = (
...selectGroupsForSelectedElements(
{ ...h.state, ...appState, selectedElementIds },
h.elements,
h.state,
null,
),
...appState,
selectedElementIds,
+4 -9
View File
@@ -115,12 +115,7 @@ export type AppState = {
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
suggestedBindings: SuggestedBinding[];
frameToHighlight: NonDeleted<ExcalidrawFrameElement> | null;
frameRendering: {
enabled: boolean;
name: boolean;
outline: boolean;
clip: boolean;
};
shouldRenderFrames: boolean;
editingFrame: string | null;
elementsToHighlight: NonDeleted<ExcalidrawElement>[] | null;
// element being edited, but not necessarily added to elements array yet
@@ -186,8 +181,8 @@ export type AppState = {
defaultSidebarDockedPreference: boolean;
lastPointerDownWith: PointerType;
selectedElementIds: Readonly<{ [id: string]: true }>;
previousSelectedElementIds: { [id: string]: true };
selectedElementIds: { [id: string]: boolean };
previousSelectedElementIds: { [id: string]: boolean };
selectedElementsAreBeingDragged: boolean;
shouldCacheIgnoreZoom: boolean;
toast: { message: string; closable?: boolean; duration?: number } | null;
@@ -548,7 +543,7 @@ export type ExcalidrawImperativeAPI = {
* the frames are still interactive in edit mode. As such, this API should be
* used in conjunction with view mode (props.viewModeEnabled).
*/
updateFrameRendering: InstanceType<typeof App>["updateFrameRendering"];
toggleFrameRendering: InstanceType<typeof App>["toggleFrameRendering"];
};
export type Device = Readonly<{
-3
View File
@@ -47,6 +47,3 @@ export type ForwardRef<T, P = any> = Parameters<
export type ExtractSetType<T extends Set<any>> = T extends Set<infer U>
? U
: never;
export type SameType<T, U> = T extends U ? (U extends T ? true : false) : false;
export type Assert<T extends true> = T;