Compare commits

...

9 Commits

Author SHA1 Message Date
Aakansha Doshi 8ec7801a31 fix: don't import from element/index to prevent mermaid bundling 2024-02-20 21:05:22 +05:30
Aakansha Doshi 2e719ff671 fix: decouple pure functions from hyperlink to prevent mermaid bundling (#7710)
* move hyperlink code into its folder

* move pure js functions to hyperlink/helpers and move actionLink to actions

* fix tests

* fix
2024-02-20 20:59:01 +05:30
Aakansha Doshi 79d9dc2f8f fix: make bounds independent of scene (#7679)
* fix: make bounds independent of scene

* pass only elements to getCommonBounds

* lint

* pass elementsMap to getVisibleAndNonSelectedElements
2024-02-19 19:39:14 +05:30
Aakansha Doshi 9013c84524 fix: make LinearElementEditor independent of scene (#7670)
* fix: make LinearElementEditor independent of scene

* more fixes

* pass elements and elementsMap to maybeBindBindableElement,getHoveredElementForBinding,bindingBorderTest,getElligibleElementsForBindableElementAndWhere,isLinearElementEligibleForNewBindingByBindable

* replace `ElementsMap` with `NonDeletedSceneElementsMap` & remove unused params

* fix lint

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-02-19 11:49:01 +05:30
Aakansha Doshi 47f87f4ecb fix: remove scene from getElementAbsoluteCoords and dependent functions and use elementsMap (#7663)
* fix: remove scene from getElementAbsoluteCoords and dependent functions and use elementsMap

* lint

* fix

* use non deleted elements where possible

* use non deleted elements map in actions

* pass elementsMap instead of array to elementOverlapsWithFrame

* lint

* fix

* pass elementsMap to getElementsCorners

* pass elementsMap to getEligibleElementsForBinding

* pass elementsMap in bindOrUnbindSelectedElements and unbindLinearElements

* pass elementsMap in elementsAreInFrameBounds,elementOverlapsWithFrame,isCursorInFrame,getElementsInResizingFrame

* pass elementsMap in getElementsWithinSelection, getElementsCompletelyInFrame, isElementContainingFrame, getElementsInNewFrame

* pass elementsMap to getElementWithTransformHandleType

* pass elementsMap to getVisibleGaps, getMaximumGroups,getReferenceSnapPoints,snapDraggedElements

* lint

* pass elementsMap to bindTextToShapeAfterDuplication,bindLinearElementToElement,getTextBindableContainerAtPosition

* revert changes for bindTextToShapeAfterDuplication
2024-02-16 11:35:01 +05:30
Aakansha Doshi 73bf50e8a8 fix: remove t from getDefaultAppState and allow name to be nullable (#7666)
* fix: remove t and allow name to be nullable

* pass name as required prop

* remove Unnamed

* pass name to excalidrawPlus as well for better type safe

* render once we have excalidrawAPI to avoid defaulting

* rename `getAppName` -> `getName` (temporary)

* stop preventing editing filenames when `props.name` supplied

* keep `name` as optional param for export functions

* keep `appState.name` on `props.name` state separate

* fix lint

* assertive first

* fix lint

* Add TODO

---------

Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com>
2024-02-15 11:11:18 +05:30
Aakansha Doshi 48c3465b19 docs: release patch v0.17.3 (#7673)
* docs: release patch v0.17.3

* update cl
2024-02-09 19:29:50 +05:30
David Luzar adc4c9f484 fix: prevent panning to trigger history on macos chrome (#7671) 2024-02-08 19:50:50 +01:00
YuBin, Hsu def1df2c68 fix: keep customData when converting to ExcalidrawElement (#7656)
* feat: keep customData when converting to ExcalidrawElement (#7654)

* docs: add changelog for keeping customData when converting to ExcalidrawElement
2024-02-08 17:23:10 +05:30
74 changed files with 1807 additions and 662 deletions
+25 -21
View File
@@ -709,27 +709,30 @@ const ExcalidrawWrapper = () => {
toggleTheme: true, toggleTheme: true,
export: { export: {
onExportToBackend, onExportToBackend,
renderCustomUI: (elements, appState, files) => { renderCustomUI: excalidrawAPI
return ( ? (elements, appState, files) => {
<ExportToExcalidrawPlus return (
elements={elements} <ExportToExcalidrawPlus
appState={appState} elements={elements}
files={files} appState={appState}
onError={(error) => { files={files}
excalidrawAPI?.updateScene({ name={excalidrawAPI.getName()}
appState: { onError={(error) => {
errorMessage: error.message, excalidrawAPI?.updateScene({
}, appState: {
}); errorMessage: error.message,
}} },
onSuccess={() => { });
excalidrawAPI?.updateScene({ }}
appState: { openDialog: null }, onSuccess={() => {
}); excalidrawAPI.updateScene({
}} appState: { openDialog: null },
/> });
); }}
}, />
);
}
: undefined,
}, },
}, },
}} }}
@@ -775,6 +778,7 @@ const ExcalidrawWrapper = () => {
excalidrawAPI.getSceneElements(), excalidrawAPI.getSceneElements(),
excalidrawAPI.getAppState(), excalidrawAPI.getAppState(),
excalidrawAPI.getFiles(), excalidrawAPI.getFiles(),
excalidrawAPI.getName(),
); );
}} }}
> >
@@ -30,6 +30,7 @@ export const exportToExcalidrawPlus = async (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: Partial<AppState>, appState: Partial<AppState>,
files: BinaryFiles, files: BinaryFiles,
name: string,
) => { ) => {
const firebase = await loadFirebaseStorage(); const firebase = await loadFirebaseStorage();
@@ -53,7 +54,7 @@ export const exportToExcalidrawPlus = async (
.ref(`/migrations/scenes/${id}`) .ref(`/migrations/scenes/${id}`)
.put(blob, { .put(blob, {
customMetadata: { customMetadata: {
data: JSON.stringify({ version: 2, name: appState.name }), data: JSON.stringify({ version: 2, name }),
created: Date.now().toString(), created: Date.now().toString(),
}, },
}); });
@@ -89,9 +90,10 @@ export const ExportToExcalidrawPlus: React.FC<{
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
appState: Partial<AppState>; appState: Partial<AppState>;
files: BinaryFiles; files: BinaryFiles;
name: string;
onError: (error: Error) => void; onError: (error: Error) => void;
onSuccess: () => void; onSuccess: () => void;
}> = ({ elements, appState, files, onError, onSuccess }) => { }> = ({ elements, appState, files, name, onError, onSuccess }) => {
const { t } = useI18n(); const { t } = useI18n();
return ( return (
<Card color="primary"> <Card color="primary">
@@ -117,7 +119,7 @@ export const ExportToExcalidrawPlus: React.FC<{
onClick={async () => { onClick={async () => {
try { try {
trackEvent("export", "eplus", `ui (${getFrame()})`); trackEvent("export", "eplus", `ui (${getFrame()})`);
await exportToExcalidrawPlus(elements, appState, files); await exportToExcalidrawPlus(elements, appState, files, name);
onSuccess(); onSuccess();
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
+11 -1
View File
@@ -19,6 +19,10 @@ Please add the latest change on the top under the correct section.
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450) - Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
### Fixes
- Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656)
### Breaking Changes ### Breaking Changes
- `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539) - `ExcalidrawEmbeddableElement.validated` was removed and moved to private editor state. This should largely not affect your apps unless you were reading from this attribute. We keep validating embeddable urls internally, and the public [`props.validateEmbeddable`](https://docs.excalidraw.com/docs/@excalidraw/excalidraw/api/props#validateembeddable) still applies. [#7539](https://github.com/excalidraw/excalidraw/pull/7539)
@@ -57,10 +61,12 @@ Please add the latest change on the top under the correct section.
- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336) - `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)
## 0.17.1 (2023-11-28) ## 0.17.3 (2024-02-09)
### Fixes ### Fixes
- Keep customData when converting to ExcalidrawElement. [#7656](https://github.com/excalidraw/excalidraw/pull/7656)
- Umd build for browser since it was breaking in v0.17.0 [#7349](https://github.com/excalidraw/excalidraw/pull/7349). Also make sure that when using `Vite`, the `process.env.IS_PREACT` is set as `"true"` (string) and not a boolean. - Umd build for browser since it was breaking in v0.17.0 [#7349](https://github.com/excalidraw/excalidraw/pull/7349). Also make sure that when using `Vite`, the `process.env.IS_PREACT` is set as `"true"` (string) and not a boolean.
``` ```
@@ -69,6 +75,10 @@ define: {
} }
``` ```
- Disable caching bounds for arrow labels [#7343](https://github.com/excalidraw/excalidraw/pull/7343)
- Bounds cached prematurely resulting in incorrectly rendered labels [#7339](https://github.com/excalidraw/excalidraw/pull/7339)
## Excalidraw Library ## Excalidraw Library
### Fixes ### Fixes
@@ -58,7 +58,11 @@ export const actionUnbindText = register({
element.id, element.id,
); );
resetOriginalContainerCache(element.id); resetOriginalContainerCache(element.id);
const { x, y } = computeBoundTextPosition(element, boundTextElement); const { x, y } = computeBoundTextPosition(
element,
boundTextElement,
elementsMap,
);
mutateElement(boundTextElement as ExcalidrawTextElement, { mutateElement(boundTextElement as ExcalidrawTextElement, {
containerId: null, containerId: null,
width, width,
@@ -145,7 +149,11 @@ export const actionBindText = register({
}), }),
}); });
const originalContainerHeight = container.height; const originalContainerHeight = container.height;
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
);
// overwritting the cache with original container height so // overwritting the cache with original container height so
// it can be restored when unbind // it can be restored when unbind
updateOriginalContainerCache(container.id, originalContainerHeight); updateOriginalContainerCache(container.id, originalContainerHeight);
@@ -286,7 +294,11 @@ export const actionWrapTextInContainer = register({
}, },
false, false,
); );
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(
textElement,
container,
app.scene.getNonDeletedElementsMap(),
);
updatedElements = pushContainerBelowText( updatedElements = pushContainerBelowText(
[...updatedElements, container], [...updatedElements, container],
@@ -107,7 +107,7 @@ export const actionCut = register({
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, event: ClipboardEvent | null, app) => { perform: (elements, appState, event: ClipboardEvent | null, app) => {
actionCopy.perform(elements, appState, event, app); actionCopy.perform(elements, appState, event, app);
return actionDeleteSelected.perform(elements, appState); return actionDeleteSelected.perform(elements, appState, null, app);
}, },
contextItemLabel: "labels.cut", contextItemLabel: "labels.cut",
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X, keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.X,
@@ -138,6 +138,7 @@ export const actionCopyAsSvg = register({
{ {
...appState, ...appState,
exportingFrame, exportingFrame,
name: app.getName(),
}, },
); );
return { return {
@@ -184,6 +185,7 @@ export const actionCopyAsPng = register({
await exportCanvas("clipboard", exportedElements, appState, app.files, { await exportCanvas("clipboard", exportedElements, appState, app.files, {
...appState, ...appState,
exportingFrame, exportingFrame,
name: app.getName(),
}); });
return { return {
appState: { appState: {
@@ -73,7 +73,7 @@ const handleGroupEditingState = (
export const actionDeleteSelected = register({ export const actionDeleteSelected = register({
name: "deleteSelectedElements", name: "deleteSelectedElements",
trackEvent: { category: "element", action: "delete" }, trackEvent: { category: "element", action: "delete" },
perform: (elements, appState) => { perform: (elements, appState, formData, app) => {
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { const {
elementId, elementId,
@@ -81,7 +81,8 @@ export const actionDeleteSelected = register({
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
} = appState.editingLinearElement; } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const elementsMap = app.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return false; return false;
} }
@@ -35,10 +35,14 @@ import {
export const actionDuplicateSelection = register({ export const actionDuplicateSelection = register({
name: "duplicateSelection", name: "duplicateSelection",
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState) => { perform: (elements, appState, formData, app) => {
const elementsMap = app.scene.getNonDeletedElementsMap();
// duplicate selected point(s) if editing a line // duplicate selected point(s) if editing a line
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const ret = LinearElementEditor.duplicateSelectedPoints(appState); const ret = LinearElementEditor.duplicateSelectedPoints(
appState,
elementsMap,
);
if (!ret) { if (!ret) {
return false; return false;
+10 -7
View File
@@ -26,14 +26,11 @@ export const actionChangeProjectName = register({
perform: (_elements, appState, value) => { perform: (_elements, appState, value) => {
return { appState: { ...appState, name: value }, commitToHistory: false }; return { appState: { ...appState, name: value }, commitToHistory: false };
}, },
PanelComponent: ({ appState, updateData, appProps, data }) => ( PanelComponent: ({ appState, updateData, appProps, data, app }) => (
<ProjectName <ProjectName
label={t("labels.fileTitle")} label={t("labels.fileTitle")}
value={appState.name || "Unnamed"} value={app.getName()}
onChange={(name: string) => updateData(name)} onChange={(name: string) => updateData(name)}
isNameEditable={
typeof appProps.name === "undefined" && !appState.viewModeEnabled
}
ignoreFocus={data?.ignoreFocus ?? false} ignoreFocus={data?.ignoreFocus ?? false}
/> />
), ),
@@ -144,8 +141,13 @@ export const actionSaveToActiveFile = register({
try { try {
const { fileHandle } = isImageFileHandle(appState.fileHandle) const { fileHandle } = isImageFileHandle(appState.fileHandle)
? await resaveAsImageWithScene(elements, appState, app.files) ? await resaveAsImageWithScene(
: await saveAsJSON(elements, appState, app.files); elements,
appState,
app.files,
app.getName(),
)
: await saveAsJSON(elements, appState, app.files, app.getName());
return { return {
commitToHistory: false, commitToHistory: false,
@@ -190,6 +192,7 @@ export const actionSaveFileToDisk = register({
fileHandle: null, fileHandle: null,
}, },
app.files, app.files,
app.getName(),
); );
return { return {
commitToHistory: false, commitToHistory: false,
@@ -1,6 +1,6 @@
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isInvisiblySmallElement } from "../element"; import { isInvisiblySmallElement } from "../element";
import { updateActiveTool } from "../utils"; import { arrayToMap, updateActiveTool } from "../utils";
import { ToolButton } from "../components/ToolButton"; import { ToolButton } from "../components/ToolButton";
import { done } from "../components/icons"; import { done } from "../components/icons";
import { t } from "../i18n"; import { t } from "../i18n";
@@ -26,10 +26,12 @@ export const actionFinalize = register({
_, _,
{ interactiveCanvas, focusContainer, scene }, { interactiveCanvas, focusContainer, scene },
) => { ) => {
const elementsMap = scene.getNonDeletedElementsMap();
if (appState.editingLinearElement) { if (appState.editingLinearElement) {
const { elementId, startBindingElement, endBindingElement } = const { elementId, startBindingElement, endBindingElement } =
appState.editingLinearElement; appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (element) { if (element) {
if (isBindingElement(element)) { if (isBindingElement(element)) {
@@ -37,6 +39,7 @@ export const actionFinalize = register({
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
); );
} }
return { return {
@@ -125,12 +128,14 @@ export const actionFinalize = register({
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiPointElement, multiPointElement,
-1, -1,
arrayToMap(elements),
); );
maybeBindLinearElement( maybeBindLinearElement(
multiPointElement, multiPointElement,
appState, appState,
Scene.getScene(multiPointElement)!, Scene.getScene(multiPointElement)!,
{ x, y }, { x, y },
elementsMap,
); );
} }
} }
@@ -186,7 +191,7 @@ export const actionFinalize = register({
// To select the linear element when user has finished mutipoint editing // To select the linear element when user has finished mutipoint editing
selectedLinearElement: selectedLinearElement:
multiPointElement && isLinearElement(multiPointElement) multiPointElement && isLinearElement(multiPointElement)
? new LinearElementEditor(multiPointElement, scene) ? new LinearElementEditor(multiPointElement)
: appState.selectedLinearElement, : appState.selectedLinearElement,
pendingImageElementId: null, pendingImageElementId: null,
}, },
+7 -6
View File
@@ -4,7 +4,6 @@ import { getNonDeletedElements } from "../element";
import { import {
ExcalidrawElement, ExcalidrawElement,
NonDeleted, NonDeleted,
NonDeletedElementsMap,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
} from "../element/types"; } from "../element/types";
import { resizeMultipleElements } from "../element/resizeElements"; import { resizeMultipleElements } from "../element/resizeElements";
@@ -68,7 +67,7 @@ export const actionFlipVertical = register({
const flipSelectedElements = ( const flipSelectedElements = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, elementsMap: NonDeletedSceneElementsMap,
appState: Readonly<AppState>, appState: Readonly<AppState>,
flipDirection: "horizontal" | "vertical", flipDirection: "horizontal" | "vertical",
) => { ) => {
@@ -83,6 +82,7 @@ const flipSelectedElements = (
const updatedElements = flipElements( const updatedElements = flipElements(
selectedElements, selectedElements,
elements,
elementsMap, elementsMap,
appState, appState,
flipDirection, flipDirection,
@@ -97,7 +97,8 @@ const flipSelectedElements = (
const flipElements = ( const flipElements = (
selectedElements: NonDeleted<ExcalidrawElement>[], selectedElements: NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedElementsMap | NonDeletedSceneElementsMap, elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
appState: AppState, appState: AppState,
flipDirection: "horizontal" | "vertical", flipDirection: "horizontal" | "vertical",
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
@@ -113,9 +114,9 @@ const flipElements = (
flipDirection === "horizontal" ? minY : maxY, flipDirection === "horizontal" ? minY : maxY,
); );
(isBindingEnabled(appState) isBindingEnabled(appState)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements(selectedElements, elements, elementsMap)
: unbindLinearElements)(selectedElements); : unbindLinearElements(selectedElements, elementsMap);
return selectedElements; return selectedElements;
}; };
+8 -1
View File
@@ -180,6 +180,8 @@ export const actionUngroup = register({
trackEvent: { category: "element" }, trackEvent: { category: "element" },
perform: (elements, appState, _, app) => { perform: (elements, appState, _, app) => {
const groupIds = getSelectedGroupIds(appState); const groupIds = getSelectedGroupIds(appState);
const elementsMap = arrayToMap(elements);
if (groupIds.length === 0) { if (groupIds.length === 0) {
return { appState, elements, commitToHistory: false }; return { appState, elements, commitToHistory: false };
} }
@@ -226,7 +228,12 @@ export const actionUngroup = register({
if (frame) { if (frame) {
nextElements = replaceAllElementsInFrame( nextElements = replaceAllElementsInFrame(
nextElements, nextElements,
getElementsInResizingFrame(nextElements, frame, appState), getElementsInResizingFrame(
nextElements,
frame,
appState,
elementsMap,
),
frame, frame,
app, app,
); );
@@ -24,7 +24,7 @@ export const actionToggleLinearEditor = register({
const editingLinearElement = const editingLinearElement =
appState.editingLinearElement?.elementId === selectedElement.id appState.editingLinearElement?.elementId === selectedElement.id
? null ? null
: new LinearElementEditor(selectedElement, app.scene); : new LinearElementEditor(selectedElement);
return { return {
appState: { appState: {
...appState, ...appState,
@@ -0,0 +1,54 @@
import { getContextMenuLabel } from "../components/hyperlink/Hyperlink";
import { LinkIcon } from "../components/icons";
import { ToolButton } from "../components/ToolButton";
import { isEmbeddableElement } from "../element/typeChecks";
import { t } from "../i18n";
import { KEYS } from "../keys";
import { getSelectedElements } from "../scene";
import { getShortcutKey } from "../utils";
import { register } from "./register";
export const actionLink = register({
name: "hyperlink",
perform: (elements, appState) => {
if (appState.showHyperlinkPopup === "editor") {
return false;
}
return {
elements,
appState: {
...appState,
showHyperlinkPopup: "editor",
openMenu: null,
},
commitToHistory: true,
};
},
trackEvent: { category: "hyperlink", action: "click" },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
contextItemLabel: (elements, appState) =>
getContextMenuLabel(elements, appState),
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1;
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
return (
<ToolButton
type="button"
icon={LinkIcon}
aria-label={t(getContextMenuLabel(elements, appState))}
title={`${
isEmbeddableElement(elements[0])
? t("labels.link.labelEmbed")
: t("labels.link.label")
} - ${getShortcutKey("CtrlOrCmd+K")}`}
onClick={() => updateData(null)}
selected={selectedElements.length === 1 && !!selectedElements[0].link}
/>
);
},
});
@@ -209,6 +209,7 @@ const changeFontSize = (
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
newElement = offsetElementAfterFontResize(oldElement, newElement); newElement = offsetElementAfterFontResize(oldElement, newElement);
@@ -730,6 +731,7 @@ export const actionChangeFontFamily = register({
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
return newElement; return newElement;
} }
@@ -829,6 +831,7 @@ export const actionChangeTextAlign = register({
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
return newElement; return newElement;
} }
@@ -918,6 +921,7 @@ export const actionChangeVerticalAlign = register({
redrawTextBoundingBox( redrawTextBoundingBox(
newElement, newElement,
app.scene.getContainerElement(oldElement), app.scene.getContainerElement(oldElement),
app.scene.getNonDeletedElementsMap(),
); );
return newElement; return newElement;
} }
@@ -43,7 +43,7 @@ export const actionSelectAll = register({
// single linear element selected // single linear element selected
Object.keys(selectedElementIds).length === 1 && Object.keys(selectedElementIds).length === 1 &&
isLinearElement(elements[0]) isLinearElement(elements[0])
? new LinearElementEditor(elements[0], app.scene) ? new LinearElementEditor(elements[0])
: null, : null,
}, },
commitToHistory: true, commitToHistory: true,
+5 -1
View File
@@ -128,7 +128,11 @@ export const actionPasteStyles = register({
element.id === newElement.containerId, element.id === newElement.containerId,
) || null; ) || null;
} }
redrawTextBoundingBox(newElement, container); redrawTextBoundingBox(
newElement,
container,
app.scene.getNonDeletedElementsMap(),
);
} }
if ( if (
+1 -1
View File
@@ -83,6 +83,6 @@ export { actionToggleObjectsSnapMode } from "./actionToggleObjectsSnapMode";
export { actionToggleStats } from "./actionToggleStats"; export { actionToggleStats } from "./actionToggleStats";
export { actionUnbindText, actionBindText } from "./actionBoundText"; export { actionUnbindText, actionBindText } from "./actionBoundText";
export { actionLink } from "../element/Hyperlink"; export { actionLink } from "./actionLink";
export { actionToggleElementLock } from "./actionElementLock"; export { actionToggleElementLock } from "./actionElementLock";
export { actionToggleLinearEditor } from "./actionLinearEditor"; export { actionToggleLinearEditor } from "./actionLinearEditor";
+1 -3
View File
@@ -7,9 +7,7 @@ import {
EXPORT_SCALES, EXPORT_SCALES,
THEME, THEME,
} from "./constants"; } from "./constants";
import { t } from "./i18n";
import { AppState, NormalizedZoomValue } from "./types"; import { AppState, NormalizedZoomValue } from "./types";
import { getDateTime } from "./utils";
const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio) const defaultExportScale = EXPORT_SCALES.includes(devicePixelRatio)
? devicePixelRatio ? devicePixelRatio
@@ -65,7 +63,7 @@ export const getDefaultAppState = (): Omit<
isRotating: false, isRotating: false,
lastPointerDownWith: "mouse", lastPointerDownWith: "mouse",
multiElement: null, multiElement: null,
name: `${t("labels.untitled")}-${getDateTime()}`, name: null,
contextMenu: null, contextMenu: null,
openMenu: null, openMenu: null,
openPopup: null, openPopup: null,
+172 -98
View File
@@ -270,6 +270,7 @@ import {
updateStable, updateStable,
addEventListener, addEventListener,
normalizeEOL, normalizeEOL,
getDateTime,
} from "../utils"; } from "../utils";
import { import {
createSrcDoc, createSrcDoc,
@@ -325,9 +326,7 @@ import {
showHyperlinkTooltip, showHyperlinkTooltip,
hideHyperlinkToolip, hideHyperlinkToolip,
Hyperlink, Hyperlink,
isPointHittingLink, } from "../components/hyperlink/Hyperlink";
isPointHittingLinkIcon,
} from "../element/Hyperlink";
import { isLocalLink, normalizeLink, toValidURL } from "../data/url"; import { isLocalLink, normalizeLink, toValidURL } from "../data/url";
import { shouldShowBoundingBox } from "../element/transformHandles"; import { shouldShowBoundingBox } from "../element/transformHandles";
import { actionUnlockAllElements } from "../actions/actionElementLock"; import { actionUnlockAllElements } from "../actions/actionElementLock";
@@ -409,6 +408,10 @@ import { withBatchedUpdates, withBatchedUpdatesThrottled } from "../reactUtils";
import { getRenderOpacity } from "../renderer/renderElement"; import { getRenderOpacity } from "../renderer/renderElement";
import { textWysiwyg } from "../element/textWysiwyg"; import { textWysiwyg } from "../element/textWysiwyg";
import { isOverScrollBars } from "../scene/scrollbars"; import { isOverScrollBars } from "../scene/scrollbars";
import {
isPointHittingLink,
isPointHittingLinkIcon,
} from "./hyperlink/helpers";
const AppContext = React.createContext<AppClassProperties>(null!); const AppContext = React.createContext<AppClassProperties>(null!);
const AppPropsContext = React.createContext<AppProps>(null!); const AppPropsContext = React.createContext<AppProps>(null!);
@@ -619,7 +622,7 @@ class App extends React.Component<AppProps, AppState> {
gridModeEnabled = false, gridModeEnabled = false,
objectsSnapModeEnabled = false, objectsSnapModeEnabled = false,
theme = defaultAppState.theme, theme = defaultAppState.theme,
name = defaultAppState.name, name = `${t("labels.untitled")}-${getDateTime()}`,
} = props; } = props;
this.state = { this.state = {
...defaultAppState, ...defaultAppState,
@@ -662,6 +665,7 @@ class App extends React.Component<AppProps, AppState> {
getSceneElements: this.getSceneElements, getSceneElements: this.getSceneElements,
getAppState: () => this.state, getAppState: () => this.state,
getFiles: () => this.files, getFiles: () => this.files,
getName: this.getName,
registerAction: (action: Action) => { registerAction: (action: Action) => {
this.actionManager.registerAction(action); this.actionManager.registerAction(action);
}, },
@@ -951,6 +955,7 @@ class App extends React.Component<AppProps, AppState> {
normalizedWidth, normalizedWidth,
normalizedHeight, normalizedHeight,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
); );
const hasBeenInitialized = this.initializedEmbeds.has(el.id); const hasBeenInitialized = this.initializedEmbeds.has(el.id);
@@ -1285,6 +1290,7 @@ class App extends React.Component<AppProps, AppState> {
scrollY: this.state.scrollY, scrollY: this.state.scrollY,
zoom: this.state.zoom, zoom: this.state.zoom,
}, },
this.scene.getNonDeletedElementsMap(),
) )
) { ) {
// if frame not visible, don't render its name // if frame not visible, don't render its name
@@ -1410,6 +1416,13 @@ class App extends React.Component<AppProps, AppState> {
}); });
}; };
private toggleOverscrollBehavior(event: React.PointerEvent) {
// when pointer inside editor, disable overscroll behavior to prevent
// panning to trigger history back/forward on MacOS Chrome
document.documentElement.style.overscrollBehaviorX =
event.type === "pointerenter" ? "none" : "auto";
}
public render() { public render() {
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
const { renderTopRightUI, renderCustomStats } = this.props; const { renderTopRightUI, renderCustomStats } = this.props;
@@ -1463,6 +1476,8 @@ class App extends React.Component<AppProps, AppState> {
onKeyDown={ onKeyDown={
this.props.handleKeyboardGlobally ? undefined : this.onKeyDown this.props.handleKeyboardGlobally ? undefined : this.onKeyDown
} }
onPointerEnter={this.toggleOverscrollBehavior}
onPointerLeave={this.toggleOverscrollBehavior}
> >
<AppContext.Provider value={this}> <AppContext.Provider value={this}>
<AppPropsContext.Provider value={this.props}> <AppPropsContext.Provider value={this.props}>
@@ -1525,6 +1540,7 @@ class App extends React.Component<AppProps, AppState> {
<Hyperlink <Hyperlink
key={firstSelectedElement.id} key={firstSelectedElement.id}
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={allElementsMap}
setAppState={this.setAppState} setAppState={this.setAppState}
onLinkOpen={this.props.onLinkOpen} onLinkOpen={this.props.onLinkOpen}
setToast={this.setToast} setToast={this.setToast}
@@ -1538,6 +1554,7 @@ class App extends React.Component<AppProps, AppState> {
isMagicFrameElement(firstSelectedElement) && ( isMagicFrameElement(firstSelectedElement) && (
<ElementCanvasButtons <ElementCanvasButtons
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={elementsMap}
> >
<ElementCanvasButton <ElementCanvasButton
title={t("labels.convertToCode")} title={t("labels.convertToCode")}
@@ -1558,6 +1575,7 @@ class App extends React.Component<AppProps, AppState> {
?.status === "done" && ( ?.status === "done" && (
<ElementCanvasButtons <ElementCanvasButtons
element={firstSelectedElement} element={firstSelectedElement}
elementsMap={elementsMap}
> >
<ElementCanvasButton <ElementCanvasButton
title={t("labels.copySource")} title={t("labels.copySource")}
@@ -1725,7 +1743,7 @@ class App extends React.Component<AppProps, AppState> {
this.files, this.files,
{ {
exportBackground: this.state.exportBackground, exportBackground: this.state.exportBackground,
name: this.state.name, name: this.getName(),
viewBackgroundColor: this.state.viewBackgroundColor, viewBackgroundColor: this.state.viewBackgroundColor,
exportingFrame: opts.exportingFrame, exportingFrame: opts.exportingFrame,
}, },
@@ -2124,7 +2142,7 @@ class App extends React.Component<AppProps, AppState> {
let gridSize = actionResult?.appState?.gridSize || null; let gridSize = actionResult?.appState?.gridSize || null;
const theme = const theme =
actionResult?.appState?.theme || this.props.theme || THEME.LIGHT; actionResult?.appState?.theme || this.props.theme || THEME.LIGHT;
let name = actionResult?.appState?.name ?? this.state.name; const name = actionResult?.appState?.name ?? this.state.name;
const errorMessage = const errorMessage =
actionResult?.appState?.errorMessage ?? this.state.errorMessage; actionResult?.appState?.errorMessage ?? this.state.errorMessage;
if (typeof this.props.viewModeEnabled !== "undefined") { if (typeof this.props.viewModeEnabled !== "undefined") {
@@ -2139,10 +2157,6 @@ class App extends React.Component<AppProps, AppState> {
gridSize = this.props.gridModeEnabled ? GRID_SIZE : null; gridSize = this.props.gridModeEnabled ? GRID_SIZE : null;
} }
if (typeof this.props.name !== "undefined") {
name = this.props.name;
}
editingElement = editingElement =
editingElement || actionResult.appState?.editingElement || null; editingElement || actionResult.appState?.editingElement || null;
@@ -2455,6 +2469,7 @@ class App extends React.Component<AppProps, AppState> {
isSomeElementSelected.clearCache(); isSomeElementSelected.clearCache();
selectGroupsForSelectedElements.clearCache(); selectGroupsForSelectedElements.clearCache();
touchTimeout = 0; touchTimeout = 0;
document.documentElement.style.overscrollBehaviorX = "";
} }
private onResize = withBatchedUpdates(() => { private onResize = withBatchedUpdates(() => {
@@ -2591,10 +2606,10 @@ class App extends React.Component<AppProps, AppState> {
componentDidUpdate(prevProps: AppProps, prevState: AppState) { componentDidUpdate(prevProps: AppProps, prevState: AppState) {
this.updateEmbeddables(); this.updateEmbeddables();
if ( const elements = this.scene.getElementsIncludingDeleted();
!this.state.showWelcomeScreen && const elementsMap = this.scene.getNonDeletedElementsMap();
!this.scene.getElementsIncludingDeleted().length
) { if (!this.state.showWelcomeScreen && !elements.length) {
this.setState({ showWelcomeScreen: true }); this.setState({ showWelcomeScreen: true });
} }
@@ -2699,12 +2714,6 @@ class App extends React.Component<AppProps, AppState> {
}); });
} }
if (this.props.name && prevProps.name !== this.props.name) {
this.setState({
name: this.props.name,
});
}
this.excalidrawContainerRef.current?.classList.toggle( this.excalidrawContainerRef.current?.classList.toggle(
"theme--dark", "theme--dark",
this.state.theme === "dark", this.state.theme === "dark",
@@ -2754,27 +2763,21 @@ class App extends React.Component<AppProps, AppState> {
LinearElementEditor.getPointAtIndexGlobalCoordinates( LinearElementEditor.getPointAtIndexGlobalCoordinates(
multiElement, multiElement,
-1, -1,
elementsMap,
), ),
), ),
elementsMap,
); );
} }
this.history.record(this.state, this.scene.getElementsIncludingDeleted()); this.history.record(this.state, elements);
// Do not notify consumers if we're still loading the scene. Among other // Do not notify consumers if we're still loading the scene. Among other
// potential issues, this fixes a case where the tab isn't focused during // potential issues, this fixes a case where the tab isn't focused during
// init, which would trigger onChange with empty elements, which would then // init, which would trigger onChange with empty elements, which would then
// override whatever is in localStorage currently. // override whatever is in localStorage currently.
if (!this.state.isLoading) { if (!this.state.isLoading) {
this.props.onChange?.( this.props.onChange?.(elements, this.state, this.files);
this.scene.getElementsIncludingDeleted(), this.onChangeEmitter.trigger(elements, this.state, this.files);
this.state,
this.files,
);
this.onChangeEmitter.trigger(
this.scene.getElementsIncludingDeleted(),
this.state,
this.files,
);
} }
} }
@@ -3124,7 +3127,11 @@ class App extends React.Component<AppProps, AppState> {
newElement, newElement,
this.scene.getElementsMapIncludingDeleted(), this.scene.getElementsMapIncludingDeleted(),
); );
redrawTextBoundingBox(newElement, container); redrawTextBoundingBox(
newElement,
container,
this.scene.getElementsMapIncludingDeleted(),
);
} }
}); });
@@ -3834,7 +3841,7 @@ class App extends React.Component<AppProps, AppState> {
y: element.y + offsetY, y: element.y + offsetY,
}); });
updateBoundElements(element, { updateBoundElements(element, this.scene.getNonDeletedElementsMap(), {
simultaneouslyUpdated: selectedElements, simultaneouslyUpdated: selectedElements,
}); });
}); });
@@ -3857,7 +3864,6 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor( editingLinearElement: new LinearElementEditor(
selectedElement, selectedElement,
this.scene,
), ),
}); });
} }
@@ -4008,9 +4014,14 @@ class App extends React.Component<AppProps, AppState> {
} }
if (isArrowKey(event.key)) { if (isArrowKey(event.key)) {
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
const elementsMap = this.scene.getNonDeletedElementsMap();
isBindingEnabled(this.state) isBindingEnabled(this.state)
? bindOrUnbindSelectedElements(selectedElements) ? bindOrUnbindSelectedElements(
: unbindLinearElements(selectedElements); selectedElements,
this.scene.getNonDeletedElements(),
elementsMap,
)
: unbindLinearElements(selectedElements, elementsMap);
this.setState({ suggestedBindings: [] }); this.setState({ suggestedBindings: [] });
} }
}); });
@@ -4112,6 +4123,14 @@ class App extends React.Component<AppProps, AppState> {
return gesture.pointers.size >= 2; return gesture.pointers.size >= 2;
}; };
public getName = () => {
return (
this.state.name ||
this.props.name ||
`${t("labels.untitled")}-${getDateTime()}`
);
};
// fires only on Safari // fires only on Safari
private onGestureStart = withBatchedUpdates((event: GestureEvent) => { private onGestureStart = withBatchedUpdates((event: GestureEvent) => {
event.preventDefault(); event.preventDefault();
@@ -4183,20 +4202,21 @@ class App extends React.Component<AppProps, AppState> {
isExistingElement?: boolean; isExistingElement?: boolean;
}, },
) { ) {
const elementsMap = this.scene.getElementsMapIncludingDeleted();
const updateElement = ( const updateElement = (
text: string, text: string,
originalText: string, originalText: string,
isDeleted: boolean, isDeleted: boolean,
) => { ) => {
this.scene.replaceAllElements([ this.scene.replaceAllElements([
// Not sure why we include deleted elements as well hence using deleted elements map
...this.scene.getElementsIncludingDeleted().map((_element) => { ...this.scene.getElementsIncludingDeleted().map((_element) => {
if (_element.id === element.id && isTextElement(_element)) { if (_element.id === element.id && isTextElement(_element)) {
return updateTextElement( return updateTextElement(
_element, _element,
getContainerElement( getContainerElement(_element, elementsMap),
_element, elementsMap,
this.scene.getElementsMapIncludingDeleted(),
),
{ {
text, text,
isDeleted, isDeleted,
@@ -4228,7 +4248,7 @@ class App extends React.Component<AppProps, AppState> {
onChange: withBatchedUpdates((text) => { onChange: withBatchedUpdates((text) => {
updateElement(text, text, false); updateElement(text, text, false);
if (isNonDeletedElement(element)) { if (isNonDeletedElement(element)) {
updateBoundElements(element); updateBoundElements(element, elementsMap);
} }
}), }),
onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => { onSubmit: withBatchedUpdates(({ text, viaKeyboard, originalText }) => {
@@ -4367,6 +4387,7 @@ class App extends React.Component<AppProps, AppState> {
!(isTextElement(element) && element.containerId)), !(isTextElement(element) && element.containerId)),
); );
const elementsMap = this.scene.getNonDeletedElementsMap();
return getElementsAtPosition(elements, (element) => return getElementsAtPosition(elements, (element) =>
hitTest( hitTest(
element, element,
@@ -4374,7 +4395,7 @@ class App extends React.Component<AppProps, AppState> {
this.frameNameBoundsCache, this.frameNameBoundsCache,
x, x,
y, y,
this.scene.getNonDeletedElementsMap(), elementsMap,
), ),
).filter((element) => { ).filter((element) => {
// hitting a frame's element from outside the frame is not considered a hit // hitting a frame's element from outside the frame is not considered a hit
@@ -4382,7 +4403,7 @@ class App extends React.Component<AppProps, AppState> {
return containingFrame && return containingFrame &&
this.state.frameRendering.enabled && this.state.frameRendering.enabled &&
this.state.frameRendering.clip this.state.frameRendering.clip
? isCursorInFrame({ x, y }, containingFrame) ? isCursorInFrame({ x, y }, containingFrame, elementsMap)
: true; : true;
}); });
} }
@@ -4564,10 +4585,7 @@ class App extends React.Component<AppProps, AppState> {
) { ) {
this.history.resumeRecording(); this.history.resumeRecording();
this.setState({ this.setState({
editingLinearElement: new LinearElementEditor( editingLinearElement: new LinearElementEditor(selectedElements[0]),
selectedElements[0],
this.scene,
),
}); });
return; return;
} else if ( } else if (
@@ -4627,6 +4645,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
sceneX, sceneX,
sceneY, sceneY,
this.scene.getNonDeletedElementsMap(),
); );
if (container) { if (container) {
@@ -4638,6 +4657,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.frameNameBoundsCache, this.frameNameBoundsCache,
[sceneX, sceneY], [sceneX, sceneY],
this.scene.getNonDeletedElementsMap(),
) )
) { ) {
const midPoint = getContainerCenter( const midPoint = getContainerCenter(
@@ -4678,6 +4698,7 @@ class App extends React.Component<AppProps, AppState> {
index <= hitElementIndex && index <= hitElementIndex &&
isPointHittingLink( isPointHittingLink(
element, element,
this.scene.getNonDeletedElementsMap(),
this.state, this.state,
[scenePointer.x, scenePointer.y], [scenePointer.x, scenePointer.y],
this.device.editor.isMobile, this.device.editor.isMobile,
@@ -4708,8 +4729,10 @@ class App extends React.Component<AppProps, AppState> {
this.lastPointerDownEvent!, this.lastPointerDownEvent!,
this.state, this.state,
); );
const elementsMap = this.scene.getNonDeletedElementsMap();
const lastPointerDownHittingLinkIcon = isPointHittingLink( const lastPointerDownHittingLinkIcon = isPointHittingLink(
this.hitLinkElement, this.hitLinkElement,
elementsMap,
this.state, this.state,
[lastPointerDownCoords.x, lastPointerDownCoords.y], [lastPointerDownCoords.x, lastPointerDownCoords.y],
this.device.editor.isMobile, this.device.editor.isMobile,
@@ -4720,6 +4743,7 @@ class App extends React.Component<AppProps, AppState> {
); );
const lastPointerUpHittingLinkIcon = isPointHittingLink( const lastPointerUpHittingLinkIcon = isPointHittingLink(
this.hitLinkElement, this.hitLinkElement,
elementsMap,
this.state, this.state,
[lastPointerUpCoords.x, lastPointerUpCoords.y], [lastPointerUpCoords.x, lastPointerUpCoords.y],
this.device.editor.isMobile, this.device.editor.isMobile,
@@ -4756,10 +4780,11 @@ class App extends React.Component<AppProps, AppState> {
x: number; x: number;
y: number; y: number;
}) => { }) => {
const elementsMap = this.scene.getNonDeletedElementsMap();
const frames = this.scene const frames = this.scene
.getNonDeletedFramesLikes() .getNonDeletedFramesLikes()
.filter((frame): frame is ExcalidrawFrameLikeElement => .filter((frame): frame is ExcalidrawFrameLikeElement =>
isCursorInFrame(sceneCoords, frame), isCursorInFrame(sceneCoords, frame, elementsMap),
); );
return frames.length ? frames[frames.length - 1] : null; return frames.length ? frames[frames.length - 1] : null;
@@ -4863,6 +4888,7 @@ class App extends React.Component<AppProps, AppState> {
y: scenePointerY, y: scenePointerY,
}, },
event, event,
this.scene.getNonDeletedElementsMap(),
); );
this.setState((prevState) => { this.setState((prevState) => {
@@ -4902,6 +4928,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
); );
if ( if (
@@ -5052,6 +5079,7 @@ class App extends React.Component<AppProps, AppState> {
scenePointerY, scenePointerY,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(),
); );
if ( if (
elementWithTransformHandleType && elementWithTransformHandleType &&
@@ -5099,7 +5127,11 @@ class App extends React.Component<AppProps, AppState> {
!this.state.selectedElementIds[this.hitLinkElement.id] !this.state.selectedElementIds[this.hitLinkElement.id]
) { ) {
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER); setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
showHyperlinkTooltip(this.hitLinkElement, this.state); showHyperlinkTooltip(
this.hitLinkElement,
this.state,
this.scene.getNonDeletedElementsMap(),
);
} else { } else {
hideHyperlinkToolip(); hideHyperlinkToolip();
if ( if (
@@ -5277,10 +5309,12 @@ class App extends React.Component<AppProps, AppState> {
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
) { ) {
const elementsMap = this.scene.getNonDeletedElementsMap();
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
elementsMap,
); );
const elementsMap = this.scene.getNonDeletedElementsMap();
const boundTextElement = getBoundTextElement(element, elementsMap); const boundTextElement = getBoundTextElement(element, elementsMap);
if (!element) { if (!element) {
@@ -5295,10 +5329,12 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.frameNameBoundsCache, this.frameNameBoundsCache,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
elementsMap,
) )
) { ) {
hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor( hoverPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element, element,
elementsMap,
this.state.zoom, this.state.zoom,
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
@@ -5728,10 +5764,12 @@ class App extends React.Component<AppProps, AppState> {
if ( if (
clicklength < 300 && clicklength < 300 &&
isIframeLikeElement(this.hitLinkElement) && isIframeLikeElement(this.hitLinkElement) &&
!isPointHittingLinkIcon(this.hitLinkElement, this.state, [ !isPointHittingLinkIcon(
scenePointer.x, this.hitLinkElement,
scenePointer.y, this.scene.getNonDeletedElementsMap(),
]) this.state,
[scenePointer.x, scenePointer.y],
)
) { ) {
this.handleEmbeddableCenterClick(this.hitLinkElement); this.handleEmbeddableCenterClick(this.hitLinkElement);
} else { } else {
@@ -6029,7 +6067,9 @@ class App extends React.Component<AppProps, AppState> {
): boolean => { ): boolean => {
if (this.state.activeTool.type === "selection") { if (this.state.activeTool.type === "selection") {
const elements = this.scene.getNonDeletedElements(); const elements = this.scene.getNonDeletedElements();
const elementsMap = this.scene.getNonDeletedElementsMap();
const selectedElements = this.scene.getSelectedElements(this.state); const selectedElements = this.scene.getSelectedElements(this.state);
if (selectedElements.length === 1 && !this.state.editingLinearElement) { if (selectedElements.length === 1 && !this.state.editingLinearElement) {
const elementWithTransformHandleType = const elementWithTransformHandleType =
getElementWithTransformHandleType( getElementWithTransformHandleType(
@@ -6039,6 +6079,7 @@ class App extends React.Component<AppProps, AppState> {
pointerDownState.origin.y, pointerDownState.origin.y,
this.state.zoom, this.state.zoom,
event.pointerType, event.pointerType,
this.scene.getNonDeletedElementsMap(),
); );
if (elementWithTransformHandleType != null) { if (elementWithTransformHandleType != null) {
this.setState({ this.setState({
@@ -6062,6 +6103,7 @@ class App extends React.Component<AppProps, AppState> {
getResizeOffsetXY( getResizeOffsetXY(
pointerDownState.resize.handleType, pointerDownState.resize.handleType,
selectedElements, selectedElements,
elementsMap,
pointerDownState.origin.x, pointerDownState.origin.x,
pointerDownState.origin.y, pointerDownState.origin.y,
), ),
@@ -6086,7 +6128,8 @@ class App extends React.Component<AppProps, AppState> {
this.history, this.history,
pointerDownState.origin, pointerDownState.origin,
linearElementEditor, linearElementEditor,
this.scene.getNonDeletedElementsMap(), this.scene.getNonDeletedElements(),
elementsMap,
); );
if (ret.hitElement) { if (ret.hitElement) {
pointerDownState.hit.element = ret.hitElement; pointerDownState.hit.element = ret.hitElement;
@@ -6342,6 +6385,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
sceneX, sceneX,
sceneY, sceneY,
this.scene.getNonDeletedElementsMap(),
); );
if (hasBoundTextElement(element)) { if (hasBoundTextElement(element)) {
@@ -6422,7 +6466,8 @@ class App extends React.Component<AppProps, AppState> {
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
pointerDownState.origin, pointerDownState.origin,
this.scene, this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
); );
this.scene.addNewElement(element); this.scene.addNewElement(element);
this.setState({ this.setState({
@@ -6690,7 +6735,8 @@ class App extends React.Component<AppProps, AppState> {
}); });
const boundElement = getHoveredElementForBinding( const boundElement = getHoveredElementForBinding(
pointerDownState.origin, pointerDownState.origin,
this.scene, this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
); );
this.scene.addNewElement(element); this.scene.addNewElement(element);
@@ -6836,6 +6882,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
selectedElements, selectedElements,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
), ),
); );
} }
@@ -6859,6 +6906,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
selectedElements, selectedElements,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
), ),
); );
} }
@@ -6958,6 +7006,7 @@ class App extends React.Component<AppProps, AppState> {
return true; return true;
} }
} }
const elementsMap = this.scene.getNonDeletedElementsMap();
if (this.state.selectedLinearElement) { if (this.state.selectedLinearElement) {
const linearElementEditor = const linearElementEditor =
@@ -6968,6 +7017,7 @@ class App extends React.Component<AppProps, AppState> {
this.state.selectedLinearElement, this.state.selectedLinearElement,
pointerCoords, pointerCoords,
this.state, this.state,
elementsMap,
) )
) { ) {
const ret = LinearElementEditor.addMidpoint( const ret = LinearElementEditor.addMidpoint(
@@ -6975,6 +7025,7 @@ class App extends React.Component<AppProps, AppState> {
pointerCoords, pointerCoords,
this.state, this.state,
!event[KEYS.CTRL_OR_CMD], !event[KEYS.CTRL_OR_CMD],
elementsMap,
); );
if (!ret) { if (!ret) {
return; return;
@@ -7133,10 +7184,11 @@ class App extends React.Component<AppProps, AppState> {
this.maybeCacheReferenceSnapPoints(event, selectedElements); this.maybeCacheReferenceSnapPoints(event, selectedElements);
const { snapOffset, snapLines } = snapDraggedElements( const { snapOffset, snapLines } = snapDraggedElements(
getSelectedElements(originalElements, this.state), originalElements,
dragOffset, dragOffset,
this.state, this.state,
event, event,
this.scene.getNonDeletedElementsMap(),
); );
this.setState({ snapLines }); this.setState({ snapLines });
@@ -7320,6 +7372,7 @@ class App extends React.Component<AppProps, AppState> {
event, event,
this.state, this.state,
this.setState.bind(this), this.setState.bind(this),
this.scene.getNonDeletedElementsMap(),
); );
// regular box-select // regular box-select
} else { } else {
@@ -7350,6 +7403,7 @@ class App extends React.Component<AppProps, AppState> {
const elementsWithinSelection = getElementsWithinSelection( const elementsWithinSelection = getElementsWithinSelection(
elements, elements,
draggingElement, draggingElement,
this.scene.getNonDeletedElementsMap(),
); );
this.setState((prevState) => { this.setState((prevState) => {
@@ -7392,10 +7446,7 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement: selectedLinearElement:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
isLinearElement(elementsWithinSelection[0]) isLinearElement(elementsWithinSelection[0])
? new LinearElementEditor( ? new LinearElementEditor(elementsWithinSelection[0])
elementsWithinSelection[0],
this.scene,
)
: null, : null,
showHyperlinkPopup: showHyperlinkPopup:
elementsWithinSelection.length === 1 && elementsWithinSelection.length === 1 &&
@@ -7481,7 +7532,7 @@ class App extends React.Component<AppProps, AppState> {
this.setState({ this.setState({
selectedElementsAreBeingDragged: false, selectedElementsAreBeingDragged: false,
}); });
const elementsMap = this.scene.getNonDeletedElementsMap();
// Handle end of dragging a point of a linear element, might close a loop // Handle end of dragging a point of a linear element, might close a loop
// and sets binding element // and sets binding element
if (this.state.editingLinearElement) { if (this.state.editingLinearElement) {
@@ -7496,6 +7547,8 @@ class App extends React.Component<AppProps, AppState> {
childEvent, childEvent,
this.state.editingLinearElement, this.state.editingLinearElement,
this.state, this.state,
this.scene.getNonDeletedElements(),
elementsMap,
); );
if (editingLinearElement !== this.state.editingLinearElement) { if (editingLinearElement !== this.state.editingLinearElement) {
this.setState({ this.setState({
@@ -7519,6 +7572,8 @@ class App extends React.Component<AppProps, AppState> {
childEvent, childEvent,
this.state.selectedLinearElement, this.state.selectedLinearElement,
this.state, this.state,
this.scene.getNonDeletedElements(),
elementsMap,
); );
const { startBindingElement, endBindingElement } = const { startBindingElement, endBindingElement } =
@@ -7529,6 +7584,7 @@ class App extends React.Component<AppProps, AppState> {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
); );
} }
@@ -7668,6 +7724,7 @@ class App extends React.Component<AppProps, AppState> {
this.state, this.state,
this.scene, this.scene,
pointerCoords, pointerCoords,
elementsMap,
); );
} }
this.setState({ suggestedBindings: [], startBoundElement: null }); this.setState({ suggestedBindings: [], startBoundElement: null });
@@ -7685,10 +7742,7 @@ class App extends React.Component<AppProps, AppState> {
}, },
prevState, prevState,
), ),
selectedLinearElement: new LinearElementEditor( selectedLinearElement: new LinearElementEditor(draggingElement),
draggingElement,
this.scene,
),
})); }));
} else { } else {
this.setState((prevState) => ({ this.setState((prevState) => ({
@@ -7738,7 +7792,13 @@ class App extends React.Component<AppProps, AppState> {
const frame = getContainingFrame(linearElement); const frame = getContainingFrame(linearElement);
if (frame && linearElement) { if (frame && linearElement) {
if (!elementOverlapsWithFrame(linearElement, frame)) { if (
!elementOverlapsWithFrame(
linearElement,
frame,
this.scene.getNonDeletedElementsMap(),
)
) {
// remove the linear element from all groups // remove the linear element from all groups
// before removing it from the frame as well // before removing it from the frame as well
mutateElement(linearElement, { mutateElement(linearElement, {
@@ -7849,6 +7909,7 @@ class App extends React.Component<AppProps, AppState> {
const elementsInsideFrame = getElementsInNewFrame( const elementsInsideFrame = getElementsInNewFrame(
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
draggingElement, draggingElement,
this.scene.getNonDeletedElementsMap(),
); );
this.scene.replaceAllElements( this.scene.replaceAllElements(
@@ -7899,6 +7960,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getElementsIncludingDeleted(), this.scene.getElementsIncludingDeleted(),
frame, frame,
this.state, this.state,
elementsMap,
), ),
frame, frame,
this, this,
@@ -7920,10 +7982,7 @@ class App extends React.Component<AppProps, AppState> {
// the one we've hit // the one we've hit
if (selectedELements.length === 1) { if (selectedELements.length === 1) {
this.setState({ this.setState({
selectedLinearElement: new LinearElementEditor( selectedLinearElement: new LinearElementEditor(hitElement),
hitElement,
this.scene,
),
}); });
} }
} }
@@ -8036,10 +8095,7 @@ class App extends React.Component<AppProps, AppState> {
selectedLinearElement: selectedLinearElement:
newSelectedElements.length === 1 && newSelectedElements.length === 1 &&
isLinearElement(newSelectedElements[0]) isLinearElement(newSelectedElements[0])
? new LinearElementEditor( ? new LinearElementEditor(newSelectedElements[0])
newSelectedElements[0],
this.scene,
)
: prevState.selectedLinearElement, : prevState.selectedLinearElement,
}; };
}); });
@@ -8113,7 +8169,7 @@ class App extends React.Component<AppProps, AppState> {
// Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1. // Don't set `selectedLinearElement` if its same as the hitElement, this is mainly to prevent resetting the `hoverPointIndex` to -1.
// Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized // Future we should update the API to take care of setting the correct `hoverPointIndex` when initialized
prevState.selectedLinearElement?.elementId !== hitElement.id prevState.selectedLinearElement?.elementId !== hitElement.id
? new LinearElementEditor(hitElement, this.scene) ? new LinearElementEditor(hitElement)
: prevState.selectedLinearElement, : prevState.selectedLinearElement,
})); }));
} }
@@ -8177,9 +8233,16 @@ class App extends React.Component<AppProps, AppState> {
} }
if (pointerDownState.drag.hasOccurred || isResizing || isRotating) { if (pointerDownState.drag.hasOccurred || isResizing || isRotating) {
(isBindingEnabled(this.state) isBindingEnabled(this.state)
? bindOrUnbindSelectedElements ? bindOrUnbindSelectedElements(
: unbindLinearElements)(this.scene.getSelectedElements(this.state)); this.scene.getSelectedElements(this.state),
this.scene.getNonDeletedElements(),
elementsMap,
)
: unbindLinearElements(
this.scene.getSelectedElements(this.state),
elementsMap,
);
} }
if (activeTool.type === "laser") { if (activeTool.type === "laser") {
@@ -8656,7 +8719,8 @@ class App extends React.Component<AppProps, AppState> {
}): void => { }): void => {
const hoveredBindableElement = getHoveredElementForBinding( const hoveredBindableElement = getHoveredElementForBinding(
pointerCoords, pointerCoords,
this.scene, this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
); );
this.setState({ this.setState({
suggestedBindings: suggestedBindings:
@@ -8683,7 +8747,8 @@ class App extends React.Component<AppProps, AppState> {
(acc: NonDeleted<ExcalidrawBindableElement>[], coords) => { (acc: NonDeleted<ExcalidrawBindableElement>[], coords) => {
const hoveredBindableElement = getHoveredElementForBinding( const hoveredBindableElement = getHoveredElementForBinding(
coords, coords,
this.scene, this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
); );
if ( if (
hoveredBindableElement != null && hoveredBindableElement != null &&
@@ -8709,7 +8774,11 @@ class App extends React.Component<AppProps, AppState> {
if (selectedElements.length > 50) { if (selectedElements.length > 50) {
return; return;
} }
const suggestedBindings = getEligibleElementsForBinding(selectedElements); const suggestedBindings = getEligibleElementsForBinding(
selectedElements,
this.scene.getNonDeletedElements(),
this.scene.getNonDeletedElementsMap(),
);
this.setState({ suggestedBindings }); this.setState({ suggestedBindings });
} }
@@ -8976,7 +9045,7 @@ class App extends React.Component<AppProps, AppState> {
this, this,
), ),
selectedLinearElement: isLinearElement(element) selectedLinearElement: isLinearElement(element)
? new LinearElementEditor(element, this.scene) ? new LinearElementEditor(element)
: null, : null,
} }
: this.state), : this.state),
@@ -9048,6 +9117,7 @@ class App extends React.Component<AppProps, AppState> {
x: gridX - pointerDownState.originInGrid.x, x: gridX - pointerDownState.originInGrid.x,
y: gridY - pointerDownState.originInGrid.y, y: gridY - pointerDownState.originInGrid.y,
}, },
this.scene.getNonDeletedElementsMap(),
); );
gridX += snapOffset.x; gridX += snapOffset.x;
@@ -9086,6 +9156,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
draggingElement as ExcalidrawFrameLikeElement, draggingElement as ExcalidrawFrameLikeElement,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
), ),
}); });
} }
@@ -9205,6 +9276,7 @@ class App extends React.Component<AppProps, AppState> {
this.scene.getNonDeletedElements(), this.scene.getNonDeletedElements(),
frame, frame,
this.state, this.state,
this.scene.getNonDeletedElementsMap(),
).forEach((element) => elementsToHighlight.add(element)); ).forEach((element) => elementsToHighlight.add(element));
}); });
@@ -9501,7 +9573,6 @@ class App extends React.Component<AppProps, AppState> {
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
// TEST HOOKS // TEST HOOKS
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------
declare global { declare global {
interface Window { interface Window {
h: { h: {
@@ -9514,20 +9585,23 @@ declare global {
} }
} }
if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) { export const createTestHook = () => {
window.h = window.h || ({} as Window["h"]); if (import.meta.env.MODE === ENV.TEST || import.meta.env.DEV) {
window.h = window.h || ({} as Window["h"]);
Object.defineProperties(window.h, { Object.defineProperties(window.h, {
elements: { elements: {
configurable: true, configurable: true,
get() { get() {
return this.app?.scene.getElementsIncludingDeleted(); return this.app?.scene.getElementsIncludingDeleted();
},
set(elements: ExcalidrawElement[]) {
return this.app?.scene.replaceAllElements(elements);
},
}, },
set(elements: ExcalidrawElement[]) { });
return this.app?.scene.replaceAllElements(elements); }
}, };
},
});
}
createTestHook();
export default App; export default App;
@@ -32,7 +32,6 @@ import { Switch } from "./Switch";
import { Tooltip } from "./Tooltip"; import { Tooltip } from "./Tooltip";
import "./ImageExportDialog.scss"; import "./ImageExportDialog.scss";
import { useAppProps } from "./App";
import { FilledButton } from "./FilledButton"; import { FilledButton } from "./FilledButton";
import { cloneJSON } from "../utils"; import { cloneJSON } from "../utils";
import { prepareElementsForExport } from "../data"; import { prepareElementsForExport } from "../data";
@@ -58,6 +57,7 @@ type ImageExportModalProps = {
files: BinaryFiles; files: BinaryFiles;
actionManager: ActionManager; actionManager: ActionManager;
onExportImage: AppClassProperties["onExportImage"]; onExportImage: AppClassProperties["onExportImage"];
name: string;
}; };
const ImageExportModal = ({ const ImageExportModal = ({
@@ -66,14 +66,14 @@ const ImageExportModal = ({
files, files,
actionManager, actionManager,
onExportImage, onExportImage,
name,
}: ImageExportModalProps) => { }: ImageExportModalProps) => {
const hasSelection = isSomeElementSelected( const hasSelection = isSomeElementSelected(
elementsSnapshot, elementsSnapshot,
appStateSnapshot, appStateSnapshot,
); );
const appProps = useAppProps(); const [projectName, setProjectName] = useState(name);
const [projectName, setProjectName] = useState(appStateSnapshot.name);
const [exportSelectionOnly, setExportSelectionOnly] = useState(hasSelection); const [exportSelectionOnly, setExportSelectionOnly] = useState(hasSelection);
const [exportWithBackground, setExportWithBackground] = useState( const [exportWithBackground, setExportWithBackground] = useState(
appStateSnapshot.exportBackground, appStateSnapshot.exportBackground,
@@ -158,10 +158,6 @@ const ImageExportModal = ({
className="TextInput" className="TextInput"
value={projectName} value={projectName}
style={{ width: "30ch" }} style={{ width: "30ch" }}
disabled={
typeof appProps.name !== "undefined" ||
appStateSnapshot.viewModeEnabled
}
onChange={(event) => { onChange={(event) => {
setProjectName(event.target.value); setProjectName(event.target.value);
actionManager.executeAction( actionManager.executeAction(
@@ -347,6 +343,7 @@ export const ImageExportDialog = ({
actionManager, actionManager,
onExportImage, onExportImage,
onCloseRequest, onCloseRequest,
name,
}: { }: {
appState: UIAppState; appState: UIAppState;
elements: readonly NonDeletedExcalidrawElement[]; elements: readonly NonDeletedExcalidrawElement[];
@@ -354,6 +351,7 @@ export const ImageExportDialog = ({
actionManager: ActionManager; actionManager: ActionManager;
onExportImage: AppClassProperties["onExportImage"]; onExportImage: AppClassProperties["onExportImage"];
onCloseRequest: () => void; onCloseRequest: () => void;
name: string;
}) => { }) => {
// we need to take a snapshot so that the exported state can't be modified // we need to take a snapshot so that the exported state can't be modified
// while the dialog is open // while the dialog is open
@@ -372,6 +370,7 @@ export const ImageExportDialog = ({
files={files} files={files}
actionManager={actionManager} actionManager={actionManager}
onExportImage={onExportImage} onExportImage={onExportImage}
name={name}
/> />
</Dialog> </Dialog>
); );
@@ -195,6 +195,7 @@ const LayerUI = ({
actionManager={actionManager} actionManager={actionManager}
onExportImage={onExportImage} onExportImage={onExportImage}
onCloseRequest={() => setAppState({ openDialog: null })} onCloseRequest={() => setAppState({ openDialog: null })}
name={app.getName()}
/> />
); );
}; };
+10 -17
View File
@@ -11,7 +11,6 @@ type Props = {
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
label: string; label: string;
isNameEditable: boolean;
ignoreFocus?: boolean; ignoreFocus?: boolean;
}; };
@@ -42,23 +41,17 @@ export const ProjectName = (props: Props) => {
return ( return (
<div className="ProjectName"> <div className="ProjectName">
<label className="ProjectName-label" htmlFor="filename"> <label className="ProjectName-label" htmlFor="filename">
{`${props.label}${props.isNameEditable ? "" : ":"}`} {`${props.label}:`}
</label> </label>
{props.isNameEditable ? ( <input
<input type="text"
type="text" className="TextInput"
className="TextInput" onBlur={handleBlur}
onBlur={handleBlur} onKeyDown={handleKeyDown}
onKeyDown={handleKeyDown} id={`${id}-filename`}
id={`${id}-filename`} value={fileName}
value={fileName} onChange={(event) => setFileName(event.target.value)}
onChange={(event) => setFileName(event.target.value)} />
/>
) : (
<span className="TextInput TextInput--readonly" id={`${id}-filename`}>
{props.value}
</span>
)}
</div> </div>
); );
}; };
@@ -1,4 +1,4 @@
@import "../css/variables.module.scss"; @import "../../css/variables.module.scss";
.excalidraw-hyperlinkContainer { .excalidraw-hyperlinkContainer {
display: flex; display: flex;
@@ -1,21 +1,20 @@
import { AppState, ExcalidrawProps, Point, UIAppState } from "../types"; import { AppState, ExcalidrawProps, Point } from "../../types";
import { import {
getShortcutKey,
sceneCoordsToViewportCoords, sceneCoordsToViewportCoords,
viewportCoordsToSceneCoords, viewportCoordsToSceneCoords,
wrapEvent, wrapEvent,
} from "../utils"; } from "../../utils";
import { getEmbedLink, embeddableURLValidator } from "./embeddable"; import { getEmbedLink, embeddableURLValidator } from "../../element/embeddable";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "../../element/mutateElement";
import { import {
ElementsMap,
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./types"; } from "../../element/types";
import { register } from "../actions/register"; import { ToolButton } from "../ToolButton";
import { ToolButton } from "../components/ToolButton"; import { FreedrawIcon, TrashIcon } from "../icons";
import { FreedrawIcon, LinkIcon, TrashIcon } from "../components/icons"; import { t } from "../../i18n";
import { t } from "../i18n";
import { import {
useCallback, useCallback,
useEffect, useEffect,
@@ -24,21 +23,19 @@ import {
useState, useState,
} from "react"; } from "react";
import clsx from "clsx"; import clsx from "clsx";
import { KEYS } from "../keys"; import { KEYS } from "../../keys";
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement"; import { EVENT, HYPERLINK_TOOLTIP_DELAY } from "../../constants";
import { rotate } from "../math"; import { getElementAbsoluteCoords } from "../../element/bounds";
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants"; import { getTooltipDiv, updateTooltipPosition } from "../Tooltip";
import { Bounds } from "./bounds"; import { getSelectedElements } from "../../scene";
import { getTooltipDiv, updateTooltipPosition } from "../components/Tooltip"; import { isPointHittingElementBoundingBox } from "../../element/collision";
import { getSelectedElements } from "../scene"; import { isLocalLink, normalizeLink } from "../../data/url";
import { isPointHittingElementBoundingBox } from "./collision";
import { getElementAbsoluteCoords } from ".";
import { isLocalLink, normalizeLink } from "../data/url";
import "./Hyperlink.scss"; import "./Hyperlink.scss";
import { trackEvent } from "../analytics"; import { trackEvent } from "../../analytics";
import { useAppProps, useExcalidrawAppState } from "../components/App"; import { useAppProps, useExcalidrawAppState } from "../App";
import { isEmbeddableElement } from "./typeChecks"; import { isEmbeddableElement } from "../../element/typeChecks";
import { getLinkHandleFromCoords } from "./helpers";
const CONTAINER_WIDTH = 320; const CONTAINER_WIDTH = 320;
const SPACE_BOTTOM = 85; const SPACE_BOTTOM = 85;
@@ -46,11 +43,6 @@ const CONTAINER_PADDING = 5;
const CONTAINER_HEIGHT = 42; const CONTAINER_HEIGHT = 42;
const AUTO_HIDE_TIMEOUT = 500; const AUTO_HIDE_TIMEOUT = 500;
export const EXTERNAL_LINK_IMG = document.createElement("img");
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
)}`;
let IS_HYPERLINK_TOOLTIP_VISIBLE = false; let IS_HYPERLINK_TOOLTIP_VISIBLE = false;
const embeddableLinkCache = new Map< const embeddableLinkCache = new Map<
@@ -60,12 +52,14 @@ const embeddableLinkCache = new Map<
export const Hyperlink = ({ export const Hyperlink = ({
element, element,
elementsMap,
setAppState, setAppState,
onLinkOpen, onLinkOpen,
setToast, setToast,
updateEmbedValidationStatus, updateEmbedValidationStatus,
}: { }: {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap;
setAppState: React.Component<any, AppState>["setState"]; setAppState: React.Component<any, AppState>["setState"];
onLinkOpen: ExcalidrawProps["onLinkOpen"]; onLinkOpen: ExcalidrawProps["onLinkOpen"];
setToast: ( setToast: (
@@ -182,7 +176,7 @@ export const Hyperlink = ({
if (timeoutId) { if (timeoutId) {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
const shouldHide = shouldHideLinkPopup(element, appState, [ const shouldHide = shouldHideLinkPopup(element, elementsMap, appState, [
event.clientX, event.clientX,
event.clientY, event.clientY,
]) as boolean; ]) as boolean;
@@ -199,7 +193,7 @@ export const Hyperlink = ({
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
}; };
}, [appState, element, isEditing, setAppState]); }, [appState, element, isEditing, setAppState, elementsMap]);
const handleRemove = useCallback(() => { const handleRemove = useCallback(() => {
trackEvent("hyperlink", "delete"); trackEvent("hyperlink", "delete");
@@ -214,7 +208,7 @@ export const Hyperlink = ({
trackEvent("hyperlink", "edit", "popup-ui"); trackEvent("hyperlink", "edit", "popup-ui");
setAppState({ showHyperlinkPopup: "editor" }); setAppState({ showHyperlinkPopup: "editor" });
}; };
const { x, y } = getCoordsForPopover(element, appState); const { x, y } = getCoordsForPopover(element, appState, elementsMap);
if ( if (
appState.contextMenu || appState.contextMenu ||
appState.draggingElement || appState.draggingElement ||
@@ -324,8 +318,9 @@ export const Hyperlink = ({
const getCoordsForPopover = ( const getCoordsForPopover = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1] = getElementAbsoluteCoords(element); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width / 2, sceneY: y1 }, { sceneX: x1 + element.width / 2, sceneY: y1 },
appState, appState,
@@ -335,51 +330,6 @@ const getCoordsForPopover = (
return { x, y }; return { x, y };
}; };
export const actionLink = register({
name: "hyperlink",
perform: (elements, appState) => {
if (appState.showHyperlinkPopup === "editor") {
return false;
}
return {
elements,
appState: {
...appState,
showHyperlinkPopup: "editor",
openMenu: null,
},
commitToHistory: true,
};
},
trackEvent: { category: "hyperlink", action: "click" },
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.K,
contextItemLabel: (elements, appState) =>
getContextMenuLabel(elements, appState),
predicate: (elements, appState) => {
const selectedElements = getSelectedElements(elements, appState);
return selectedElements.length === 1;
},
PanelComponent: ({ elements, appState, updateData }) => {
const selectedElements = getSelectedElements(elements, appState);
return (
<ToolButton
type="button"
icon={LinkIcon}
aria-label={t(getContextMenuLabel(elements, appState))}
title={`${
isEmbeddableElement(elements[0])
? t("labels.link.labelEmbed")
: t("labels.link.label")
} - ${getShortcutKey("CtrlOrCmd+K")}`}
onClick={() => updateData(null)}
selected={selectedElements.length === 1 && !!selectedElements[0].link}
/>
);
},
});
export const getContextMenuLabel = ( export const getContextMenuLabel = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
@@ -395,89 +345,17 @@ export const getContextMenuLabel = (
return label; return label;
}; };
export const getLinkHandleFromCoords = (
[x1, y1, x2, y2]: Bounds,
angle: number,
appState: Pick<UIAppState, "zoom">,
): Bounds => {
const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value;
const linkHeight = size / appState.zoom.value;
const linkMarginY = size / appState.zoom.value;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
const centeringOffset = (size - 8) / (2 * appState.zoom.value);
const dashedLineMargin = 4 / appState.zoom.value;
// Same as `ne` resize handle
const x = x2 + dashedLineMargin - centeringOffset;
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
const [rotatedX, rotatedY] = rotate(
x + linkWidth / 2,
y + linkHeight / 2,
centerX,
centerY,
angle,
);
return [
rotatedX - linkWidth / 2,
rotatedY - linkHeight / 2,
linkWidth,
linkHeight,
];
};
export const isPointHittingLinkIcon = (
element: NonDeletedExcalidrawElement,
appState: AppState,
[x, y]: Point,
) => {
const threshold = 4 / appState.zoom.value;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2],
element.angle,
appState,
);
const hitLink =
x > linkX - threshold &&
x < linkX + threshold + linkWidth &&
y > linkY - threshold &&
y < linkY + linkHeight + threshold;
return hitLink;
};
export const isPointHittingLink = (
element: NonDeletedExcalidrawElement,
appState: AppState,
[x, y]: Point,
isMobile: boolean,
) => {
if (!element.link || appState.selectedElementIds[element.id]) {
return false;
}
const threshold = 4 / appState.zoom.value;
if (
!isMobile &&
appState.viewModeEnabled &&
isPointHittingElementBoundingBox(element, [x, y], threshold, null)
) {
return true;
}
return isPointHittingLinkIcon(element, appState, [x, y]);
};
let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null; let HYPERLINK_TOOLTIP_TIMEOUT_ID: number | null = null;
export const showHyperlinkTooltip = ( export const showHyperlinkTooltip = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
if (HYPERLINK_TOOLTIP_TIMEOUT_ID) { if (HYPERLINK_TOOLTIP_TIMEOUT_ID) {
clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID); clearTimeout(HYPERLINK_TOOLTIP_TIMEOUT_ID);
} }
HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout( HYPERLINK_TOOLTIP_TIMEOUT_ID = window.setTimeout(
() => renderTooltip(element, appState), () => renderTooltip(element, appState, elementsMap),
HYPERLINK_TOOLTIP_DELAY, HYPERLINK_TOOLTIP_DELAY,
); );
}; };
@@ -485,6 +363,7 @@ export const showHyperlinkTooltip = (
const renderTooltip = ( const renderTooltip = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
if (!element.link) { if (!element.link) {
return; return;
@@ -496,7 +375,7 @@ const renderTooltip = (
tooltipDiv.style.maxWidth = "20rem"; tooltipDiv.style.maxWidth = "20rem";
tooltipDiv.textContent = element.link; tooltipDiv.textContent = element.link;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords( const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2], [x1, y1, x2, y2],
@@ -533,8 +412,9 @@ export const hideHyperlinkToolip = () => {
} }
}; };
export const shouldHideLinkPopup = ( const shouldHideLinkPopup = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState, appState: AppState,
[clientX, clientY]: Point, [clientX, clientY]: Point,
): Boolean => { ): Boolean => {
@@ -546,11 +426,17 @@ export const shouldHideLinkPopup = (
const threshold = 15 / appState.zoom.value; const threshold = 15 / appState.zoom.value;
// hitbox to prevent hiding when hovered in element bounding box // hitbox to prevent hiding when hovered in element bounding box
if ( if (
isPointHittingElementBoundingBox(element, [sceneX, sceneY], threshold, null) isPointHittingElementBoundingBox(
element,
elementsMap,
[sceneX, sceneY],
threshold,
null,
)
) { ) {
return false; return false;
} }
const [x1, y1, x2] = getElementAbsoluteCoords(element); const [x1, y1, x2] = getElementAbsoluteCoords(element, elementsMap);
// hit box to prevent hiding when hovered in the vertical area between element and popover // hit box to prevent hiding when hovered in the vertical area between element and popover
if ( if (
sceneX >= x1 && sceneX >= x1 &&
@@ -561,7 +447,11 @@ export const shouldHideLinkPopup = (
return false; return false;
} }
// hit box to prevent hiding when hovered around popover within threshold // hit box to prevent hiding when hovered around popover within threshold
const { x: popoverX, y: popoverY } = getCoordsForPopover(element, appState); const { x: popoverX, y: popoverY } = getCoordsForPopover(
element,
appState,
elementsMap,
);
if ( if (
clientX >= popoverX - threshold && clientX >= popoverX - threshold &&
@@ -0,0 +1,93 @@
import { MIME_TYPES } from "../../constants";
import { Bounds, getElementAbsoluteCoords } from "../../element/bounds";
import { isPointHittingElementBoundingBox } from "../../element/collision";
import { ElementsMap, NonDeletedExcalidrawElement } from "../../element/types";
import { rotate } from "../../math";
import { DEFAULT_LINK_SIZE } from "../../renderer/renderElement";
import { AppState, Point, UIAppState } from "../../types";
export const EXTERNAL_LINK_IMG = document.createElement("img");
EXTERNAL_LINK_IMG.src = `data:${MIME_TYPES.svg}, ${encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#1971c2" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-external-link"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg>`,
)}`;
export const getLinkHandleFromCoords = (
[x1, y1, x2, y2]: Bounds,
angle: number,
appState: Pick<UIAppState, "zoom">,
): Bounds => {
const size = DEFAULT_LINK_SIZE;
const linkWidth = size / appState.zoom.value;
const linkHeight = size / appState.zoom.value;
const linkMarginY = size / appState.zoom.value;
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
const centeringOffset = (size - 8) / (2 * appState.zoom.value);
const dashedLineMargin = 4 / appState.zoom.value;
// Same as `ne` resize handle
const x = x2 + dashedLineMargin - centeringOffset;
const y = y1 - dashedLineMargin - linkMarginY + centeringOffset;
const [rotatedX, rotatedY] = rotate(
x + linkWidth / 2,
y + linkHeight / 2,
centerX,
centerY,
angle,
);
return [
rotatedX - linkWidth / 2,
rotatedY - linkHeight / 2,
linkWidth,
linkHeight,
];
};
export const isPointHittingLinkIcon = (
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState,
[x, y]: Point,
) => {
const threshold = 4 / appState.zoom.value;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [linkX, linkY, linkWidth, linkHeight] = getLinkHandleFromCoords(
[x1, y1, x2, y2],
element.angle,
appState,
);
const hitLink =
x > linkX - threshold &&
x < linkX + threshold + linkWidth &&
y > linkY - threshold &&
y < linkY + linkHeight + threshold;
return hitLink;
};
export const isPointHittingLink = (
element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState,
[x, y]: Point,
isMobile: boolean,
) => {
if (!element.link || appState.selectedElementIds[element.id]) {
return false;
}
const threshold = 4 / appState.zoom.value;
if (
!isMobile &&
appState.viewModeEnabled &&
isPointHittingElementBoundingBox(
element,
elementsMap,
[x, y],
threshold,
null,
)
) {
return true;
}
return isPointHittingLinkIcon(element, elementsMap, appState, [x, y]);
};
+6
View File
@@ -381,3 +381,9 @@ export const EDITOR_LS_KEYS = {
MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw", MERMAID_TO_EXCALIDRAW: "mermaid-to-excalidraw",
PUBLISH_LIBRARY: "publish-library-data", PUBLISH_LIBRARY: "publish-library-data",
} as const; } as const;
/**
* not translated as this is used only in public, stateless API as default value
* where filename is optional and we can't retrieve name from app state
*/
export const DEFAULT_FILENAME = "Untitled";
@@ -14,6 +14,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -49,6 +50,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -79,6 +81,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "ellipse-1", "elementId": "ellipse-1",
@@ -132,6 +135,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "ellipse-1", "elementId": "ellipse-1",
@@ -190,6 +194,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing s
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -227,6 +232,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
}, },
], ],
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -271,6 +277,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
}, },
], ],
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -313,6 +320,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "text-2", "elementId": "text-2",
@@ -368,6 +376,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to existing t
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id48", "containerId": "id48",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -410,6 +419,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id40", "elementId": "id40",
@@ -465,6 +475,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id37", "containerId": "id37",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -507,6 +518,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -542,6 +554,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to shapes whe
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -577,6 +590,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": { "endBinding": {
"elementId": "id44", "elementId": "id44",
@@ -632,6 +646,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id41", "containerId": "id41",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -676,6 +691,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
}, },
], ],
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -720,6 +736,7 @@ exports[`Test Transform > Test arrow bindings > should bind arrows to text when
}, },
], ],
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -757,6 +774,7 @@ exports[`Test Transform > should not allow duplicate ids 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -787,6 +805,7 @@ exports[`Test Transform > should transform linear elements 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -832,6 +851,7 @@ exports[`Test Transform > should transform linear elements 2`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "triangle", "endArrowhead": "triangle",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -877,6 +897,7 @@ exports[`Test Transform > should transform linear elements 3`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -922,6 +943,7 @@ exports[`Test Transform > should transform linear elements 4`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -967,6 +989,7 @@ exports[`Test Transform > should transform regular shapes 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -997,6 +1020,7 @@ exports[`Test Transform > should transform regular shapes 2`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1027,6 +1051,7 @@ exports[`Test Transform > should transform regular shapes 3`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1057,6 +1082,7 @@ exports[`Test Transform > should transform regular shapes 4`] = `
"angle": 0, "angle": 0,
"backgroundColor": "#c0eb75", "backgroundColor": "#c0eb75",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1087,6 +1113,7 @@ exports[`Test Transform > should transform regular shapes 5`] = `
"angle": 0, "angle": 0,
"backgroundColor": "#ffc9c9", "backgroundColor": "#ffc9c9",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1117,6 +1144,7 @@ exports[`Test Transform > should transform regular shapes 6`] = `
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1149,6 +1177,7 @@ exports[`Test Transform > should transform text element 1`] = `
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1188,6 +1217,7 @@ exports[`Test Transform > should transform text element 2`] = `
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1230,6 +1260,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -1280,6 +1311,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -1330,6 +1362,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -1380,6 +1413,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -1427,6 +1461,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id25", "containerId": "id25",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1466,6 +1501,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id26", "containerId": "id26",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1505,6 +1541,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id27", "containerId": "id27",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1545,6 +1582,7 @@ exports[`Test Transform > should transform to labelled arrows when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id28", "containerId": "id28",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1588,6 +1626,7 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1623,6 +1662,7 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1658,6 +1698,7 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1693,6 +1734,7 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1728,6 +1770,7 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1763,6 +1806,7 @@ exports[`Test Transform > should transform to text containers when label provide
"type": "text", "type": "text",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1795,6 +1839,7 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id13", "containerId": "id13",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1834,6 +1879,7 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id14", "containerId": "id14",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1874,6 +1920,7 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id15", "containerId": "id15",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1916,6 +1963,7 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id16", "containerId": "id16",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1956,6 +2004,7 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id17", "containerId": "id17",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
@@ -1997,6 +2046,7 @@ exports[`Test Transform > should transform to text containers when label provide
"baseline": 0, "baseline": 0,
"boundElements": null, "boundElements": null,
"containerId": "id18", "containerId": "id18",
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 20, "fontSize": 20,
+9 -3
View File
@@ -2,7 +2,12 @@ import {
copyBlobToClipboardAsPng, copyBlobToClipboardAsPng,
copyTextToSystemClipboard, copyTextToSystemClipboard,
} from "../clipboard"; } from "../clipboard";
import { DEFAULT_EXPORT_PADDING, isFirefox, MIME_TYPES } from "../constants"; import {
DEFAULT_EXPORT_PADDING,
DEFAULT_FILENAME,
isFirefox,
MIME_TYPES,
} from "../constants";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { isFrameLikeElement } from "../element/typeChecks"; import { isFrameLikeElement } from "../element/typeChecks";
import { import {
@@ -84,14 +89,15 @@ export const exportCanvas = async (
exportBackground, exportBackground,
exportPadding = DEFAULT_EXPORT_PADDING, exportPadding = DEFAULT_EXPORT_PADDING,
viewBackgroundColor, viewBackgroundColor,
name, name = appState.name || DEFAULT_FILENAME,
fileHandle = null, fileHandle = null,
exportingFrame = null, exportingFrame = null,
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
name: string; /** filename, if applicable */
name?: string;
fileHandle?: FileSystemHandle | null; fileHandle?: FileSystemHandle | null;
exportingFrame: ExcalidrawFrameLikeElement | null; exportingFrame: ExcalidrawFrameLikeElement | null;
}, },
+4 -1
View File
@@ -1,6 +1,7 @@
import { fileOpen, fileSave } from "./filesystem"; import { fileOpen, fileSave } from "./filesystem";
import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState"; import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState";
import { import {
DEFAULT_FILENAME,
EXPORT_DATA_TYPES, EXPORT_DATA_TYPES,
EXPORT_SOURCE, EXPORT_SOURCE,
MIME_TYPES, MIME_TYPES,
@@ -71,6 +72,8 @@ export const saveAsJSON = async (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
/** filename */
name: string = appState.name || DEFAULT_FILENAME,
) => { ) => {
const serialized = serializeAsJSON(elements, appState, files, "local"); const serialized = serializeAsJSON(elements, appState, files, "local");
const blob = new Blob([serialized], { const blob = new Blob([serialized], {
@@ -78,7 +81,7 @@ export const saveAsJSON = async (
}); });
const fileHandle = await fileSave(blob, { const fileHandle = await fileSave(blob, {
name: appState.name, name,
extension: "excalidraw", extension: "excalidraw",
description: "Excalidraw file", description: "Excalidraw file",
fileHandle: isImageFileHandle(appState.fileHandle) fileHandle: isImageFileHandle(appState.fileHandle)
+2 -1
View File
@@ -7,8 +7,9 @@ export const resaveAsImageWithScene = async (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
appState: AppState, appState: AppState,
files: BinaryFiles, files: BinaryFiles,
name: string,
) => { ) => {
const { exportBackground, viewBackgroundColor, name, fileHandle } = appState; const { exportBackground, viewBackgroundColor, fileHandle } = appState;
const fileHandleType = getFileHandleType(fileHandle); const fileHandleType = getFileHandleType(fileHandle);
+1
View File
@@ -462,6 +462,7 @@ export const restoreElements = (
refreshTextDimensions( refreshTextDimensions(
element, element,
getContainerElement(element, restoredElementsMap), getContainerElement(element, restoredElementsMap),
restoredElementsMap,
), ),
); );
} }
@@ -822,4 +822,22 @@ describe("Test Transform", () => {
"Duplicate id found for rect-1", "Duplicate id found for rect-1",
); );
}); });
it("should contains customData if provided", () => {
const rawData = [
{
type: "rectangle",
x: 100,
y: 100,
customData: { createdBy: "user01" },
},
];
const convertedElements = convertToExcalidrawElements(
rawData as ExcalidrawElementSkeleton[],
opts,
);
expect(convertedElements[0].customData).toStrictEqual({
createdBy: "user01",
});
});
}); });
+14 -3
View File
@@ -39,11 +39,12 @@ import {
ExcalidrawTextElement, ExcalidrawTextElement,
FileId, FileId,
FontFamilyValues, FontFamilyValues,
NonDeletedSceneElementsMap,
TextAlign, TextAlign,
VerticalAlign, VerticalAlign,
} from "../element/types"; } from "../element/types";
import { MarkOptional } from "../utility-types"; import { MarkOptional } from "../utility-types";
import { arrayToMap, assertNever, cloneJSON, getFontString } from "../utils"; import { assertNever, cloneJSON, getFontString, toBrandedType } from "../utils";
import { getSizeFromPoints } from "../points"; import { getSizeFromPoints } from "../points";
import { randomId } from "../random"; import { randomId } from "../random";
@@ -222,7 +223,7 @@ const bindTextToContainer = (
}), }),
}); });
redrawTextBoundingBox(textElement, container); redrawTextBoundingBox(textElement, container, elementsMap);
return [container, textElement] as const; return [container, textElement] as const;
}; };
@@ -231,6 +232,7 @@ const bindLinearElementToElement = (
start: ValidLinearElement["start"], start: ValidLinearElement["start"],
end: ValidLinearElement["end"], end: ValidLinearElement["end"],
elementStore: ElementStore, elementStore: ElementStore,
elementsMap: NonDeletedSceneElementsMap,
): { ): {
linearElement: ExcalidrawLinearElement; linearElement: ExcalidrawLinearElement;
startBoundElement?: ExcalidrawElement; startBoundElement?: ExcalidrawElement;
@@ -316,6 +318,7 @@ const bindLinearElementToElement = (
linearElement, linearElement,
startBoundElement as ExcalidrawBindableElement, startBoundElement as ExcalidrawBindableElement,
"start", "start",
elementsMap,
); );
} }
} }
@@ -390,6 +393,7 @@ const bindLinearElementToElement = (
linearElement, linearElement,
endBoundElement as ExcalidrawBindableElement, endBoundElement as ExcalidrawBindableElement,
"end", "end",
elementsMap,
); );
} }
} }
@@ -457,6 +461,10 @@ class ElementStore {
return Array.from(this.excalidrawElements.values()); return Array.from(this.excalidrawElements.values());
}; };
getElementsMap = () => {
return toBrandedType<NonDeletedSceneElementsMap>(this.excalidrawElements);
};
getElement = (id: string) => { getElement = (id: string) => {
return this.excalidrawElements.get(id); return this.excalidrawElements.get(id);
}; };
@@ -612,6 +620,7 @@ export const convertToExcalidrawElements = (
} }
} }
const elementsMap = elementStore.getElementsMap();
// Add labels and arrow bindings // Add labels and arrow bindings
for (const [id, element] of elementsWithIds) { for (const [id, element] of elementsWithIds) {
const excalidrawElement = elementStore.getElement(id)!; const excalidrawElement = elementStore.getElement(id)!;
@@ -625,7 +634,7 @@ export const convertToExcalidrawElements = (
let [container, text] = bindTextToContainer( let [container, text] = bindTextToContainer(
excalidrawElement, excalidrawElement,
element?.label, element?.label,
arrayToMap(elementStore.getElements()), elementsMap,
); );
elementStore.add(container); elementStore.add(container);
elementStore.add(text); elementStore.add(text);
@@ -653,6 +662,7 @@ export const convertToExcalidrawElements = (
originalStart, originalStart,
originalEnd, originalEnd,
elementStore, elementStore,
elementsMap,
); );
container = linearElement; container = linearElement;
elementStore.add(linearElement); elementStore.add(linearElement);
@@ -677,6 +687,7 @@ export const convertToExcalidrawElements = (
start, start,
end, end,
elementStore, elementStore,
elementsMap,
); );
elementStore.add(linearElement); elementStore.add(linearElement);
@@ -1,6 +1,6 @@
import { AppState } from "../types"; import { AppState } from "../types";
import { sceneCoordsToViewportCoords } from "../utils"; import { sceneCoordsToViewportCoords } from "../utils";
import { NonDeletedExcalidrawElement } from "./types"; import { ElementsMap, NonDeletedExcalidrawElement } from "./types";
import { getElementAbsoluteCoords } from "."; import { getElementAbsoluteCoords } from ".";
import { useExcalidrawAppState } from "../components/App"; import { useExcalidrawAppState } from "../components/App";
@@ -11,8 +11,9 @@ const CONTAINER_PADDING = 5;
const getContainerCoords = ( const getContainerCoords = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1] = getElementAbsoluteCoords(element); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x1 + element.width, sceneY: y1 }, { sceneX: x1 + element.width, sceneY: y1 },
appState, appState,
@@ -25,9 +26,11 @@ const getContainerCoords = (
export const ElementCanvasButtons = ({ export const ElementCanvasButtons = ({
children, children,
element, element,
elementsMap,
}: { }: {
children: React.ReactNode; children: React.ReactNode;
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap;
}) => { }) => {
const appState = useExcalidrawAppState(); const appState = useExcalidrawAppState();
@@ -42,7 +45,7 @@ export const ElementCanvasButtons = ({
return null; return null;
} }
const { x, y } = getContainerCoords(element, appState); const { x, y } = getContainerCoords(element, appState, elementsMap);
return ( return (
<div <div
+140 -42
View File
@@ -5,6 +5,8 @@ import {
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
PointBinding, PointBinding,
ExcalidrawElement, ExcalidrawElement,
ElementsMap,
NonDeletedSceneElementsMap,
} from "./types"; } from "./types";
import { getElementAtPosition } from "../scene"; import { getElementAtPosition } from "../scene";
import { AppState } from "../types"; import { AppState } from "../types";
@@ -66,6 +68,7 @@ export const bindOrUnbindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startBindingElement: ExcalidrawBindableElement | null | "keep", startBindingElement: ExcalidrawBindableElement | null | "keep",
endBindingElement: ExcalidrawBindableElement | null | "keep", endBindingElement: ExcalidrawBindableElement | null | "keep",
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set(); const boundToElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set(); const unboundFromElementIds: Set<ExcalidrawBindableElement["id"]> = new Set();
@@ -76,6 +79,7 @@ export const bindOrUnbindLinearElement = (
"start", "start",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap,
); );
bindOrUnbindLinearElementEdge( bindOrUnbindLinearElementEdge(
linearElement, linearElement,
@@ -84,6 +88,7 @@ export const bindOrUnbindLinearElement = (
"end", "end",
boundToElementIds, boundToElementIds,
unboundFromElementIds, unboundFromElementIds,
elementsMap,
); );
const onlyUnbound = Array.from(unboundFromElementIds).filter( const onlyUnbound = Array.from(unboundFromElementIds).filter(
@@ -111,6 +116,7 @@ const bindOrUnbindLinearElementEdge = (
boundToElementIds: Set<ExcalidrawBindableElement["id"]>, boundToElementIds: Set<ExcalidrawBindableElement["id"]>,
// Is mutated // Is mutated
unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>, unboundFromElementIds: Set<ExcalidrawBindableElement["id"]>,
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
if (bindableElement !== "keep") { if (bindableElement !== "keep") {
if (bindableElement != null) { if (bindableElement != null) {
@@ -127,7 +133,12 @@ const bindOrUnbindLinearElementEdge = (
: startOrEnd === "start" || : startOrEnd === "start" ||
otherEdgeBindableElement.id !== bindableElement.id) otherEdgeBindableElement.id !== bindableElement.id)
) { ) {
bindLinearElement(linearElement, bindableElement, startOrEnd); bindLinearElement(
linearElement,
bindableElement,
startOrEnd,
elementsMap,
);
boundToElementIds.add(bindableElement.id); boundToElementIds.add(bindableElement.id);
} }
} else { } else {
@@ -140,31 +151,48 @@ const bindOrUnbindLinearElementEdge = (
}; };
export const bindOrUnbindSelectedElements = ( export const bindOrUnbindSelectedElements = (
elements: NonDeleted<ExcalidrawElement>[], selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
elements.forEach((element) => { selectedElements.forEach((selectedElement) => {
if (isBindingElement(element)) { if (isBindingElement(selectedElement)) {
bindOrUnbindLinearElement( bindOrUnbindLinearElement(
element, selectedElement,
getElligibleElementForBindingElement(element, "start"), getElligibleElementForBindingElement(
getElligibleElementForBindingElement(element, "end"), selectedElement,
"start",
elements,
elementsMap,
),
getElligibleElementForBindingElement(
selectedElement,
"end",
elements,
elementsMap,
),
elementsMap,
); );
} else if (isBindableElement(element)) { } else if (isBindableElement(selectedElement)) {
maybeBindBindableElement(element); maybeBindBindableElement(selectedElement, elementsMap);
} }
}); });
}; };
const maybeBindBindableElement = ( const maybeBindBindableElement = (
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
getElligibleElementsForBindableElementAndWhere(bindableElement).forEach( getElligibleElementsForBindableElementAndWhere(
([linearElement, where]) => bindableElement,
bindOrUnbindLinearElement( elementsMap,
linearElement, ).forEach(([linearElement, where]) =>
where === "end" ? "keep" : bindableElement, bindOrUnbindLinearElement(
where === "start" ? "keep" : bindableElement, linearElement,
), where === "end" ? "keep" : bindableElement,
where === "start" ? "keep" : bindableElement,
elementsMap,
),
); );
}; };
@@ -173,11 +201,21 @@ export const maybeBindLinearElement = (
appState: AppState, appState: AppState,
scene: Scene, scene: Scene,
pointerCoords: { x: number; y: number }, pointerCoords: { x: number; y: number },
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
if (appState.startBoundElement != null) { if (appState.startBoundElement != null) {
bindLinearElement(linearElement, appState.startBoundElement, "start"); bindLinearElement(
linearElement,
appState.startBoundElement,
"start",
elementsMap,
);
} }
const hoveredElement = getHoveredElementForBinding(pointerCoords, scene); const hoveredElement = getHoveredElementForBinding(
pointerCoords,
scene.getNonDeletedElements(),
elementsMap,
);
if ( if (
hoveredElement != null && hoveredElement != null &&
!isLinearElementSimpleAndAlreadyBoundOnOppositeEdge( !isLinearElementSimpleAndAlreadyBoundOnOppositeEdge(
@@ -186,7 +224,7 @@ export const maybeBindLinearElement = (
"end", "end",
) )
) { ) {
bindLinearElement(linearElement, hoveredElement, "end"); bindLinearElement(linearElement, hoveredElement, "end", elementsMap);
} }
}; };
@@ -194,11 +232,17 @@ export const bindLinearElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
mutateElement(linearElement, { mutateElement(linearElement, {
[startOrEnd === "start" ? "startBinding" : "endBinding"]: { [startOrEnd === "start" ? "startBinding" : "endBinding"]: {
elementId: hoveredElement.id, elementId: hoveredElement.id,
...calculateFocusAndGap(linearElement, hoveredElement, startOrEnd), ...calculateFocusAndGap(
linearElement,
hoveredElement,
startOrEnd,
elementsMap,
),
} as PointBinding, } as PointBinding,
}); });
@@ -240,10 +284,11 @@ export const isLinearElementSimpleAndAlreadyBound = (
export const unbindLinearElements = ( export const unbindLinearElements = (
elements: NonDeleted<ExcalidrawElement>[], elements: NonDeleted<ExcalidrawElement>[],
elementsMap: NonDeletedSceneElementsMap,
): void => { ): void => {
elements.forEach((element) => { elements.forEach((element) => {
if (isBindingElement(element)) { if (isBindingElement(element)) {
bindOrUnbindLinearElement(element, null, null); bindOrUnbindLinearElement(element, null, null, elementsMap);
} }
}); });
}; };
@@ -266,13 +311,14 @@ export const getHoveredElementForBinding = (
x: number; x: number;
y: number; y: number;
}, },
scene: Scene, elements: readonly NonDeletedExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
const hoveredElement = getElementAtPosition( const hoveredElement = getElementAtPosition(
scene.getNonDeletedElements(), elements,
(element) => (element) =>
isBindableElement(element, false) && isBindableElement(element, false) &&
bindingBorderTest(element, pointerCoords), bindingBorderTest(element, pointerCoords, elementsMap),
); );
return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null; return hoveredElement as NonDeleted<ExcalidrawBindableElement> | null;
}; };
@@ -281,21 +327,33 @@ const calculateFocusAndGap = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
hoveredElement: ExcalidrawBindableElement, hoveredElement: ExcalidrawBindableElement,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap,
): { focus: number; gap: number } => { ): { focus: number; gap: number } => {
const direction = startOrEnd === "start" ? -1 : 1; const direction = startOrEnd === "start" ? -1 : 1;
const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1; const edgePointIndex = direction === -1 ? 0 : linearElement.points.length - 1;
const adjacentPointIndex = edgePointIndex - direction; const adjacentPointIndex = edgePointIndex - direction;
const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const edgePoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
edgePointIndex, edgePointIndex,
elementsMap,
); );
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
adjacentPointIndex, adjacentPointIndex,
elementsMap,
); );
return { return {
focus: determineFocusDistance(hoveredElement, adjacentPoint, edgePoint), focus: determineFocusDistance(
gap: Math.max(1, distanceToBindableElement(hoveredElement, edgePoint)), hoveredElement,
adjacentPoint,
edgePoint,
elementsMap,
),
gap: Math.max(
1,
distanceToBindableElement(hoveredElement, edgePoint, elementsMap),
),
}; };
}; };
@@ -306,6 +364,8 @@ const calculateFocusAndGap = (
// in explicitly. // in explicitly.
export const updateBoundElements = ( export const updateBoundElements = (
changedElement: NonDeletedExcalidrawElement, changedElement: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
options?: { options?: {
simultaneouslyUpdated?: readonly ExcalidrawElement[]; simultaneouslyUpdated?: readonly ExcalidrawElement[];
newSize?: { width: number; height: number }; newSize?: { width: number; height: number };
@@ -355,12 +415,14 @@ export const updateBoundElements = (
"start", "start",
startBinding, startBinding,
changedElement as ExcalidrawBindableElement, changedElement as ExcalidrawBindableElement,
elementsMap,
); );
updateBoundPoint( updateBoundPoint(
element, element,
"end", "end",
endBinding, endBinding,
changedElement as ExcalidrawBindableElement, changedElement as ExcalidrawBindableElement,
elementsMap,
); );
const boundText = getBoundTextElement( const boundText = getBoundTextElement(
element, element,
@@ -393,6 +455,7 @@ const updateBoundPoint = (
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
binding: PointBinding | null | undefined, binding: PointBinding | null | undefined,
changedElement: ExcalidrawBindableElement, changedElement: ExcalidrawBindableElement,
elementsMap: ElementsMap,
): void => { ): void => {
if ( if (
binding == null || binding == null ||
@@ -414,11 +477,13 @@ const updateBoundPoint = (
const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates( const adjacentPoint = LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement, linearElement,
adjacentPointIndex, adjacentPointIndex,
elementsMap,
); );
const focusPointAbsolute = determineFocusPoint( const focusPointAbsolute = determineFocusPoint(
bindingElement, bindingElement,
binding.focus, binding.focus,
adjacentPoint, adjacentPoint,
elementsMap,
); );
let newEdgePoint; let newEdgePoint;
// The linear element was not originally pointing inside the bound shape, // The linear element was not originally pointing inside the bound shape,
@@ -431,6 +496,7 @@ const updateBoundPoint = (
adjacentPoint, adjacentPoint,
focusPointAbsolute, focusPointAbsolute,
binding.gap, binding.gap,
elementsMap,
); );
if (intersections.length === 0) { if (intersections.length === 0) {
// This should never happen, since focusPoint should always be // This should never happen, since focusPoint should always be
@@ -449,6 +515,7 @@ const updateBoundPoint = (
point: LinearElementEditor.pointFromAbsoluteCoords( point: LinearElementEditor.pointFromAbsoluteCoords(
linearElement, linearElement,
newEdgePoint, newEdgePoint,
elementsMap,
), ),
}, },
], ],
@@ -479,30 +546,47 @@ const maybeCalculateNewGapWhenScaling = (
// TODO: this is a bottleneck, optimise // TODO: this is a bottleneck, optimise
export const getEligibleElementsForBinding = ( export const getEligibleElementsForBinding = (
elements: NonDeleted<ExcalidrawElement>[], selectedElements: NonDeleted<ExcalidrawElement>[],
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): SuggestedBinding[] => { ): SuggestedBinding[] => {
const includedElementIds = new Set(elements.map(({ id }) => id)); const includedElementIds = new Set(selectedElements.map(({ id }) => id));
return elements.flatMap((element) => return selectedElements.flatMap((selectedElement) =>
isBindingElement(element, false) isBindingElement(selectedElement, false)
? (getElligibleElementsForBindingElement( ? (getElligibleElementsForBindingElement(
element as NonDeleted<ExcalidrawLinearElement>, selectedElement as NonDeleted<ExcalidrawLinearElement>,
elements,
elementsMap,
).filter( ).filter(
(element) => !includedElementIds.has(element.id), (element) => !includedElementIds.has(element.id),
) as SuggestedBinding[]) ) as SuggestedBinding[])
: isBindableElement(element, false) : isBindableElement(selectedElement, false)
? getElligibleElementsForBindableElementAndWhere(element).filter( ? getElligibleElementsForBindableElementAndWhere(
(binding) => !includedElementIds.has(binding[0].id), selectedElement,
) elementsMap,
).filter((binding) => !includedElementIds.has(binding[0].id))
: [], : [],
); );
}; };
const getElligibleElementsForBindingElement = ( const getElligibleElementsForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): NonDeleted<ExcalidrawBindableElement>[] => { ): NonDeleted<ExcalidrawBindableElement>[] => {
return [ return [
getElligibleElementForBindingElement(linearElement, "start"), getElligibleElementForBindingElement(
getElligibleElementForBindingElement(linearElement, "end"), linearElement,
"start",
elements,
elementsMap,
),
getElligibleElementForBindingElement(
linearElement,
"end",
elements,
elementsMap,
),
].filter( ].filter(
(element): element is NonDeleted<ExcalidrawBindableElement> => (element): element is NonDeleted<ExcalidrawBindableElement> =>
element != null, element != null,
@@ -512,27 +596,37 @@ const getElligibleElementsForBindingElement = (
const getElligibleElementForBindingElement = ( const getElligibleElementForBindingElement = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elements: readonly ExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): NonDeleted<ExcalidrawBindableElement> | null => { ): NonDeleted<ExcalidrawBindableElement> | null => {
return getHoveredElementForBinding( return getHoveredElementForBinding(
getLinearElementEdgeCoors(linearElement, startOrEnd), getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
Scene.getScene(linearElement)!, elements,
elementsMap,
); );
}; };
const getLinearElementEdgeCoors = ( const getLinearElementEdgeCoors = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
elementsMap: NonDeletedSceneElementsMap,
): { x: number; y: number } => { ): { x: number; y: number } => {
const index = startOrEnd === "start" ? 0 : -1; const index = startOrEnd === "start" ? 0 : -1;
return tupleToCoors( return tupleToCoors(
LinearElementEditor.getPointAtIndexGlobalCoordinates(linearElement, index), LinearElementEditor.getPointAtIndexGlobalCoordinates(
linearElement,
index,
elementsMap,
),
); );
}; };
const getElligibleElementsForBindableElementAndWhere = ( const getElligibleElementsForBindableElementAndWhere = (
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: NonDeletedSceneElementsMap,
): SuggestedPointBinding[] => { ): SuggestedPointBinding[] => {
return Scene.getScene(bindableElement)! const scene = Scene.getScene(bindableElement)!;
return scene
.getNonDeletedElements() .getNonDeletedElements()
.map((element) => { .map((element) => {
if (!isBindingElement(element, false)) { if (!isBindingElement(element, false)) {
@@ -542,11 +636,13 @@ const getElligibleElementsForBindableElementAndWhere = (
element, element,
"start", "start",
bindableElement, bindableElement,
elementsMap,
); );
const canBindEnd = isLinearElementEligibleForNewBindingByBindable( const canBindEnd = isLinearElementEligibleForNewBindingByBindable(
element, element,
"end", "end",
bindableElement, bindableElement,
elementsMap,
); );
if (!canBindStart && !canBindEnd) { if (!canBindStart && !canBindEnd) {
return null; return null;
@@ -564,6 +660,7 @@ const isLinearElementEligibleForNewBindingByBindable = (
linearElement: NonDeleted<ExcalidrawLinearElement>, linearElement: NonDeleted<ExcalidrawLinearElement>,
startOrEnd: "start" | "end", startOrEnd: "start" | "end",
bindableElement: NonDeleted<ExcalidrawBindableElement>, bindableElement: NonDeleted<ExcalidrawBindableElement>,
elementsMap: NonDeletedSceneElementsMap,
): boolean => { ): boolean => {
const existingBinding = const existingBinding =
linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"]; linearElement[startOrEnd === "start" ? "startBinding" : "endBinding"];
@@ -576,7 +673,8 @@ const isLinearElementEligibleForNewBindingByBindable = (
) && ) &&
bindingBorderTest( bindingBorderTest(
bindableElement, bindableElement,
getLinearElementEdgeCoors(linearElement, startOrEnd), getLinearElementEdgeCoors(linearElement, startOrEnd, elementsMap),
elementsMap,
) )
); );
}; };
+43 -19
View File
@@ -1,4 +1,5 @@
import { ROUNDNESS } from "../constants"; import { ROUNDNESS } from "../constants";
import { arrayToMap } from "../utils";
import { getElementAbsoluteCoords, getElementBounds } from "./bounds"; import { getElementAbsoluteCoords, getElementBounds } from "./bounds";
import { ExcalidrawElement, ExcalidrawLinearElement } from "./types"; import { ExcalidrawElement, ExcalidrawLinearElement } from "./types";
@@ -35,35 +36,41 @@ const _ce = ({
describe("getElementAbsoluteCoords", () => { describe("getElementAbsoluteCoords", () => {
it("test x1 coordinate", () => { it("test x1 coordinate", () => {
const [x1] = getElementAbsoluteCoords(_ce({ x: 10, y: 0, w: 10, h: 0 })); const element = _ce({ x: 10, y: 20, w: 10, h: 0 });
const [x1] = getElementAbsoluteCoords(element, arrayToMap([element]));
expect(x1).toEqual(10); expect(x1).toEqual(10);
}); });
it("test x2 coordinate", () => { it("test x2 coordinate", () => {
const [, , x2] = getElementAbsoluteCoords( const element = _ce({ x: 10, y: 20, w: 10, h: 0 });
_ce({ x: 10, y: 0, w: 10, h: 0 }), const [, , x2] = getElementAbsoluteCoords(element, arrayToMap([element]));
);
expect(x2).toEqual(20); expect(x2).toEqual(20);
}); });
it("test y1 coordinate", () => { it("test y1 coordinate", () => {
const [, y1] = getElementAbsoluteCoords(_ce({ x: 0, y: 10, w: 0, h: 10 })); const element = _ce({ x: 0, y: 10, w: 0, h: 10 });
const [, y1] = getElementAbsoluteCoords(element, arrayToMap([element]));
expect(y1).toEqual(10); expect(y1).toEqual(10);
}); });
it("test y2 coordinate", () => { it("test y2 coordinate", () => {
const [, , , y2] = getElementAbsoluteCoords( const element = _ce({ x: 0, y: 10, w: 0, h: 10 });
_ce({ x: 0, y: 10, w: 0, h: 10 }), const [, , , y2] = getElementAbsoluteCoords(element, arrayToMap([element]));
);
expect(y2).toEqual(20); expect(y2).toEqual(20);
}); });
}); });
describe("getElementBounds", () => { describe("getElementBounds", () => {
it("rectangle", () => { it("rectangle", () => {
const [x1, y1, x2, y2] = getElementBounds( const element = _ce({
_ce({ x: 40, y: 30, w: 20, h: 10, a: Math.PI / 4, t: "rectangle" }), x: 40,
); y: 30,
w: 20,
h: 10,
a: Math.PI / 4,
t: "rectangle",
});
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
expect(x1).toEqual(39.39339828220179); expect(x1).toEqual(39.39339828220179);
expect(y1).toEqual(24.393398282201787); expect(y1).toEqual(24.393398282201787);
expect(x2).toEqual(60.60660171779821); expect(x2).toEqual(60.60660171779821);
@@ -71,9 +78,17 @@ describe("getElementBounds", () => {
}); });
it("diamond", () => { it("diamond", () => {
const [x1, y1, x2, y2] = getElementBounds( const element = _ce({
_ce({ x: 40, y: 30, w: 20, h: 10, a: Math.PI / 4, t: "diamond" }), x: 40,
); y: 30,
w: 20,
h: 10,
a: Math.PI / 4,
t: "diamond",
});
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
expect(x1).toEqual(42.928932188134524); expect(x1).toEqual(42.928932188134524);
expect(y1).toEqual(27.928932188134524); expect(y1).toEqual(27.928932188134524);
expect(x2).toEqual(57.071067811865476); expect(x2).toEqual(57.071067811865476);
@@ -81,9 +96,16 @@ describe("getElementBounds", () => {
}); });
it("ellipse", () => { it("ellipse", () => {
const [x1, y1, x2, y2] = getElementBounds( const element = _ce({
_ce({ x: 40, y: 30, w: 20, h: 10, a: Math.PI / 4, t: "ellipse" }), x: 40,
); y: 30,
w: 20,
h: 10,
a: Math.PI / 4,
t: "ellipse",
});
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
expect(x1).toEqual(42.09430584957905); expect(x1).toEqual(42.09430584957905);
expect(y1).toEqual(27.09430584957905); expect(y1).toEqual(27.09430584957905);
expect(x2).toEqual(57.90569415042095); expect(x2).toEqual(57.90569415042095);
@@ -91,7 +113,7 @@ describe("getElementBounds", () => {
}); });
it("curved line", () => { it("curved line", () => {
const [x1, y1, x2, y2] = getElementBounds({ const element = {
..._ce({ ..._ce({
t: "line", t: "line",
x: 449.58203125, x: 449.58203125,
@@ -105,7 +127,9 @@ describe("getElementBounds", () => {
[67.33984375, 92.48828125] as [number, number], [67.33984375, 92.48828125] as [number, number],
[-102.7890625, 52.15625] as [number, number], [-102.7890625, 52.15625] as [number, number],
], ],
} as ExcalidrawLinearElement); } as ExcalidrawLinearElement;
const [x1, y1, x2, y2] = getElementBounds(element, arrayToMap([element]));
expect(x1).toEqual(360.3176068760539); expect(x1).toEqual(360.3176068760539);
expect(y1).toEqual(185.90654264413516); expect(y1).toEqual(185.90654264413516);
expect(x2).toEqual(480.87005902729743); expect(x2).toEqual(480.87005902729743);
+36 -32
View File
@@ -5,7 +5,6 @@ import {
ExcalidrawFreeDrawElement, ExcalidrawFreeDrawElement,
NonDeleted, NonDeleted,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ElementsMapOrArray,
ElementsMap, ElementsMap,
} from "./types"; } from "./types";
import { distance2d, rotate, rotatePoint } from "../math"; import { distance2d, rotate, rotatePoint } from "../math";
@@ -25,7 +24,7 @@ import { getBoundTextElement, getContainerElement } from "./textElement";
import { LinearElementEditor } from "./linearElementEditor"; import { LinearElementEditor } from "./linearElementEditor";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
import { ShapeCache } from "../scene/ShapeCache"; import { ShapeCache } from "../scene/ShapeCache";
import Scene from "../scene/Scene"; import { arrayToMap } from "../utils";
export type RectangleBox = { export type RectangleBox = {
x: number; x: number;
@@ -63,7 +62,7 @@ export class ElementBounds {
} }
>(); >();
static getBounds(element: ExcalidrawElement) { static getBounds(element: ExcalidrawElement, elementsMap: ElementsMap) {
const cachedBounds = ElementBounds.boundsCache.get(element); const cachedBounds = ElementBounds.boundsCache.get(element);
if ( if (
@@ -75,23 +74,12 @@ export class ElementBounds {
) { ) {
return cachedBounds.bounds; return cachedBounds.bounds;
} }
const scene = Scene.getScene(element); const bounds = ElementBounds.calculateBounds(element, elementsMap);
const bounds = ElementBounds.calculateBounds(
element,
scene?.getNonDeletedElementsMap() || new Map(),
);
// hack to ensure that downstream checks could retrieve element Scene ElementBounds.boundsCache.set(element, {
// so as to have correctly calculated bounds version: element.version,
// FIXME remove when we get rid of all the id:Scene / element:Scene mapping bounds,
const shouldCache = !!scene; });
if (shouldCache) {
ElementBounds.boundsCache.set(element, {
version: element.version,
bounds,
});
}
return bounds; return bounds;
} }
@@ -102,8 +90,10 @@ export class ElementBounds {
): Bounds { ): Bounds {
let bounds: Bounds; let bounds: Bounds;
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
const [minX, minY, maxX, maxY] = getBoundsFromPoints( const [minX, minY, maxX, maxY] = getBoundsFromPoints(
element.points.map(([x, y]) => element.points.map(([x, y]) =>
@@ -159,10 +149,9 @@ export class ElementBounds {
// This set of functions retrieves the absolute position of the 4 points. // This set of functions retrieves the absolute position of the 4 points.
export const getElementAbsoluteCoords = ( export const getElementAbsoluteCoords = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
includeBoundText: boolean = false, includeBoundText: boolean = false,
): [number, number, number, number, number, number] => { ): [number, number, number, number, number, number] => {
const elementsMap =
Scene.getScene(element)?.getElementsMapIncludingDeleted() || new Map();
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
return getFreeDrawElementAbsoluteCoords(element); return getFreeDrawElementAbsoluteCoords(element);
} else if (isLinearElement(element)) { } else if (isLinearElement(element)) {
@@ -179,6 +168,7 @@ export const getElementAbsoluteCoords = (
const coords = LinearElementEditor.getBoundTextElementPosition( const coords = LinearElementEditor.getBoundTextElementPosition(
container, container,
element as ExcalidrawTextElementWithContainer, element as ExcalidrawTextElementWithContainer,
elementsMap,
); );
return [ return [
coords.x, coords.x,
@@ -207,8 +197,12 @@ export const getElementAbsoluteCoords = (
*/ */
export const getElementLineSegments = ( export const getElementLineSegments = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
): [Point, Point][] => { ): [Point, Point][] => {
const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
const center: Point = [cx, cy]; const center: Point = [cx, cy];
@@ -703,6 +697,7 @@ const getLinearElementRotatedBounds = (
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap,
[x, y, x, y], [x, y, x, y],
boundTextElement, boundTextElement,
); );
@@ -727,6 +722,7 @@ const getLinearElementRotatedBounds = (
if (boundTextElement) { if (boundTextElement) {
const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText( const coordsWithBoundText = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap,
coords, coords,
boundTextElement, boundTextElement,
); );
@@ -740,11 +736,17 @@ const getLinearElementRotatedBounds = (
return coords; return coords;
}; };
export const getElementBounds = (element: ExcalidrawElement): Bounds => { export const getElementBounds = (
return ElementBounds.getBounds(element); element: ExcalidrawElement,
elementsMap: ElementsMap,
): Bounds => {
return ElementBounds.getBounds(element, elementsMap);
}; };
export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
if ("size" in elements ? !elements.size : !elements.length) { export const getCommonBounds = (
elements: readonly ExcalidrawElement[],
): Bounds => {
if (!elements.length) {
return [0, 0, 0, 0]; return [0, 0, 0, 0];
} }
@@ -753,8 +755,10 @@ export const getCommonBounds = (elements: ElementsMapOrArray): Bounds => {
let minY = Infinity; let minY = Infinity;
let maxY = -Infinity; let maxY = -Infinity;
const elementsMap = arrayToMap(elements);
elements.forEach((element) => { elements.forEach((element) => {
const [x1, y1, x2, y2] = getElementBounds(element); const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
minX = Math.min(minX, x1); minX = Math.min(minX, x1);
minY = Math.min(minY, y1); minY = Math.min(minY, y1);
maxX = Math.max(maxX, x2); maxX = Math.max(maxX, x2);
@@ -860,9 +864,9 @@ export const getClosestElementBounds = (
let minDistance = Infinity; let minDistance = Infinity;
let closestElement = elements[0]; let closestElement = elements[0];
const elementsMap = arrayToMap(elements);
elements.forEach((element) => { elements.forEach((element) => {
const [x1, y1, x2, y2] = getElementBounds(element); const [x1, y1, x2, y2] = getElementBounds(element, elementsMap);
const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y); const distance = distance2d((x1 + x2) / 2, (y1 + y2) / 2, from.x, from.y);
if (distance < minDistance) { if (distance < minDistance) {
@@ -871,7 +875,7 @@ export const getClosestElementBounds = (
} }
}); });
return getElementBounds(closestElement); return getElementBounds(closestElement, elementsMap);
}; };
export interface BoundingBox { export interface BoundingBox {
+70 -20
View File
@@ -91,6 +91,7 @@ export const hitTest = (
) { ) {
return isPointHittingElementBoundingBox( return isPointHittingElementBoundingBox(
element, element,
elementsMap,
point, point,
threshold, threshold,
frameNameBoundsCache, frameNameBoundsCache,
@@ -116,6 +117,7 @@ export const hitTest = (
appState, appState,
frameNameBoundsCache, frameNameBoundsCache,
point, point,
elementsMap,
); );
}; };
@@ -145,9 +147,11 @@ export const isHittingElementBoundingBoxWithoutHittingElement = (
appState, appState,
frameNameBoundsCache, frameNameBoundsCache,
[x, y], [x, y],
elementsMap,
) && ) &&
isPointHittingElementBoundingBox( isPointHittingElementBoundingBox(
element, element,
elementsMap,
[x, y], [x, y],
threshold, threshold,
frameNameBoundsCache, frameNameBoundsCache,
@@ -160,6 +164,7 @@ export const isHittingElementNotConsideringBoundingBox = (
appState: AppState, appState: AppState,
frameNameBoundsCache: FrameNameBoundsCache | null, frameNameBoundsCache: FrameNameBoundsCache | null,
point: Point, point: Point,
elementsMap: ElementsMap,
): boolean => { ): boolean => {
const threshold = 10 / appState.zoom.value; const threshold = 10 / appState.zoom.value;
const check = isTextElement(element) const check = isTextElement(element)
@@ -169,6 +174,7 @@ export const isHittingElementNotConsideringBoundingBox = (
: isNearCheck; : isNearCheck;
return hitTestPointAgainstElement({ return hitTestPointAgainstElement({
element, element,
elementsMap,
point, point,
threshold, threshold,
check, check,
@@ -183,6 +189,7 @@ const isElementSelected = (
export const isPointHittingElementBoundingBox = ( export const isPointHittingElementBoundingBox = (
element: NonDeleted<ExcalidrawElement>, element: NonDeleted<ExcalidrawElement>,
elementsMap: ElementsMap,
[x, y]: Point, [x, y]: Point,
threshold: number, threshold: number,
frameNameBoundsCache: FrameNameBoundsCache | null, frameNameBoundsCache: FrameNameBoundsCache | null,
@@ -194,6 +201,7 @@ export const isPointHittingElementBoundingBox = (
if (isFrameLikeElement(element)) { if (isFrameLikeElement(element)) {
return hitTestPointAgainstElement({ return hitTestPointAgainstElement({
element, element,
elementsMap,
point: [x, y], point: [x, y],
threshold, threshold,
check: isInsideCheck, check: isInsideCheck,
@@ -201,7 +209,7 @@ export const isPointHittingElementBoundingBox = (
}); });
} }
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const elementCenterX = (x1 + x2) / 2; const elementCenterX = (x1 + x2) / 2;
const elementCenterY = (y1 + y2) / 2; const elementCenterY = (y1 + y2) / 2;
// reverse rotate to take element's angle into account. // reverse rotate to take element's angle into account.
@@ -224,12 +232,14 @@ export const isPointHittingElementBoundingBox = (
export const bindingBorderTest = ( export const bindingBorderTest = (
element: NonDeleted<ExcalidrawBindableElement>, element: NonDeleted<ExcalidrawBindableElement>,
{ x, y }: { x: number; y: number }, { x, y }: { x: number; y: number },
elementsMap: ElementsMap,
): boolean => { ): boolean => {
const threshold = maxBindingGap(element, element.width, element.height); const threshold = maxBindingGap(element, element.width, element.height);
const check = isOutsideCheck; const check = isOutsideCheck;
const point: Point = [x, y]; const point: Point = [x, y];
return hitTestPointAgainstElement({ return hitTestPointAgainstElement({
element, element,
elementsMap,
point, point,
threshold, threshold,
check, check,
@@ -251,6 +261,7 @@ export const maxBindingGap = (
type HitTestArgs = { type HitTestArgs = {
element: NonDeletedExcalidrawElement; element: NonDeletedExcalidrawElement;
elementsMap: ElementsMap;
point: Point; point: Point;
threshold: number; threshold: number;
check: (distance: number, threshold: number) => boolean; check: (distance: number, threshold: number) => boolean;
@@ -266,19 +277,28 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
case "text": case "text":
case "diamond": case "diamond":
case "ellipse": case "ellipse":
const distance = distanceToBindableElement(args.element, args.point); const distance = distanceToBindableElement(
args.element,
args.point,
args.elementsMap,
);
return args.check(distance, args.threshold); return args.check(distance, args.threshold);
case "freedraw": { case "freedraw": {
if ( if (
!args.check( !args.check(
distanceToRectangle(args.element, args.point), distanceToRectangle(args.element, args.point, args.elementsMap),
args.threshold, args.threshold,
) )
) { ) {
return false; return false;
} }
return hitTestFreeDrawElement(args.element, args.point, args.threshold); return hitTestFreeDrawElement(
args.element,
args.point,
args.threshold,
args.elementsMap,
);
} }
case "arrow": case "arrow":
case "line": case "line":
@@ -293,7 +313,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
// check distance to frame element first // check distance to frame element first
if ( if (
args.check( args.check(
distanceToBindableElement(args.element, args.point), distanceToBindableElement(args.element, args.point, args.elementsMap),
args.threshold, args.threshold,
) )
) { ) {
@@ -316,6 +336,7 @@ const hitTestPointAgainstElement = (args: HitTestArgs): boolean => {
export const distanceToBindableElement = ( export const distanceToBindableElement = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
switch (element.type) { switch (element.type) {
case "rectangle": case "rectangle":
@@ -325,11 +346,11 @@ export const distanceToBindableElement = (
case "embeddable": case "embeddable":
case "frame": case "frame":
case "magicframe": case "magicframe":
return distanceToRectangle(element, point); return distanceToRectangle(element, point, elementsMap);
case "diamond": case "diamond":
return distanceToDiamond(element, point); return distanceToDiamond(element, point, elementsMap);
case "ellipse": case "ellipse":
return distanceToEllipse(element, point); return distanceToEllipse(element, point, elementsMap);
} }
}; };
@@ -358,8 +379,13 @@ const distanceToRectangle = (
| ExcalidrawIframeLikeElement | ExcalidrawIframeLikeElement
| ExcalidrawFrameLikeElement, | ExcalidrawFrameLikeElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); const [, pointRel, hwidth, hheight] = pointRelativeToElement(
element,
point,
elementsMap,
);
return Math.max( return Math.max(
GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)), GAPoint.distanceToLine(pointRel, GALine.equation(0, 1, -hheight)),
GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)), GAPoint.distanceToLine(pointRel, GALine.equation(1, 0, -hwidth)),
@@ -377,8 +403,13 @@ const distanceToRectangleBox = (box: RectangleBox, point: Point): number => {
const distanceToDiamond = ( const distanceToDiamond = (
element: ExcalidrawDiamondElement, element: ExcalidrawDiamondElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); const [, pointRel, hwidth, hheight] = pointRelativeToElement(
element,
point,
elementsMap,
);
const side = GALine.equation(hheight, hwidth, -hheight * hwidth); const side = GALine.equation(hheight, hwidth, -hheight * hwidth);
return GAPoint.distanceToLine(pointRel, side); return GAPoint.distanceToLine(pointRel, side);
}; };
@@ -386,16 +417,22 @@ const distanceToDiamond = (
const distanceToEllipse = ( const distanceToEllipse = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const [pointRel, tangent] = ellipseParamsForTest(element, point); const [pointRel, tangent] = ellipseParamsForTest(element, point, elementsMap);
return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent); return -GALine.sign(tangent) * GAPoint.distanceToLine(pointRel, tangent);
}; };
const ellipseParamsForTest = ( const ellipseParamsForTest = (
element: ExcalidrawEllipseElement, element: ExcalidrawEllipseElement,
point: Point, point: Point,
elementsMap: ElementsMap,
): [GA.Point, GA.Line] => { ): [GA.Point, GA.Line] => {
const [, pointRel, hwidth, hheight] = pointRelativeToElement(element, point); const [, pointRel, hwidth, hheight] = pointRelativeToElement(
element,
point,
elementsMap,
);
const [px, py] = GAPoint.toTuple(pointRel); const [px, py] = GAPoint.toTuple(pointRel);
// We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)` // We're working in positive quadrant, so start with `t = 45deg`, `tx=cos(t)`
@@ -440,6 +477,7 @@ const hitTestFreeDrawElement = (
element: ExcalidrawFreeDrawElement, element: ExcalidrawFreeDrawElement,
point: Point, point: Point,
threshold: number, threshold: number,
elementsMap: ElementsMap,
): boolean => { ): boolean => {
// Check point-distance-to-line-segment for every segment in the // Check point-distance-to-line-segment for every segment in the
// element's points (its input points, not its outline points). // element's points (its input points, not its outline points).
@@ -454,7 +492,10 @@ const hitTestFreeDrawElement = (
y = point[1] - element.y; y = point[1] - element.y;
} else { } else {
// Counter-rotate the point around center before testing // Counter-rotate the point around center before testing
const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(element); const [minX, minY, maxX, maxY] = getElementAbsoluteCoords(
element,
elementsMap,
);
const rotatedPoint = rotatePoint( const rotatedPoint = rotatePoint(
point, point,
[minX + (maxX - minX) / 2, minY + (maxY - minY) / 2], [minX + (maxX - minX) / 2, minY + (maxY - minY) / 2],
@@ -520,6 +561,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
const [point, pointAbs, hwidth, hheight] = pointRelativeToElement( const [point, pointAbs, hwidth, hheight] = pointRelativeToElement(
args.element, args.element,
args.point, args.point,
args.elementsMap,
); );
const side1 = GALine.equation(0, 1, -hheight); const side1 = GALine.equation(0, 1, -hheight);
const side2 = GALine.equation(1, 0, -hwidth); const side2 = GALine.equation(1, 0, -hwidth);
@@ -572,9 +614,10 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
const pointRelativeToElement = ( const pointRelativeToElement = (
element: ExcalidrawElement, element: ExcalidrawElement,
pointTuple: Point, pointTuple: Point,
elementsMap: ElementsMap,
): [GA.Point, GA.Point, number, number] => { ): [GA.Point, GA.Point, number, number] => {
const point = GAPoint.from(pointTuple); const point = GAPoint.from(pointTuple);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const center = coordsCenter(x1, y1, x2, y2); const center = coordsCenter(x1, y1, x2, y2);
// GA has angle orientation opposite to `rotate` // GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle); const rotate = GATransform.rotation(center, element.angle);
@@ -609,11 +652,12 @@ const pointRelativeToDivElement = (
// Returns point in absolute coordinates // Returns point in absolute coordinates
export const pointInAbsoluteCoords = ( export const pointInAbsoluteCoords = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
// Point relative to the element position // Point relative to the element position
point: Point, point: Point,
): Point => { ): Point => {
const [x, y] = point; const [x, y] = point;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x2 - x1) / 2; const cx = (x2 - x1) / 2;
const cy = (y2 - y1) / 2; const cy = (y2 - y1) / 2;
const [rotatedX, rotatedY] = rotate(x, y, cx, cy, element.angle); const [rotatedX, rotatedY] = rotate(x, y, cx, cy, element.angle);
@@ -622,8 +666,9 @@ export const pointInAbsoluteCoords = (
const relativizationToElementCenter = ( const relativizationToElementCenter = (
element: ExcalidrawElement, element: ExcalidrawElement,
elementsMap: ElementsMap,
): GA.Transform => { ): GA.Transform => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const center = coordsCenter(x1, y1, x2, y2); const center = coordsCenter(x1, y1, x2, y2);
// GA has angle orientation opposite to `rotate` // GA has angle orientation opposite to `rotate`
const rotate = GATransform.rotation(center, element.angle); const rotate = GATransform.rotation(center, element.angle);
@@ -649,12 +694,14 @@ const coordsCenter = (
// of the element. // of the element.
export const determineFocusDistance = ( export const determineFocusDistance = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates // Point on the line, in absolute coordinates
a: Point, a: Point,
// Another point on the line, in absolute coordinates (closer to element) // Another point on the line, in absolute coordinates (closer to element)
b: Point, b: Point,
elementsMap: ElementsMap,
): number => { ): number => {
const relateToCenter = relativizationToElementCenter(element); const relateToCenter = relativizationToElementCenter(element, elementsMap);
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a)); const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b)); const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
const line = GALine.through(aRel, bRel); const line = GALine.through(aRel, bRel);
@@ -693,13 +740,14 @@ export const determineFocusPoint = (
// returned focusPoint // returned focusPoint
focus: number, focus: number,
adjecentPoint: Point, adjecentPoint: Point,
elementsMap: ElementsMap,
): Point => { ): Point => {
if (focus === 0) { if (focus === 0) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const center = coordsCenter(x1, y1, x2, y2); const center = coordsCenter(x1, y1, x2, y2);
return GAPoint.toTuple(center); return GAPoint.toTuple(center);
} }
const relateToCenter = relativizationToElementCenter(element); const relateToCenter = relativizationToElementCenter(element, elementsMap);
const adjecentPointRel = GATransform.apply( const adjecentPointRel = GATransform.apply(
relateToCenter, relateToCenter,
GAPoint.from(adjecentPoint), GAPoint.from(adjecentPoint),
@@ -728,14 +776,16 @@ export const determineFocusPoint = (
// and the `element`, in ascending order of distance from `a`. // and the `element`, in ascending order of distance from `a`.
export const intersectElementWithLine = ( export const intersectElementWithLine = (
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
// Point on the line, in absolute coordinates // Point on the line, in absolute coordinates
a: Point, a: Point,
// Another point on the line, in absolute coordinates // Another point on the line, in absolute coordinates
b: Point, b: Point,
// If given, the element is inflated by this value // If given, the element is inflated by this value
gap: number = 0, gap: number = 0,
elementsMap: ElementsMap,
): Point[] => { ): Point[] => {
const relateToCenter = relativizationToElementCenter(element); const relateToCenter = relativizationToElementCenter(element, elementsMap);
const aRel = GATransform.apply(relateToCenter, GAPoint.from(a)); const aRel = GATransform.apply(relateToCenter, GAPoint.from(a));
const bRel = GATransform.apply(relateToCenter, GAPoint.from(b)); const bRel = GATransform.apply(relateToCenter, GAPoint.from(b));
const line = GALine.through(aRel, bRel); const line = GALine.through(aRel, bRel);
+1 -1
View File
@@ -65,7 +65,7 @@ export const dragSelectedElements = (
updateElementCoords(pointerDownState, textElement, adjustedOffset); updateElementCoords(pointerDownState, textElement, adjustedOffset);
} }
} }
updateBoundElements(element, { updateBoundElements(element, scene.getElementsMapIncludingDeleted(), {
simultaneouslyUpdated: Array.from(elementsToUpdate), simultaneouslyUpdated: Array.from(elementsToUpdate),
}); });
}); });
@@ -6,6 +6,8 @@ import {
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ElementsMap, ElementsMap,
NonDeletedExcalidrawElement,
NonDeletedSceneElementsMap,
} from "./types"; } from "./types";
import { import {
distance2d, distance2d,
@@ -36,7 +38,6 @@ import {
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import History from "../history"; import History from "../history";
import Scene from "../scene/Scene";
import { import {
bindOrUnbindLinearElement, bindOrUnbindLinearElement,
getHoveredElementForBinding, getHoveredElementForBinding,
@@ -86,11 +87,10 @@ export class LinearElementEditor {
public readonly hoverPointIndex: number; public readonly hoverPointIndex: number;
public readonly segmentMidPointHoveredCoords: Point | null; public readonly segmentMidPointHoveredCoords: Point | null;
constructor(element: NonDeleted<ExcalidrawLinearElement>, scene: Scene) { constructor(element: NonDeleted<ExcalidrawLinearElement>) {
this.elementId = element.id as string & { this.elementId = element.id as string & {
_brand: "excalidrawLinearElementId"; _brand: "excalidrawLinearElementId";
}; };
Scene.mapElementToScene(this.elementId, scene);
LinearElementEditor.normalizePoints(element); LinearElementEditor.normalizePoints(element);
this.selectedPointsIndices = null; this.selectedPointsIndices = null;
@@ -123,8 +123,11 @@ export class LinearElementEditor {
* @param id the `elementId` from the instance of this class (so that we can * @param id the `elementId` from the instance of this class (so that we can
* statically guarantee this method returns an ExcalidrawLinearElement) * statically guarantee this method returns an ExcalidrawLinearElement)
*/ */
static getElement(id: InstanceType<typeof LinearElementEditor>["elementId"]) { static getElement(
const element = Scene.getScene(id)?.getNonDeletedElement(id); id: InstanceType<typeof LinearElementEditor>["elementId"],
elementsMap: ElementsMap,
) {
const element = elementsMap.get(id);
if (element) { if (element) {
return element as NonDeleted<ExcalidrawLinearElement>; return element as NonDeleted<ExcalidrawLinearElement>;
} }
@@ -135,6 +138,7 @@ export class LinearElementEditor {
event: PointerEvent, event: PointerEvent,
appState: AppState, appState: AppState,
setState: React.Component<any, AppState>["setState"], setState: React.Component<any, AppState>["setState"],
elementsMap: NonDeletedSceneElementsMap,
) { ) {
if ( if (
!appState.editingLinearElement || !appState.editingLinearElement ||
@@ -145,16 +149,18 @@ export class LinearElementEditor {
const { editingLinearElement } = appState; const { editingLinearElement } = appState;
const { selectedPointsIndices, elementId } = editingLinearElement; const { selectedPointsIndices, elementId } = editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return false; return false;
} }
const [selectionX1, selectionY1, selectionX2, selectionY2] = const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(appState.draggingElement); getElementAbsoluteCoords(appState.draggingElement, elementsMap);
const pointsSceneCoords = const pointsSceneCoords = LinearElementEditor.getPointsGlobalCoordinates(
LinearElementEditor.getPointsGlobalCoordinates(element); element,
elementsMap,
);
const nextSelectedPoints = pointsSceneCoords.reduce( const nextSelectedPoints = pointsSceneCoords.reduce(
(acc: number[], point, index) => { (acc: number[], point, index) => {
@@ -194,13 +200,13 @@ export class LinearElementEditor {
pointSceneCoords: { x: number; y: number }[], pointSceneCoords: { x: number; y: number }[],
) => void, ) => void,
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
elementsMap: ElementsMap, elementsMap: NonDeletedSceneElementsMap,
): boolean { ): boolean {
if (!linearElementEditor) { if (!linearElementEditor) {
return false; return false;
} }
const { selectedPointsIndices, elementId } = linearElementEditor; const { selectedPointsIndices, elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return false; return false;
} }
@@ -222,6 +228,7 @@ export class LinearElementEditor {
const [width, height] = LinearElementEditor._getShiftLockedDelta( const [width, height] = LinearElementEditor._getShiftLockedDelta(
element, element,
elementsMap,
referencePoint, referencePoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -239,6 +246,7 @@ export class LinearElementEditor {
} else { } else {
const newDraggingPointPosition = LinearElementEditor.createPointAt( const newDraggingPointPosition = LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -255,6 +263,7 @@ export class LinearElementEditor {
linearElementEditor.pointerDownState.lastClickedPoint linearElementEditor.pointerDownState.lastClickedPoint
? LinearElementEditor.createPointAt( ? LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointerX - linearElementEditor.pointerOffset.x, scenePointerX - linearElementEditor.pointerOffset.x,
scenePointerY - linearElementEditor.pointerOffset.y, scenePointerY - linearElementEditor.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -290,6 +299,7 @@ export class LinearElementEditor {
LinearElementEditor.getPointGlobalCoordinates( LinearElementEditor.getPointGlobalCoordinates(
element, element,
element.points[0], element.points[0],
elementsMap,
), ),
), ),
); );
@@ -303,6 +313,7 @@ export class LinearElementEditor {
LinearElementEditor.getPointGlobalCoordinates( LinearElementEditor.getPointGlobalCoordinates(
element, element,
element.points[lastSelectedIndex], element.points[lastSelectedIndex],
elementsMap,
), ),
), ),
); );
@@ -323,10 +334,12 @@ export class LinearElementEditor {
event: PointerEvent, event: PointerEvent,
editingLinearElement: LinearElementEditor, editingLinearElement: LinearElementEditor,
appState: AppState, appState: AppState,
elements: readonly NonDeletedExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): LinearElementEditor { ): LinearElementEditor {
const { elementId, selectedPointsIndices, isDragging, pointerDownState } = const { elementId, selectedPointsIndices, isDragging, pointerDownState } =
editingLinearElement; editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return editingLinearElement; return editingLinearElement;
} }
@@ -364,9 +377,11 @@ export class LinearElementEditor {
LinearElementEditor.getPointAtIndexGlobalCoordinates( LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
selectedPoint!, selectedPoint!,
elementsMap,
), ),
), ),
Scene.getScene(element)!, elements,
elementsMap,
) )
: null; : null;
@@ -425,15 +440,23 @@ export class LinearElementEditor {
) { ) {
return editorMidPointsCache.points; return editorMidPointsCache.points;
} }
LinearElementEditor.updateEditorMidPointsCache(element, appState); LinearElementEditor.updateEditorMidPointsCache(
element,
elementsMap,
appState,
);
return editorMidPointsCache.points!; return editorMidPointsCache.points!;
}; };
static updateEditorMidPointsCache = ( static updateEditorMidPointsCache = (
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
) => { ) => {
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
let index = 0; let index = 0;
const midpoints: (Point | null)[] = []; const midpoints: (Point | null)[] = [];
@@ -455,6 +478,7 @@ export class LinearElementEditor {
points[index], points[index],
points[index + 1], points[index + 1],
index + 1, index + 1,
elementsMap,
); );
midpoints.push(segmentMidPoint); midpoints.push(segmentMidPoint);
index++; index++;
@@ -471,12 +495,13 @@ export class LinearElementEditor {
elementsMap: ElementsMap, elementsMap: ElementsMap,
) => { ) => {
const { elementId } = linearElementEditor; const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return null; return null;
} }
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element, element,
elementsMap,
appState.zoom, appState.zoom,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
@@ -484,7 +509,10 @@ export class LinearElementEditor {
if (clickedPointIndex >= 0) { if (clickedPointIndex >= 0) {
return null; return null;
} }
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
if (points.length >= 3 && !appState.editingLinearElement) { if (points.length >= 3 && !appState.editingLinearElement) {
return null; return null;
} }
@@ -550,6 +578,7 @@ export class LinearElementEditor {
startPoint: Point, startPoint: Point,
endPoint: Point, endPoint: Point,
endPointIndex: number, endPointIndex: number,
elementsMap: ElementsMap,
) { ) {
let segmentMidPoint = centerPoint(startPoint, endPoint); let segmentMidPoint = centerPoint(startPoint, endPoint);
if (element.points.length > 2 && element.roundness) { if (element.points.length > 2 && element.roundness) {
@@ -574,6 +603,7 @@ export class LinearElementEditor {
segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates( segmentMidPoint = LinearElementEditor.getPointGlobalCoordinates(
element, element,
[tx, ty], [tx, ty],
elementsMap,
); );
} }
} }
@@ -589,6 +619,7 @@ export class LinearElementEditor {
) { ) {
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
elementsMap,
); );
if (!element) { if (!element) {
return -1; return -1;
@@ -614,7 +645,8 @@ export class LinearElementEditor {
history: History, history: History,
scenePointer: { x: number; y: number }, scenePointer: { x: number; y: number },
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
elementsMap: ElementsMap, elements: readonly NonDeletedExcalidrawElement[],
elementsMap: NonDeletedSceneElementsMap,
): { ): {
didAddPoint: boolean; didAddPoint: boolean;
hitElement: NonDeleted<ExcalidrawElement> | null; hitElement: NonDeleted<ExcalidrawElement> | null;
@@ -631,7 +663,7 @@ export class LinearElementEditor {
} }
const { elementId } = linearElementEditor; const { elementId } = linearElementEditor;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return ret; return ret;
@@ -658,6 +690,7 @@ export class LinearElementEditor {
...element.points, ...element.points,
LinearElementEditor.createPointAt( LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -683,7 +716,8 @@ export class LinearElementEditor {
lastUncommittedPoint: null, lastUncommittedPoint: null,
endBindingElement: getHoveredElementForBinding( endBindingElement: getHoveredElementForBinding(
scenePointer, scenePointer,
Scene.getScene(element)!, elements,
elementsMap,
), ),
}; };
@@ -693,6 +727,7 @@ export class LinearElementEditor {
const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor( const clickedPointIndex = LinearElementEditor.getPointIndexUnderCursor(
element, element,
elementsMap,
appState.zoom, appState.zoom,
scenePointer.x, scenePointer.x,
scenePointer.y, scenePointer.y,
@@ -713,11 +748,12 @@ export class LinearElementEditor {
element, element,
startBindingElement, startBindingElement,
endBindingElement, endBindingElement,
elementsMap,
); );
} }
} }
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const targetPoint = const targetPoint =
@@ -779,12 +815,13 @@ export class LinearElementEditor {
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
): LinearElementEditor | null { ): LinearElementEditor | null {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return null; return null;
} }
const { elementId, lastUncommittedPoint } = appState.editingLinearElement; const { elementId, lastUncommittedPoint } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return appState.editingLinearElement; return appState.editingLinearElement;
} }
@@ -809,6 +846,7 @@ export class LinearElementEditor {
const [width, height] = LinearElementEditor._getShiftLockedDelta( const [width, height] = LinearElementEditor._getShiftLockedDelta(
element, element,
elementsMap,
lastCommittedPoint, lastCommittedPoint,
[scenePointerX, scenePointerY], [scenePointerX, scenePointerY],
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -821,6 +859,7 @@ export class LinearElementEditor {
} else { } else {
newPoint = LinearElementEditor.createPointAt( newPoint = LinearElementEditor.createPointAt(
element, element,
elementsMap,
scenePointerX - appState.editingLinearElement.pointerOffset.x, scenePointerX - appState.editingLinearElement.pointerOffset.x,
scenePointerY - appState.editingLinearElement.pointerOffset.y, scenePointerY - appState.editingLinearElement.pointerOffset.y,
event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize, event[KEYS.CTRL_OR_CMD] ? null : appState.gridSize,
@@ -847,8 +886,9 @@ export class LinearElementEditor {
static getPointGlobalCoordinates( static getPointGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
point: Point, point: Point,
elementsMap: ElementsMap,
) { ) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
@@ -860,8 +900,9 @@ export class LinearElementEditor {
/** scene coords */ /** scene coords */
static getPointsGlobalCoordinates( static getPointsGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
): Point[] { ): Point[] {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
return element.points.map((point) => { return element.points.map((point) => {
@@ -873,13 +914,15 @@ export class LinearElementEditor {
static getPointAtIndexGlobalCoordinates( static getPointAtIndexGlobalCoordinates(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
indexMaybeFromEnd: number, // -1 for last element indexMaybeFromEnd: number, // -1 for last element
elementsMap: ElementsMap,
): Point { ): Point {
const index = const index =
indexMaybeFromEnd < 0 indexMaybeFromEnd < 0
? element.points.length + indexMaybeFromEnd ? element.points.length + indexMaybeFromEnd
: indexMaybeFromEnd; : indexMaybeFromEnd;
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
@@ -893,8 +936,9 @@ export class LinearElementEditor {
static pointFromAbsoluteCoords( static pointFromAbsoluteCoords(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
absoluteCoords: Point, absoluteCoords: Point,
elementsMap: ElementsMap,
): Point { ): Point {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [x, y] = rotate( const [x, y] = rotate(
@@ -909,12 +953,15 @@ export class LinearElementEditor {
static getPointIndexUnderCursor( static getPointIndexUnderCursor(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
zoom: AppState["zoom"], zoom: AppState["zoom"],
x: number, x: number,
y: number, y: number,
) { ) {
const pointHandles = const pointHandles = LinearElementEditor.getPointsGlobalCoordinates(
LinearElementEditor.getPointsGlobalCoordinates(element); element,
elementsMap,
);
let idx = pointHandles.length; let idx = pointHandles.length;
// loop from right to left because points on the right are rendered over // loop from right to left because points on the right are rendered over
// points on the left, thus should take precedence when clicking, if they // points on the left, thus should take precedence when clicking, if they
@@ -934,12 +981,13 @@ export class LinearElementEditor {
static createPointAt( static createPointAt(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
scenePointerX: number, scenePointerX: number,
scenePointerY: number, scenePointerY: number,
gridSize: number | null, gridSize: number | null,
): Point { ): Point {
const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize); const pointerOnGrid = getGridPoint(scenePointerX, scenePointerY, gridSize);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const [rotatedX, rotatedY] = rotate( const [rotatedX, rotatedY] = rotate(
@@ -980,14 +1028,14 @@ export class LinearElementEditor {
mutateElement(element, LinearElementEditor.getNormalizedPoints(element)); mutateElement(element, LinearElementEditor.getNormalizedPoints(element));
} }
static duplicateSelectedPoints(appState: AppState) { static duplicateSelectedPoints(appState: AppState, elementsMap: ElementsMap) {
if (!appState.editingLinearElement) { if (!appState.editingLinearElement) {
return false; return false;
} }
const { selectedPointsIndices, elementId } = appState.editingLinearElement; const { selectedPointsIndices, elementId } = appState.editingLinearElement;
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element || selectedPointsIndices === null) { if (!element || selectedPointsIndices === null) {
return false; return false;
@@ -1149,9 +1197,11 @@ export class LinearElementEditor {
linearElementEditor: LinearElementEditor, linearElementEditor: LinearElementEditor,
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) { ) {
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
elementsMap,
); );
if (!element) { if (!element) {
@@ -1190,9 +1240,11 @@ export class LinearElementEditor {
pointerCoords: PointerCoords, pointerCoords: PointerCoords,
appState: AppState, appState: AppState,
snapToGrid: boolean, snapToGrid: boolean,
elementsMap: ElementsMap,
) { ) {
const element = LinearElementEditor.getElement( const element = LinearElementEditor.getElement(
linearElementEditor.elementId, linearElementEditor.elementId,
elementsMap,
); );
if (!element) { if (!element) {
return; return;
@@ -1208,6 +1260,7 @@ export class LinearElementEditor {
const midpoint = LinearElementEditor.createPointAt( const midpoint = LinearElementEditor.createPointAt(
element, element,
elementsMap,
pointerCoords.x, pointerCoords.x,
pointerCoords.y, pointerCoords.y,
snapToGrid ? appState.gridSize : null, snapToGrid ? appState.gridSize : null,
@@ -1260,6 +1313,7 @@ export class LinearElementEditor {
private static _getShiftLockedDelta( private static _getShiftLockedDelta(
element: NonDeleted<ExcalidrawLinearElement>, element: NonDeleted<ExcalidrawLinearElement>,
elementsMap: ElementsMap,
referencePoint: Point, referencePoint: Point,
scenePointer: Point, scenePointer: Point,
gridSize: number | null, gridSize: number | null,
@@ -1267,6 +1321,7 @@ export class LinearElementEditor {
const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates( const referencePointCoords = LinearElementEditor.getPointGlobalCoordinates(
element, element,
referencePoint, referencePoint,
elementsMap,
); );
const [gridX, gridY] = getGridPoint( const [gridX, gridY] = getGridPoint(
@@ -1288,8 +1343,12 @@ export class LinearElementEditor {
static getBoundTextElementPosition = ( static getBoundTextElementPosition = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
): { x: number; y: number } => { ): { x: number; y: number } => {
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
if (points.length < 2) { if (points.length < 2) {
mutateElement(boundTextElement, { isDeleted: true }); mutateElement(boundTextElement, { isDeleted: true });
} }
@@ -1300,6 +1359,7 @@ export class LinearElementEditor {
const midPoint = LinearElementEditor.getPointGlobalCoordinates( const midPoint = LinearElementEditor.getPointGlobalCoordinates(
element, element,
element.points[index], element.points[index],
elementsMap,
); );
x = midPoint[0] - boundTextElement.width / 2; x = midPoint[0] - boundTextElement.width / 2;
y = midPoint[1] - boundTextElement.height / 2; y = midPoint[1] - boundTextElement.height / 2;
@@ -1319,6 +1379,7 @@ export class LinearElementEditor {
points[index], points[index],
points[index + 1], points[index + 1],
index + 1, index + 1,
elementsMap,
); );
} }
x = midSegmentMidpoint[0] - boundTextElement.width / 2; x = midSegmentMidpoint[0] - boundTextElement.width / 2;
@@ -1329,6 +1390,7 @@ export class LinearElementEditor {
static getMinMaxXYWithBoundText = ( static getMinMaxXYWithBoundText = (
element: ExcalidrawLinearElement, element: ExcalidrawLinearElement,
elementsMap: ElementsMap,
elementBounds: Bounds, elementBounds: Bounds,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
): [number, number, number, number, number, number] => { ): [number, number, number, number, number, number] => {
@@ -1339,6 +1401,7 @@ export class LinearElementEditor {
LinearElementEditor.getBoundTextElementPosition( LinearElementEditor.getBoundTextElementPosition(
element, element,
boundTextElement, boundTextElement,
elementsMap,
); );
const boundTextX2 = boundTextX1 + boundTextElement.width; const boundTextX2 = boundTextX1 + boundTextElement.width;
const boundTextY2 = boundTextY1 + boundTextElement.height; const boundTextY2 = boundTextY1 + boundTextElement.height;
@@ -1479,6 +1542,7 @@ export class LinearElementEditor {
if (boundTextElement) { if (boundTextElement) {
coords = LinearElementEditor.getMinMaxXYWithBoundText( coords = LinearElementEditor.getMinMaxXYWithBoundText(
element, element,
elementsMap,
[x1, y1, x2, y2], [x1, y1, x2, y2],
boundTextElement, boundTextElement,
); );
+9 -3
View File
@@ -16,6 +16,7 @@ import {
ExcalidrawEmbeddableElement, ExcalidrawEmbeddableElement,
ExcalidrawMagicFrameElement, ExcalidrawMagicFrameElement,
ExcalidrawIframeElement, ExcalidrawIframeElement,
ElementsMap,
} from "./types"; } from "./types";
import { import {
arrayToMap, arrayToMap,
@@ -68,6 +69,7 @@ export type ElementConstructorOpts = MarkOptional<
| "roundness" | "roundness"
| "locked" | "locked"
| "opacity" | "opacity"
| "customData"
>; >;
const _newElementBase = <T extends ExcalidrawElement>( const _newElementBase = <T extends ExcalidrawElement>(
@@ -121,6 +123,7 @@ const _newElementBase = <T extends ExcalidrawElement>(
updated: getUpdatedTimestamp(), updated: getUpdatedTimestamp(),
link, link,
locked, locked,
customData: rest.customData,
}; };
return element; return element;
}; };
@@ -258,6 +261,7 @@ export const newTextElement = (
const getAdjustedDimensions = ( const getAdjustedDimensions = (
element: ExcalidrawTextElement, element: ExcalidrawTextElement,
elementsMap: ElementsMap,
nextText: string, nextText: string,
): { ): {
x: number; x: number;
@@ -292,7 +296,7 @@ const getAdjustedDimensions = (
x = element.x - offsets.x; x = element.x - offsets.x;
y = element.y - offsets.y; y = element.y - offsets.y;
} else { } else {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords( const [nextX1, nextY1, nextX2, nextY2] = getResizedElementAbsoluteCoords(
element, element,
@@ -333,6 +337,7 @@ const getAdjustedDimensions = (
export const refreshTextDimensions = ( export const refreshTextDimensions = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null, container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
text = textElement.text, text = textElement.text,
) => { ) => {
if (textElement.isDeleted) { if (textElement.isDeleted) {
@@ -345,13 +350,14 @@ export const refreshTextDimensions = (
getBoundTextMaxWidth(container, textElement), getBoundTextMaxWidth(container, textElement),
); );
} }
const dimensions = getAdjustedDimensions(textElement, text); const dimensions = getAdjustedDimensions(textElement, elementsMap, text);
return { text, ...dimensions }; return { text, ...dimensions };
}; };
export const updateTextElement = ( export const updateTextElement = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawTextContainer | null, container: ExcalidrawTextContainer | null,
elementsMap: ElementsMap,
{ {
text, text,
isDeleted, isDeleted,
@@ -365,7 +371,7 @@ export const updateTextElement = (
return newElementWith(textElement, { return newElementWith(textElement, {
originalText, originalText,
isDeleted: isDeleted ?? textElement.isDeleted, isDeleted: isDeleted ?? textElement.isDeleted,
...refreshTextDimensions(textElement, container, originalText), ...refreshTextDimensions(textElement, container, elementsMap, originalText),
}); });
}; };
+19 -10
View File
@@ -86,11 +86,12 @@ export const transformElements = (
if (transformHandleType === "rotation") { if (transformHandleType === "rotation") {
rotateSingleElement( rotateSingleElement(
element, element,
elementsMap,
pointerX, pointerX,
pointerY, pointerY,
shouldRotateWithDiscreteAngle, shouldRotateWithDiscreteAngle,
); );
updateBoundElements(element); updateBoundElements(element, elementsMap);
} else if ( } else if (
isTextElement(element) && isTextElement(element) &&
(transformHandleType === "nw" || (transformHandleType === "nw" ||
@@ -106,7 +107,7 @@ export const transformElements = (
pointerX, pointerX,
pointerY, pointerY,
); );
updateBoundElements(element); updateBoundElements(element, elementsMap);
} else if (transformHandleType) { } else if (transformHandleType) {
resizeSingleElement( resizeSingleElement(
originalElements, originalElements,
@@ -157,11 +158,12 @@ export const transformElements = (
const rotateSingleElement = ( const rotateSingleElement = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
shouldRotateWithDiscreteAngle: boolean, shouldRotateWithDiscreteAngle: boolean,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
let angle: number; let angle: number;
@@ -266,7 +268,7 @@ const resizeSingleTextElement = (
pointerX: number, pointerX: number,
pointerY: number, pointerY: number,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
// rotation pointer with reverse angle // rotation pointer with reverse angle
@@ -629,7 +631,7 @@ export const resizeSingleElement = (
) { ) {
mutateElement(element, resizedElement); mutateElement(element, resizedElement);
updateBoundElements(element, { updateBoundElements(element, elementsMap, {
newSize: { width: resizedElement.width, height: resizedElement.height }, newSize: { width: resizedElement.width, height: resizedElement.height },
}); });
@@ -696,7 +698,11 @@ export const resizeMultipleElements = (
if (!isBoundToContainer(text)) { if (!isBoundToContainer(text)) {
return acc; return acc;
} }
const xy = LinearElementEditor.getBoundTextElementPosition(orig, text); const xy = LinearElementEditor.getBoundTextElementPosition(
orig,
text,
elementsMap,
);
return [...acc, { ...text, ...xy }]; return [...acc, { ...text, ...xy }];
}, [] as ExcalidrawTextElementWithContainer[]); }, [] as ExcalidrawTextElementWithContainer[]);
@@ -879,7 +885,7 @@ export const resizeMultipleElements = (
mutateElement(element, update, false); mutateElement(element, update, false);
updateBoundElements(element, { updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elementsToUpdate, simultaneouslyUpdated: elementsToUpdate,
newSize: { width, height }, newSize: { width, height },
}); });
@@ -921,7 +927,7 @@ const rotateMultipleElements = (
elements elements
.filter((element) => !isFrameLikeElement(element)) .filter((element) => !isFrameLikeElement(element))
.forEach((element) => { .forEach((element) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
const origAngle = const origAngle =
@@ -942,7 +948,9 @@ const rotateMultipleElements = (
}, },
false, false,
); );
updateBoundElements(element, { simultaneouslyUpdated: elements }); updateBoundElements(element, elementsMap, {
simultaneouslyUpdated: elements,
});
const boundText = getBoundTextElement(element, elementsMap); const boundText = getBoundTextElement(element, elementsMap);
if (boundText && !isArrowElement(element)) { if (boundText && !isArrowElement(element)) {
@@ -964,12 +972,13 @@ const rotateMultipleElements = (
export const getResizeOffsetXY = ( export const getResizeOffsetXY = (
transformHandleType: MaybeTransformHandleType, transformHandleType: MaybeTransformHandleType,
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
elementsMap: ElementsMap,
x: number, x: number,
y: number, y: number,
): [number, number] => { ): [number, number] => {
const [x1, y1, x2, y2] = const [x1, y1, x2, y2] =
selectedElements.length === 1 selectedElements.length === 1
? getElementAbsoluteCoords(selectedElements[0]) ? getElementAbsoluteCoords(selectedElements[0], elementsMap)
: getCommonBounds(selectedElements); : getCommonBounds(selectedElements);
const cx = (x1 + x2) / 2; const cx = (x1 + x2) / 2;
const cy = (y1 + y2) / 2; const cy = (y1 + y2) / 2;
+5 -1
View File
@@ -2,6 +2,7 @@ import {
ExcalidrawElement, ExcalidrawElement,
PointerType, PointerType,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
ElementsMap,
} from "./types"; } from "./types";
import { import {
@@ -27,6 +28,7 @@ const isInsideTransformHandle = (
export const resizeTest = ( export const resizeTest = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
appState: AppState, appState: AppState,
x: number, x: number,
y: number, y: number,
@@ -38,7 +40,7 @@ export const resizeTest = (
} }
const { rotation: rotationTransformHandle, ...transformHandles } = const { rotation: rotationTransformHandle, ...transformHandles } =
getTransformHandles(element, zoom, pointerType); getTransformHandles(element, zoom, elementsMap, pointerType);
if ( if (
rotationTransformHandle && rotationTransformHandle &&
@@ -70,6 +72,7 @@ export const getElementWithTransformHandleType = (
scenePointerY: number, scenePointerY: number,
zoom: Zoom, zoom: Zoom,
pointerType: PointerType, pointerType: PointerType,
elementsMap: ElementsMap,
) => { ) => {
return elements.reduce((result, element) => { return elements.reduce((result, element) => {
if (result) { if (result) {
@@ -77,6 +80,7 @@ export const getElementWithTransformHandleType = (
} }
const transformHandleType = resizeTest( const transformHandleType = resizeTest(
element, element,
elementsMap,
appState, appState,
scenePointerX, scenePointerX,
scenePointerY, scenePointerY,
+3 -2
View File
@@ -1,4 +1,4 @@
import { ExcalidrawElement } from "./types"; import { ElementsMap, ExcalidrawElement } from "./types";
import { mutateElement } from "./mutateElement"; import { mutateElement } from "./mutateElement";
import { isFreeDrawElement, isLinearElement } from "./typeChecks"; import { isFreeDrawElement, isLinearElement } from "./typeChecks";
import { SHIFT_LOCKING_ANGLE } from "../constants"; import { SHIFT_LOCKING_ANGLE } from "../constants";
@@ -26,8 +26,9 @@ export const isElementInViewport = (
scrollX: number; scrollX: number;
scrollY: number; scrollY: number;
}, },
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates const [x1, y1, x2, y2] = getElementBounds(element, elementsMap); // scene coordinates
const topLeftSceneCoords = viewportCoordsToSceneCoords( const topLeftSceneCoords = viewportCoordsToSceneCoords(
{ {
clientX: viewTransformations.offsetLeft, clientX: viewTransformations.offsetLeft,
+27 -8
View File
@@ -53,6 +53,7 @@ const splitIntoLines = (text: string) => {
export const redrawTextBoundingBox = ( export const redrawTextBoundingBox = (
textElement: ExcalidrawTextElement, textElement: ExcalidrawTextElement,
container: ExcalidrawElement | null, container: ExcalidrawElement | null,
elementsMap: ElementsMap,
) => { ) => {
let maxWidth = undefined; let maxWidth = undefined;
const boundTextUpdates = { const boundTextUpdates = {
@@ -110,7 +111,11 @@ export const redrawTextBoundingBox = (
...textElement, ...textElement,
...boundTextUpdates, ...boundTextUpdates,
} as ExcalidrawTextElementWithContainer; } as ExcalidrawTextElementWithContainer;
const { x, y } = computeBoundTextPosition(container, updatedTextElement); const { x, y } = computeBoundTextPosition(
container,
updatedTextElement,
elementsMap,
);
boundTextUpdates.x = x; boundTextUpdates.x = x;
boundTextUpdates.y = y; boundTextUpdates.y = y;
} }
@@ -119,11 +124,11 @@ export const redrawTextBoundingBox = (
}; };
export const bindTextToShapeAfterDuplication = ( export const bindTextToShapeAfterDuplication = (
sceneElements: ExcalidrawElement[], newElements: ExcalidrawElement[],
oldElements: ExcalidrawElement[], oldElements: ExcalidrawElement[],
oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>, oldIdToDuplicatedId: Map<ExcalidrawElement["id"], ExcalidrawElement["id"]>,
): void => { ): void => {
const sceneElementMap = arrayToMap(sceneElements) as Map< const newElementsMap = arrayToMap(newElements) as Map<
ExcalidrawElement["id"], ExcalidrawElement["id"],
ExcalidrawElement ExcalidrawElement
>; >;
@@ -134,7 +139,7 @@ export const bindTextToShapeAfterDuplication = (
if (boundTextElementId) { if (boundTextElementId) {
const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId); const newTextElementId = oldIdToDuplicatedId.get(boundTextElementId);
if (newTextElementId) { if (newTextElementId) {
const newContainer = sceneElementMap.get(newElementId); const newContainer = newElementsMap.get(newElementId);
if (newContainer) { if (newContainer) {
mutateElement(newContainer, { mutateElement(newContainer, {
boundElements: (element.boundElements || []) boundElements: (element.boundElements || [])
@@ -149,7 +154,7 @@ export const bindTextToShapeAfterDuplication = (
}), }),
}); });
} }
const newTextElement = sceneElementMap.get(newTextElementId); const newTextElement = newElementsMap.get(newTextElementId);
if (newTextElement && isTextElement(newTextElement)) { if (newTextElement && isTextElement(newTextElement)) {
mutateElement(newTextElement, { mutateElement(newTextElement, {
containerId: newContainer ? newElementId : null, containerId: newContainer ? newElementId : null,
@@ -236,7 +241,7 @@ export const handleBindTextResize = (
if (!isArrowElement(container)) { if (!isArrowElement(container)) {
mutateElement( mutateElement(
textElement, textElement,
computeBoundTextPosition(container, textElement), computeBoundTextPosition(container, textElement, elementsMap),
); );
} }
} }
@@ -245,11 +250,13 @@ export const handleBindTextResize = (
export const computeBoundTextPosition = ( export const computeBoundTextPosition = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
) => { ) => {
if (isArrowElement(container)) { if (isArrowElement(container)) {
return LinearElementEditor.getBoundTextElementPosition( return LinearElementEditor.getBoundTextElementPosition(
container, container,
boundTextElement, boundTextElement,
elementsMap,
); );
} }
const containerCoords = getContainerCoords(container); const containerCoords = getContainerCoords(container);
@@ -698,12 +705,16 @@ export const getContainerCenter = (
y: container.y + container.height / 2, y: container.y + container.height / 2,
}; };
} }
const points = LinearElementEditor.getPointsGlobalCoordinates(container); const points = LinearElementEditor.getPointsGlobalCoordinates(
container,
elementsMap,
);
if (points.length % 2 === 1) { if (points.length % 2 === 1) {
const index = Math.floor(container.points.length / 2); const index = Math.floor(container.points.length / 2);
const midPoint = LinearElementEditor.getPointGlobalCoordinates( const midPoint = LinearElementEditor.getPointGlobalCoordinates(
container, container,
container.points[index], container.points[index],
elementsMap,
); );
return { x: midPoint[0], y: midPoint[1] }; return { x: midPoint[0], y: midPoint[1] };
} }
@@ -719,6 +730,7 @@ export const getContainerCenter = (
points[index], points[index],
points[index + 1], points[index + 1],
index + 1, index + 1,
elementsMap,
); );
} }
return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] }; return { x: midSegmentMidpoint[0], y: midSegmentMidpoint[1] };
@@ -757,11 +769,13 @@ export const getTextElementAngle = (
export const getBoundTextElementPosition = ( export const getBoundTextElementPosition = (
container: ExcalidrawElement, container: ExcalidrawElement,
boundTextElement: ExcalidrawTextElementWithContainer, boundTextElement: ExcalidrawTextElementWithContainer,
elementsMap: ElementsMap,
) => { ) => {
if (isArrowElement(container)) { if (isArrowElement(container)) {
return LinearElementEditor.getBoundTextElementPosition( return LinearElementEditor.getBoundTextElementPosition(
container, container,
boundTextElement, boundTextElement,
elementsMap,
); );
} }
}; };
@@ -804,6 +818,7 @@ export const getTextBindableContainerAtPosition = (
appState: AppState, appState: AppState,
x: number, x: number,
y: number, y: number,
elementsMap: ElementsMap,
): ExcalidrawTextContainer | null => { ): ExcalidrawTextContainer | null => {
const selectedElements = getSelectedElements(elements, appState); const selectedElements = getSelectedElements(elements, appState);
if (selectedElements.length === 1) { if (selectedElements.length === 1) {
@@ -817,7 +832,10 @@ export const getTextBindableContainerAtPosition = (
if (elements[index].isDeleted) { if (elements[index].isDeleted) {
continue; continue;
} }
const [x1, y1, x2, y2] = getElementAbsoluteCoords(elements[index]); const [x1, y1, x2, y2] = getElementAbsoluteCoords(
elements[index],
elementsMap,
);
if ( if (
isArrowElement(elements[index]) && isArrowElement(elements[index]) &&
isHittingElementNotConsideringBoundingBox( isHittingElementNotConsideringBoundingBox(
@@ -825,6 +843,7 @@ export const getTextBindableContainerAtPosition = (
appState, appState,
null, null,
[x, y], [x, y],
elementsMap,
) )
) { ) {
hitElement = elements[index]; hitElement = elements[index];
+11 -5
View File
@@ -121,13 +121,13 @@ export const textWysiwyg = ({
return; return;
} }
const { textAlign, verticalAlign } = updatedTextElement; const { textAlign, verticalAlign } = updatedTextElement;
const elementsMap = app.scene.getNonDeletedElementsMap();
if (updatedTextElement && isTextElement(updatedTextElement)) { if (updatedTextElement && isTextElement(updatedTextElement)) {
let coordX = updatedTextElement.x; let coordX = updatedTextElement.x;
let coordY = updatedTextElement.y; let coordY = updatedTextElement.y;
const container = getContainerElement( const container = getContainerElement(
updatedTextElement, updatedTextElement,
app.scene.getElementsMapIncludingDeleted(), app.scene.getNonDeletedElementsMap(),
); );
let maxWidth = updatedTextElement.width; let maxWidth = updatedTextElement.width;
@@ -143,6 +143,7 @@ export const textWysiwyg = ({
LinearElementEditor.getBoundTextElementPosition( LinearElementEditor.getBoundTextElementPosition(
container, container,
updatedTextElement as ExcalidrawTextElementWithContainer, updatedTextElement as ExcalidrawTextElementWithContainer,
elementsMap,
); );
coordX = boundTextCoords.x; coordX = boundTextCoords.x;
coordY = boundTextCoords.y; coordY = boundTextCoords.y;
@@ -200,6 +201,7 @@ export const textWysiwyg = ({
const { y } = computeBoundTextPosition( const { y } = computeBoundTextPosition(
container, container,
updatedTextElement as ExcalidrawTextElementWithContainer, updatedTextElement as ExcalidrawTextElementWithContainer,
elementsMap,
); );
coordY = y; coordY = y;
} }
@@ -326,7 +328,7 @@ export const textWysiwyg = ({
} }
const container = getContainerElement( const container = getContainerElement(
element, element,
app.scene.getElementsMapIncludingDeleted(), app.scene.getNonDeletedElementsMap(),
); );
const font = getFontString({ const font = getFontString({
@@ -513,7 +515,7 @@ export const textWysiwyg = ({
let text = editable.value; let text = editable.value;
const container = getContainerElement( const container = getContainerElement(
updateElement, updateElement,
app.scene.getElementsMapIncludingDeleted(), app.scene.getNonDeletedElementsMap(),
); );
if (container) { if (container) {
@@ -541,7 +543,11 @@ export const textWysiwyg = ({
), ),
}); });
} }
redrawTextBoundingBox(updateElement, container); redrawTextBoundingBox(
updateElement,
container,
app.scene.getNonDeletedElementsMap(),
);
} }
onSubmit({ onSubmit({
@@ -1,4 +1,5 @@
import { import {
ElementsMap,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
PointerType, PointerType,
@@ -230,6 +231,8 @@ export const getTransformHandlesFromCoords = (
export const getTransformHandles = ( export const getTransformHandles = (
element: ExcalidrawElement, element: ExcalidrawElement,
zoom: Zoom, zoom: Zoom,
elementsMap: ElementsMap,
pointerType: PointerType = "mouse", pointerType: PointerType = "mouse",
): TransformHandles => { ): TransformHandles => {
// so that when locked element is selected (especially when you toggle lock // so that when locked element is selected (especially when you toggle lock
@@ -267,7 +270,7 @@ export const getTransformHandles = (
? DEFAULT_TRANSFORM_HANDLE_SPACING + 8 ? DEFAULT_TRANSFORM_HANDLE_SPACING + 8
: DEFAULT_TRANSFORM_HANDLE_SPACING; : DEFAULT_TRANSFORM_HANDLE_SPACING;
return getTransformHandlesFromCoords( return getTransformHandlesFromCoords(
getElementAbsoluteCoords(element, true), getElementAbsoluteCoords(element, elementsMap, true),
element.angle, element.angle,
zoom, zoom,
pointerType, pointerType,
+55 -31
View File
@@ -65,10 +65,11 @@ export const bindElementsToFramesAfterDuplication = (
export function isElementIntersectingFrame( export function isElementIntersectingFrame(
element: ExcalidrawElement, element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) { ) {
const frameLineSegments = getElementLineSegments(frame); const frameLineSegments = getElementLineSegments(frame, elementsMap);
const elementLineSegments = getElementLineSegments(element); const elementLineSegments = getElementLineSegments(element, elementsMap);
const intersecting = frameLineSegments.some((frameLineSegment) => const intersecting = frameLineSegments.some((frameLineSegment) =>
elementLineSegments.some((elementLineSegment) => elementLineSegments.some((elementLineSegment) =>
@@ -82,9 +83,10 @@ export function isElementIntersectingFrame(
export const getElementsCompletelyInFrame = ( export const getElementsCompletelyInFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => ) =>
omitGroupsContainingFrameLikes( omitGroupsContainingFrameLikes(
getElementsWithinSelection(elements, frame, false), getElementsWithinSelection(elements, frame, elementsMap, false),
).filter( ).filter(
(element) => (element) =>
(!isFrameLikeElement(element) && !element.frameId) || (!isFrameLikeElement(element) && !element.frameId) ||
@@ -95,8 +97,9 @@ export const isElementContainingFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
element: ExcalidrawElement, element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
return getElementsWithinSelection(elements, element).some( return getElementsWithinSelection(elements, element, elementsMap).some(
(e) => e.id === frame.id, (e) => e.id === frame.id,
); );
}; };
@@ -104,13 +107,22 @@ export const isElementContainingFrame = (
export const getElementsIntersectingFrame = ( export const getElementsIntersectingFrame = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => elements.filter((element) => isElementIntersectingFrame(element, frame)); ) => {
const elementsMap = arrayToMap(elements);
return elements.filter((element) =>
isElementIntersectingFrame(element, frame, elementsMap),
);
};
export const elementsAreInFrameBounds = ( export const elementsAreInFrameBounds = (
elements: readonly ExcalidrawElement[], elements: readonly ExcalidrawElement[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(frame); const [frameX1, frameY1, frameX2, frameY2] = getElementAbsoluteCoords(
frame,
elementsMap,
);
const [elementX1, elementY1, elementX2, elementY2] = const [elementX1, elementY1, elementX2, elementY2] =
getCommonBounds(elements); getCommonBounds(elements);
@@ -126,11 +138,12 @@ export const elementsAreInFrameBounds = (
export const elementOverlapsWithFrame = ( export const elementOverlapsWithFrame = (
element: ExcalidrawElement, element: ExcalidrawElement,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
return ( return (
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame, elementsMap) ||
isElementIntersectingFrame(element, frame) || isElementIntersectingFrame(element, frame, elementsMap) ||
isElementContainingFrame([frame], element, frame) isElementContainingFrame([frame], element, frame, elementsMap)
); );
}; };
@@ -140,8 +153,9 @@ export const isCursorInFrame = (
y: number; y: number;
}, },
frame: NonDeleted<ExcalidrawFrameLikeElement>, frame: NonDeleted<ExcalidrawFrameLikeElement>,
elementsMap: ElementsMap,
) => { ) => {
const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame); const [fx1, fy1, fx2, fy2] = getElementAbsoluteCoords(frame, elementsMap);
return isPointWithinBounds( return isPointWithinBounds(
[fx1, fy1], [fx1, fy1],
@@ -155,6 +169,7 @@ export const groupsAreAtLeastIntersectingTheFrame = (
groupIds: readonly string[], groupIds: readonly string[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const elementsMap = arrayToMap(elements);
const elementsInGroup = groupIds.flatMap((groupId) => const elementsInGroup = groupIds.flatMap((groupId) =>
getElementsInGroup(elements, groupId), getElementsInGroup(elements, groupId),
); );
@@ -165,8 +180,8 @@ export const groupsAreAtLeastIntersectingTheFrame = (
return !!elementsInGroup.find( return !!elementsInGroup.find(
(element) => (element) =>
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame, elementsMap) ||
isElementIntersectingFrame(element, frame), isElementIntersectingFrame(element, frame, elementsMap),
); );
}; };
@@ -175,6 +190,7 @@ export const groupsAreCompletelyOutOfFrame = (
groupIds: readonly string[], groupIds: readonly string[],
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const elementsMap = arrayToMap(elements);
const elementsInGroup = groupIds.flatMap((groupId) => const elementsInGroup = groupIds.flatMap((groupId) =>
getElementsInGroup(elements, groupId), getElementsInGroup(elements, groupId),
); );
@@ -186,8 +202,8 @@ export const groupsAreCompletelyOutOfFrame = (
return ( return (
elementsInGroup.find( elementsInGroup.find(
(element) => (element) =>
elementsAreInFrameBounds([element], frame) || elementsAreInFrameBounds([element], frame, elementsMap) ||
isElementIntersectingFrame(element, frame), isElementIntersectingFrame(element, frame, elementsMap),
) === undefined ) === undefined
); );
}; };
@@ -258,14 +274,15 @@ export const getElementsInResizingFrame = (
allElements: ExcalidrawElementsIncludingDeleted, allElements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
): ExcalidrawElement[] => { ): ExcalidrawElement[] => {
const prevElementsInFrame = getFrameChildren(allElements, frame.id); const prevElementsInFrame = getFrameChildren(allElements, frame.id);
const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame); const nextElementsInFrame = new Set<ExcalidrawElement>(prevElementsInFrame);
const elementsCompletelyInFrame = new Set([ const elementsCompletelyInFrame = new Set([
...getElementsCompletelyInFrame(allElements, frame), ...getElementsCompletelyInFrame(allElements, frame, elementsMap),
...prevElementsInFrame.filter((element) => ...prevElementsInFrame.filter((element) =>
isElementContainingFrame(allElements, element, frame), isElementContainingFrame(allElements, element, frame, elementsMap),
), ),
]); ]);
@@ -283,7 +300,7 @@ export const getElementsInResizingFrame = (
); );
for (const element of elementsNotCompletelyInFrame) { for (const element of elementsNotCompletelyInFrame) {
if (!isElementIntersectingFrame(element, frame)) { if (!isElementIntersectingFrame(element, frame, elementsMap)) {
if (element.groupIds.length === 0) { if (element.groupIds.length === 0) {
nextElementsInFrame.delete(element); nextElementsInFrame.delete(element);
} }
@@ -334,7 +351,7 @@ export const getElementsInResizingFrame = (
if (isSelected) { if (isSelected) {
const elementsInGroup = getElementsInGroup(allElements, id); const elementsInGroup = getElementsInGroup(allElements, id);
if (elementsAreInFrameBounds(elementsInGroup, frame)) { if (elementsAreInFrameBounds(elementsInGroup, frame, elementsMap)) {
for (const element of elementsInGroup) { for (const element of elementsInGroup) {
nextElementsInFrame.add(element); nextElementsInFrame.add(element);
} }
@@ -348,12 +365,13 @@ export const getElementsInResizingFrame = (
}; };
export const getElementsInNewFrame = ( export const getElementsInNewFrame = (
allElements: ExcalidrawElementsIncludingDeleted, elements: ExcalidrawElementsIncludingDeleted,
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
elementsMap: ElementsMap,
) => { ) => {
return omitGroupsContainingFrameLikes( return omitGroupsContainingFrameLikes(
allElements, elements,
getElementsCompletelyInFrame(allElements, frame), getElementsCompletelyInFrame(elements, frame, elementsMap),
); );
}; };
@@ -388,7 +406,7 @@ export const filterElementsEligibleAsFrameChildren = (
frame: ExcalidrawFrameLikeElement, frame: ExcalidrawFrameLikeElement,
) => { ) => {
const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>(); const otherFrames = new Set<ExcalidrawFrameLikeElement["id"]>();
const elementsMap = arrayToMap(elements);
elements = omitGroupsContainingFrameLikes(elements); elements = omitGroupsContainingFrameLikes(elements);
for (const element of elements) { for (const element of elements) {
@@ -415,14 +433,18 @@ export const filterElementsEligibleAsFrameChildren = (
if (!processedGroups.has(shallowestGroupId)) { if (!processedGroups.has(shallowestGroupId)) {
processedGroups.add(shallowestGroupId); processedGroups.add(shallowestGroupId);
const groupElements = getElementsInGroup(elements, shallowestGroupId); const groupElements = getElementsInGroup(elements, shallowestGroupId);
if (groupElements.some((el) => elementOverlapsWithFrame(el, frame))) { if (
groupElements.some((el) =>
elementOverlapsWithFrame(el, frame, elementsMap),
)
) {
for (const child of groupElements) { for (const child of groupElements) {
eligibleElements.push(child); eligibleElements.push(child);
} }
} }
} }
} else { } else {
const overlaps = elementOverlapsWithFrame(element, frame); const overlaps = elementOverlapsWithFrame(element, frame, elementsMap);
if (overlaps) { if (overlaps) {
eligibleElements.push(element); eligibleElements.push(element);
} }
@@ -682,12 +704,12 @@ export const getTargetFrame = (
// given an element, return if the element is in some frame // given an element, return if the element is in some frame
export const isElementInFrame = ( export const isElementInFrame = (
element: ExcalidrawElement, element: ExcalidrawElement,
allElements: ElementsMap, allElementsMap: ElementsMap,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
) => { ) => {
const frame = getTargetFrame(element, allElements, appState); const frame = getTargetFrame(element, allElementsMap, appState);
const _element = isTextElement(element) const _element = isTextElement(element)
? getContainerElement(element, allElements) || element ? getContainerElement(element, allElementsMap) || element
: element; : element;
if (frame) { if (frame) {
@@ -703,16 +725,18 @@ export const isElementInFrame = (
} }
if (_element.groupIds.length === 0) { if (_element.groupIds.length === 0) {
return elementOverlapsWithFrame(_element, frame); return elementOverlapsWithFrame(_element, frame, allElementsMap);
} }
const allElementsInGroup = new Set( const allElementsInGroup = new Set(
_element.groupIds.flatMap((gid) => getElementsInGroup(allElements, gid)), _element.groupIds.flatMap((gid) =>
getElementsInGroup(allElementsMap, gid),
),
); );
if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) { if (appState.editingGroupId && appState.selectedElementsAreBeingDragged) {
const selectedElements = new Set( const selectedElements = new Set(
getSelectedElements(allElements, appState), getSelectedElements(allElementsMap, appState),
); );
const editingGroupOverlapsFrame = appState.frameToHighlight !== null; const editingGroupOverlapsFrame = appState.frameToHighlight !== null;
@@ -733,7 +757,7 @@ export const isElementInFrame = (
} }
for (const elementInGroup of allElementsInGroup) { for (const elementInGroup of allElementsInGroup) {
if (elementOverlapsWithFrame(elementInGroup, frame)) { if (elementOverlapsWithFrame(elementInGroup, frame, allElementsMap)) {
return true; return true;
} }
} }
+25 -12
View File
@@ -7,6 +7,7 @@ import {
ExcalidrawTextElementWithContainer, ExcalidrawTextElementWithContainer,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
NonDeletedSceneElementsMap, NonDeletedSceneElementsMap,
ElementsMap,
} from "../element/types"; } from "../element/types";
import { import {
isTextElement, isTextElement,
@@ -137,6 +138,7 @@ export interface ExcalidrawElementWithCanvas {
const cappedElementCanvasSize = ( const cappedElementCanvasSize = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
zoom: Zoom, zoom: Zoom,
): { ): {
width: number; width: number;
@@ -155,7 +157,7 @@ const cappedElementCanvasSize = (
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const elementWidth = const elementWidth =
isLinearElement(element) || isFreeDrawElement(element) isLinearElement(element) || isFreeDrawElement(element)
? distance(x1, x2) ? distance(x1, x2)
@@ -200,7 +202,11 @@ const generateElementCanvas = (
const context = canvas.getContext("2d")!; const context = canvas.getContext("2d")!;
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
const { width, height, scale } = cappedElementCanvasSize(element, zoom); const { width, height, scale } = cappedElementCanvasSize(
element,
elementsMap,
zoom,
);
canvas.width = width; canvas.width = width;
canvas.height = height; canvas.height = height;
@@ -209,7 +215,7 @@ const generateElementCanvas = (
let canvasOffsetY = 0; let canvasOffsetY = 0;
if (isLinearElement(element) || isFreeDrawElement(element)) { if (isLinearElement(element) || isFreeDrawElement(element)) {
const [x1, y1] = getElementAbsoluteCoords(element); const [x1, y1] = getElementAbsoluteCoords(element, elementsMap);
canvasOffsetX = canvasOffsetX =
element.x > x1 element.x > x1
@@ -468,7 +474,7 @@ const drawElementFromCanvas = (
const element = elementWithCanvas.element; const element = elementWithCanvas.element;
const padding = getCanvasPadding(element); const padding = getCanvasPadding(element);
const zoom = elementWithCanvas.scale; const zoom = elementWithCanvas.scale;
let [x1, y1, x2, y2] = getElementAbsoluteCoords(element); let [x1, y1, x2, y2] = getElementAbsoluteCoords(element, allElementsMap);
// Free draw elements will otherwise "shuffle" as the min x and y change // Free draw elements will otherwise "shuffle" as the min x and y change
if (isFreeDrawElement(element)) { if (isFreeDrawElement(element)) {
@@ -513,8 +519,10 @@ const drawElementFromCanvas = (
elementWithCanvas.canvas.height, elementWithCanvas.canvas.height,
); );
const [, , , , boundTextCx, boundTextCy] = const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
getElementAbsoluteCoords(boundTextElement); boundTextElement,
allElementsMap,
);
tempCanvasContext.rotate(-element.angle); tempCanvasContext.rotate(-element.angle);
@@ -694,7 +702,7 @@ export const renderElement = (
ShapeCache.generateElementShape(element, null); ShapeCache.generateElementShape(element, null);
if (renderConfig.isExporting) { if (renderConfig.isExporting) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2 + appState.scrollX; const cx = (x1 + x2) / 2 + appState.scrollX;
const cy = (y1 + y2) / 2 + appState.scrollY; const cy = (y1 + y2) / 2 + appState.scrollY;
const shiftX = (x2 - x1) / 2 - (element.x - x1); const shiftX = (x2 - x1) / 2 - (element.x - x1);
@@ -737,7 +745,7 @@ export const renderElement = (
// rely on existing shapes // rely on existing shapes
ShapeCache.generateElementShape(element, renderConfig); ShapeCache.generateElementShape(element, renderConfig);
if (renderConfig.isExporting) { if (renderConfig.isExporting) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const cx = (x1 + x2) / 2 + appState.scrollX; const cx = (x1 + x2) / 2 + appState.scrollX;
const cy = (y1 + y2) / 2 + appState.scrollY; const cy = (y1 + y2) / 2 + appState.scrollY;
let shiftX = (x2 - x1) / 2 - (element.x - x1); let shiftX = (x2 - x1) / 2 - (element.x - x1);
@@ -749,6 +757,7 @@ export const renderElement = (
LinearElementEditor.getBoundTextElementPosition( LinearElementEditor.getBoundTextElementPosition(
container, container,
element as ExcalidrawTextElementWithContainer, element as ExcalidrawTextElementWithContainer,
elementsMap,
); );
shiftX = (x2 - x1) / 2 - (boundTextCoords.x - x1); shiftX = (x2 - x1) / 2 - (boundTextCoords.x - x1);
shiftY = (y2 - y1) / 2 - (boundTextCoords.y - y1); shiftY = (y2 - y1) / 2 - (boundTextCoords.y - y1);
@@ -804,8 +813,10 @@ export const renderElement = (
tempCanvasContext.rotate(-element.angle); tempCanvasContext.rotate(-element.angle);
// Shift the canvas to center of bound text // Shift the canvas to center of bound text
const [, , , , boundTextCx, boundTextCy] = const [, , , , boundTextCx, boundTextCy] = getElementAbsoluteCoords(
getElementAbsoluteCoords(boundTextElement); boundTextElement,
elementsMap,
);
const boundTextShiftX = (x1 + x2) / 2 - boundTextCx; const boundTextShiftX = (x1 + x2) / 2 - boundTextCx;
const boundTextShiftY = (y1 + y2) / 2 - boundTextCy; const boundTextShiftY = (y1 + y2) / 2 - boundTextCy;
tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY); tempCanvasContext.translate(-boundTextShiftX, -boundTextShiftY);
@@ -939,17 +950,18 @@ export const renderElementToSvg = (
renderConfig: SVGRenderConfig, renderConfig: SVGRenderConfig,
) => { ) => {
const offset = { x: offsetX, y: offsetY }; const offset = { x: offsetX, y: offsetY };
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
let cx = (x2 - x1) / 2 - (element.x - x1); let cx = (x2 - x1) / 2 - (element.x - x1);
let cy = (y2 - y1) / 2 - (element.y - y1); let cy = (y2 - y1) / 2 - (element.y - y1);
if (isTextElement(element)) { if (isTextElement(element)) {
const container = getContainerElement(element, elementsMap); const container = getContainerElement(element, elementsMap);
if (isArrowElement(container)) { if (isArrowElement(container)) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(container); const [x1, y1, x2, y2] = getElementAbsoluteCoords(container, elementsMap);
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
container, container,
element as ExcalidrawTextElementWithContainer, element as ExcalidrawTextElementWithContainer,
elementsMap,
); );
cx = (x2 - x1) / 2 - (boundTextCoords.x - x1); cx = (x2 - x1) / 2 - (boundTextCoords.x - x1);
cy = (y2 - y1) / 2 - (boundTextCoords.y - y1); cy = (y2 - y1) / 2 - (boundTextCoords.y - y1);
@@ -1151,6 +1163,7 @@ export const renderElementToSvg = (
const boundTextCoords = LinearElementEditor.getBoundTextElementPosition( const boundTextCoords = LinearElementEditor.getBoundTextElementPosition(
element, element,
boundText, boundText,
elementsMap,
); );
const maskX = offsetX + boundTextCoords.x - element.x; const maskX = offsetX + boundTextCoords.x - element.x;
+44 -16
View File
@@ -17,6 +17,7 @@ import {
GroupId, GroupId,
ExcalidrawBindableElement, ExcalidrawBindableElement,
ExcalidrawFrameLikeElement, ExcalidrawFrameLikeElement,
ElementsMap,
} from "../element/types"; } from "../element/types";
import { import {
getElementAbsoluteCoords, getElementAbsoluteCoords,
@@ -72,7 +73,7 @@ import {
import { import {
EXTERNAL_LINK_IMG, EXTERNAL_LINK_IMG,
getLinkHandleFromCoords, getLinkHandleFromCoords,
} from "../element/Hyperlink"; } from "../components/hyperlink/helpers";
import { renderSnaps } from "./renderSnaps"; import { renderSnaps } from "./renderSnaps";
import { import {
isEmbeddableElement, isEmbeddableElement,
@@ -256,7 +257,10 @@ const renderLinearPointHandles = (
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
context.lineWidth = 1 / appState.zoom.value; context.lineWidth = 1 / appState.zoom.value;
const points = LinearElementEditor.getPointsGlobalCoordinates(element); const points = LinearElementEditor.getPointsGlobalCoordinates(
element,
elementsMap,
);
const { POINT_HANDLE_SIZE } = LinearElementEditor; const { POINT_HANDLE_SIZE } = LinearElementEditor;
const radius = appState.editingLinearElement const radius = appState.editingLinearElement
@@ -340,6 +344,7 @@ const highlightPoint = (
const renderLinearElementPointHighlight = ( const renderLinearElementPointHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
elementsMap: ElementsMap,
) => { ) => {
const { elementId, hoverPointIndex } = appState.selectedLinearElement!; const { elementId, hoverPointIndex } = appState.selectedLinearElement!;
if ( if (
@@ -349,13 +354,15 @@ const renderLinearElementPointHighlight = (
) { ) {
return; return;
} }
const element = LinearElementEditor.getElement(elementId); const element = LinearElementEditor.getElement(elementId, elementsMap);
if (!element) { if (!element) {
return; return;
} }
const point = LinearElementEditor.getPointAtIndexGlobalCoordinates( const point = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
hoverPointIndex, hoverPointIndex,
elementsMap,
); );
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
@@ -510,12 +517,22 @@ const _renderInteractiveScene = ({
appState.suggestedBindings appState.suggestedBindings
.filter((binding) => binding != null) .filter((binding) => binding != null)
.forEach((suggestedBinding) => { .forEach((suggestedBinding) => {
renderBindingHighlight(context, appState, suggestedBinding!); renderBindingHighlight(
context,
appState,
suggestedBinding!,
elementsMap,
);
}); });
} }
if (appState.frameToHighlight) { if (appState.frameToHighlight) {
renderFrameHighlight(context, appState, appState.frameToHighlight); renderFrameHighlight(
context,
appState,
appState.frameToHighlight,
elementsMap,
);
} }
if (appState.elementsToHighlight) { if (appState.elementsToHighlight) {
@@ -545,7 +562,7 @@ const _renderInteractiveScene = ({
appState.selectedLinearElement && appState.selectedLinearElement &&
appState.selectedLinearElement.hoverPointIndex >= 0 appState.selectedLinearElement.hoverPointIndex >= 0
) { ) {
renderLinearElementPointHighlight(context, appState); renderLinearElementPointHighlight(context, appState, elementsMap);
} }
// Paint selected elements // Paint selected elements
if (!appState.multiElement && !appState.editingLinearElement) { if (!appState.multiElement && !appState.editingLinearElement) {
@@ -608,7 +625,7 @@ const _renderInteractiveScene = ({
if (selectionColors.length) { if (selectionColors.length) {
const [elementX1, elementY1, elementX2, elementY2, cx, cy] = const [elementX1, elementY1, elementX2, elementY2, cx, cy] =
getElementAbsoluteCoords(element, true); getElementAbsoluteCoords(element, elementsMap, true);
selections.push({ selections.push({
angle: element.angle, angle: element.angle,
elementX1, elementX1,
@@ -666,7 +683,8 @@ const _renderInteractiveScene = ({
const transformHandles = getTransformHandles( const transformHandles = getTransformHandles(
selectedElements[0], selectedElements[0],
appState.zoom, appState.zoom,
"mouse", // when we render we don't know which pointer type so use mouse elementsMap,
"mouse", // when we render we don't know which pointer type so use mouse,
); );
if (!appState.viewModeEnabled && showBoundingBox) { if (!appState.viewModeEnabled && showBoundingBox) {
renderTransformHandles( renderTransformHandles(
@@ -868,7 +886,7 @@ const _renderInteractiveScene = ({
let scrollBars; let scrollBars;
if (renderConfig.renderScrollbars) { if (renderConfig.renderScrollbars) {
scrollBars = getScrollBars( scrollBars = getScrollBars(
elementsMap, visibleElements,
normalizedWidth, normalizedWidth,
normalizedHeight, normalizedHeight,
appState, appState,
@@ -953,7 +971,11 @@ const _renderStaticScene = ({
element.groupIds.length > 0 && element.groupIds.length > 0 &&
appState.frameToHighlight && appState.frameToHighlight &&
appState.selectedElementIds[element.id] && appState.selectedElementIds[element.id] &&
(elementOverlapsWithFrame(element, appState.frameToHighlight) || (elementOverlapsWithFrame(
element,
appState.frameToHighlight,
elementsMap,
) ||
element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId))) element.groupIds.find((groupId) => groupsToBeAddedToFrame.has(groupId)))
) { ) {
element.groupIds.forEach((groupId) => element.groupIds.forEach((groupId) =>
@@ -1004,7 +1026,7 @@ const _renderStaticScene = ({
); );
} }
if (!isExporting) { if (!isExporting) {
renderLinkIcon(element, context, appState); renderLinkIcon(element, context, appState, elementsMap);
} }
} catch (error: any) { } catch (error: any) {
console.error(error); console.error(error);
@@ -1048,7 +1070,7 @@ const _renderStaticScene = ({
); );
} }
if (!isExporting) { if (!isExporting) {
renderLinkIcon(element, context, appState); renderLinkIcon(element, context, appState, elementsMap);
} }
}; };
// - when exporting the whole canvas, we DO NOT apply clipping // - when exporting the whole canvas, we DO NOT apply clipping
@@ -1247,6 +1269,7 @@ const renderBindingHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
suggestedBinding: SuggestedBinding, suggestedBinding: SuggestedBinding,
elementsMap: ElementsMap,
) => { ) => {
const renderHighlight = Array.isArray(suggestedBinding) const renderHighlight = Array.isArray(suggestedBinding)
? renderBindingHighlightForSuggestedPointBinding ? renderBindingHighlightForSuggestedPointBinding
@@ -1254,7 +1277,7 @@ const renderBindingHighlight = (
context.save(); context.save();
context.translate(appState.scrollX, appState.scrollY); context.translate(appState.scrollX, appState.scrollY);
renderHighlight(context, suggestedBinding as any); renderHighlight(context, suggestedBinding as any, elementsMap);
context.restore(); context.restore();
}; };
@@ -1262,8 +1285,9 @@ const renderBindingHighlight = (
const renderBindingHighlightForBindableElement = ( const renderBindingHighlightForBindableElement = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
element: ExcalidrawBindableElement, element: ExcalidrawBindableElement,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
const threshold = maxBindingGap(element, width, height); const threshold = maxBindingGap(element, width, height);
@@ -1323,8 +1347,9 @@ const renderFrameHighlight = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
frame: NonDeleted<ExcalidrawFrameLikeElement>, frame: NonDeleted<ExcalidrawFrameLikeElement>,
elementsMap: ElementsMap,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame); const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
const width = x2 - x1; const width = x2 - x1;
const height = y2 - y1; const height = y2 - y1;
@@ -1398,6 +1423,7 @@ const renderElementsBoxHighlight = (
const renderBindingHighlightForSuggestedPointBinding = ( const renderBindingHighlightForSuggestedPointBinding = (
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
suggestedBinding: SuggestedPointBinding, suggestedBinding: SuggestedPointBinding,
elementsMap: ElementsMap,
) => { ) => {
const [element, startOrEnd, bindableElement] = suggestedBinding; const [element, startOrEnd, bindableElement] = suggestedBinding;
@@ -1416,6 +1442,7 @@ const renderBindingHighlightForSuggestedPointBinding = (
const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates( const [x, y] = LinearElementEditor.getPointAtIndexGlobalCoordinates(
element, element,
index, index,
elementsMap,
); );
fillCircle(context, x, y, threshold); fillCircle(context, x, y, threshold);
}); });
@@ -1426,9 +1453,10 @@ const renderLinkIcon = (
element: NonDeletedExcalidrawElement, element: NonDeletedExcalidrawElement,
context: CanvasRenderingContext2D, context: CanvasRenderingContext2D,
appState: StaticCanvasAppState, appState: StaticCanvasAppState,
elementsMap: ElementsMap,
) => { ) => {
if (element.link && !appState.selectedElementIds[element.id]) { if (element.link && !appState.selectedElementIds[element.id]) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element); const [x1, y1, x2, y2] = getElementAbsoluteCoords(element, elementsMap);
const [x, y, width, height] = getLinkHandleFromCoords( const [x, y, width, height] = getLinkHandleFromCoords(
[x1, y1, x2, y2], [x1, y1, x2, y2],
element.angle, element.angle,
+2 -4
View File
@@ -60,10 +60,8 @@ export class Fonts {
return newElementWith(element, { return newElementWith(element, {
...refreshTextDimensions( ...refreshTextDimensions(
element, element,
getContainerElement( getContainerElement(element, this.scene.getNonDeletedElementsMap()),
element, this.scene.getNonDeletedElementsMap(),
this.scene.getElementsMapIncludingDeleted(),
),
), ),
}); });
} }
+13 -7
View File
@@ -40,13 +40,19 @@ export class Renderer {
const visibleElements: NonDeletedExcalidrawElement[] = []; const visibleElements: NonDeletedExcalidrawElement[] = [];
for (const element of elementsMap.values()) { for (const element of elementsMap.values()) {
if ( if (
isElementInViewport(element, width, height, { isElementInViewport(
zoom, element,
offsetLeft, width,
offsetTop, height,
scrollX, {
scrollY, zoom,
}) offsetLeft,
offsetTop,
scrollX,
scrollY,
},
elementsMap,
)
) { ) {
visibleElements.push(element); visibleElements.push(element);
} }
+3 -2
View File
@@ -39,7 +39,7 @@ import {
getFrameLikeTitle, getFrameLikeTitle,
getRootElements, getRootElements,
} from "../frame"; } from "../frame";
import { newTextElement } from "../element"; import { newTextElement } from "../element/newElement";
import { Mutable } from "../utility-types"; import { Mutable } from "../utility-types";
import { newElementWith } from "../element/mutateElement"; import { newElementWith } from "../element/mutateElement";
import Scene from "./Scene"; import Scene from "./Scene";
@@ -392,8 +392,9 @@ export const exportToSvg = async (
const frameElements = getFrameLikeElements(elements); const frameElements = getFrameLikeElements(elements);
let exportingFrameClipPath = ""; let exportingFrameClipPath = "";
const elementsMap = arrayToMap(elements);
for (const frame of frameElements) { for (const frame of frameElements) {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame); const [x1, y1, x2, y2] = getElementAbsoluteCoords(frame, elementsMap);
const cx = (x2 - x1) / 2 - (frame.x - x1); const cx = (x2 - x1) / 2 - (frame.x - x1);
const cy = (y2 - y1) / 2 - (frame.y - y1); const cy = (y2 - y1) / 2 - (frame.y - y1);
+4 -3
View File
@@ -1,20 +1,21 @@
import { getCommonBounds } from "../element"; import { getCommonBounds } from "../element";
import { InteractiveCanvasAppState } from "../types"; import { InteractiveCanvasAppState } from "../types";
import { RenderableElementsMap, ScrollBars } from "./types"; import { ScrollBars } from "./types";
import { getGlobalCSSVariable } from "../utils"; import { getGlobalCSSVariable } from "../utils";
import { getLanguage } from "../i18n"; import { getLanguage } from "../i18n";
import { ExcalidrawElement } from "../element/types";
export const SCROLLBAR_MARGIN = 4; export const SCROLLBAR_MARGIN = 4;
export const SCROLLBAR_WIDTH = 6; export const SCROLLBAR_WIDTH = 6;
export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)"; export const SCROLLBAR_COLOR = "rgba(0,0,0,0.3)";
export const getScrollBars = ( export const getScrollBars = (
elements: RenderableElementsMap, elements: readonly ExcalidrawElement[],
viewportWidth: number, viewportWidth: number,
viewportHeight: number, viewportHeight: number,
appState: InteractiveCanvasAppState, appState: InteractiveCanvasAppState,
): ScrollBars => { ): ScrollBars => {
if (!elements.size) { if (!elements.length) {
return { return {
horizontal: null, horizontal: null,
vertical: null, vertical: null,
+14 -5
View File
@@ -1,4 +1,5 @@
import { import {
ElementsMap,
ElementsMapOrArray, ElementsMapOrArray,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
@@ -44,18 +45,24 @@ export const excludeElementsInFramesFromSelection = <
export const getElementsWithinSelection = ( export const getElementsWithinSelection = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selection: NonDeletedExcalidrawElement, selection: NonDeletedExcalidrawElement,
elementsMap: ElementsMap,
excludeElementsInFrames: boolean = true, excludeElementsInFrames: boolean = true,
) => { ) => {
const [selectionX1, selectionY1, selectionX2, selectionY2] = const [selectionX1, selectionY1, selectionX2, selectionY2] =
getElementAbsoluteCoords(selection); getElementAbsoluteCoords(selection, elementsMap);
let elementsInSelection = elements.filter((element) => { let elementsInSelection = elements.filter((element) => {
let [elementX1, elementY1, elementX2, elementY2] = let [elementX1, elementY1, elementX2, elementY2] = getElementBounds(
getElementBounds(element); element,
elementsMap,
);
const containingFrame = getContainingFrame(element); const containingFrame = getContainingFrame(element);
if (containingFrame) { if (containingFrame) {
const [fx1, fy1, fx2, fy2] = getElementBounds(containingFrame); const [fx1, fy1, fx2, fy2] = getElementBounds(
containingFrame,
elementsMap,
);
elementX1 = Math.max(fx1, elementX1); elementX1 = Math.max(fx1, elementX1);
elementY1 = Math.max(fy1, elementY1); elementY1 = Math.max(fy1, elementY1);
@@ -82,7 +89,7 @@ export const getElementsWithinSelection = (
const containingFrame = getContainingFrame(element); const containingFrame = getContainingFrame(element);
if (containingFrame) { if (containingFrame) {
return elementOverlapsWithFrame(element, containingFrame); return elementOverlapsWithFrame(element, containingFrame, elementsMap);
} }
return true; return true;
@@ -95,6 +102,7 @@ export const getVisibleAndNonSelectedElements = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selectedElements: readonly NonDeletedExcalidrawElement[], selectedElements: readonly NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const selectedElementsSet = new Set( const selectedElementsSet = new Set(
selectedElements.map((element) => element.id), selectedElements.map((element) => element.id),
@@ -105,6 +113,7 @@ export const getVisibleAndNonSelectedElements = (
appState.width, appState.width,
appState.height, appState.height,
appState, appState,
elementsMap,
); );
return !selectedElementsSet.has(element.id) && isVisible; return !selectedElementsSet.has(element.id) && isVisible;
+29 -16
View File
@@ -8,15 +8,18 @@ import {
import { MaybeTransformHandleType } from "./element/transformHandles"; import { MaybeTransformHandleType } from "./element/transformHandles";
import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks"; import { isBoundToContainer, isFrameLikeElement } from "./element/typeChecks";
import { import {
ElementsMap,
ExcalidrawElement, ExcalidrawElement,
NonDeletedExcalidrawElement, NonDeletedExcalidrawElement,
} from "./element/types"; } from "./element/types";
import { getMaximumGroups } from "./groups"; import { getMaximumGroups } from "./groups";
import { KEYS } from "./keys"; import { KEYS } from "./keys";
import { rangeIntersection, rangesOverlap, rotatePoint } from "./math"; import { rangeIntersection, rangesOverlap, rotatePoint } from "./math";
import { getVisibleAndNonSelectedElements } from "./scene/selection"; import {
getSelectedElements,
getVisibleAndNonSelectedElements,
} from "./scene/selection";
import { AppState, KeyboardModifiersObject, Point } from "./types"; import { AppState, KeyboardModifiersObject, Point } from "./types";
import { arrayToMap } from "./utils";
const SNAP_DISTANCE = 8; const SNAP_DISTANCE = 8;
@@ -167,6 +170,7 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => {
export const getElementsCorners = ( export const getElementsCorners = (
elements: ExcalidrawElement[], elements: ExcalidrawElement[],
elementsMap: ElementsMap,
{ {
omitCenter, omitCenter,
boundingBoxCorners, boundingBoxCorners,
@@ -185,7 +189,10 @@ export const getElementsCorners = (
if (elements.length === 1) { if (elements.length === 1) {
const element = elements[0]; const element = elements[0];
let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(element); let [x1, y1, x2, y2, cx, cy] = getElementAbsoluteCoords(
element,
elementsMap,
);
if (dragOffset) { if (dragOffset) {
x1 += dragOffset.x; x1 += dragOffset.x;
@@ -262,6 +269,7 @@ const getReferenceElements = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selectedElements: NonDeletedExcalidrawElement[], selectedElements: NonDeletedExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const selectedFrames = selectedElements const selectedFrames = selectedElements
.filter((element) => isFrameLikeElement(element)) .filter((element) => isFrameLikeElement(element))
@@ -271,6 +279,7 @@ const getReferenceElements = (
elements, elements,
selectedElements, selectedElements,
appState, appState,
elementsMap,
).filter( ).filter(
(element) => !(element.frameId && selectedFrames.includes(element.frameId)), (element) => !(element.frameId && selectedFrames.includes(element.frameId)),
); );
@@ -280,17 +289,16 @@ export const getVisibleGaps = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const referenceElements: ExcalidrawElement[] = getReferenceElements( const referenceElements: ExcalidrawElement[] = getReferenceElements(
elements, elements,
selectedElements, selectedElements,
appState, appState,
elementsMap,
); );
const referenceBounds = getMaximumGroups( const referenceBounds = getMaximumGroups(referenceElements, elementsMap)
referenceElements,
arrayToMap(elements),
)
.filter( .filter(
(elementsGroup) => (elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
@@ -569,19 +577,20 @@ export const getReferenceSnapPoints = (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
selectedElements: ExcalidrawElement[], selectedElements: ExcalidrawElement[],
appState: AppState, appState: AppState,
elementsMap: ElementsMap,
) => { ) => {
const referenceElements = getReferenceElements( const referenceElements = getReferenceElements(
elements, elements,
selectedElements, selectedElements,
appState, appState,
elementsMap,
); );
return getMaximumGroups(referenceElements, elementsMap)
return getMaximumGroups(referenceElements, arrayToMap(elements))
.filter( .filter(
(elementsGroup) => (elementsGroup) =>
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])), !(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
) )
.flatMap((elementGroup) => getElementsCorners(elementGroup)); .flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
}; };
const getPointSnaps = ( const getPointSnaps = (
@@ -641,11 +650,13 @@ const getPointSnaps = (
}; };
export const snapDraggedElements = ( export const snapDraggedElements = (
selectedElements: ExcalidrawElement[], elements: ExcalidrawElement[],
dragOffset: Vector2D, dragOffset: Vector2D,
appState: AppState, appState: AppState,
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
elementsMap: ElementsMap,
) => { ) => {
const selectedElements = getSelectedElements(elements, appState);
if ( if (
!isSnappingEnabled({ appState, event, selectedElements }) || !isSnappingEnabled({ appState, event, selectedElements }) ||
selectedElements.length === 0 selectedElements.length === 0
@@ -658,7 +669,6 @@ export const snapDraggedElements = (
snapLines: [], snapLines: [],
}; };
} }
dragOffset.x = round(dragOffset.x); dragOffset.x = round(dragOffset.x);
dragOffset.y = round(dragOffset.y); dragOffset.y = round(dragOffset.y);
const nearestSnapsX: Snaps = []; const nearestSnapsX: Snaps = [];
@@ -669,7 +679,7 @@ export const snapDraggedElements = (
y: snapDistance, y: snapDistance,
}; };
const selectionPoints = getElementsCorners(selectedElements, { const selectionPoints = getElementsCorners(selectedElements, elementsMap, {
dragOffset, dragOffset,
}); });
@@ -719,7 +729,7 @@ export const snapDraggedElements = (
getPointSnaps( getPointSnaps(
selectedElements, selectedElements,
getElementsCorners(selectedElements, { getElementsCorners(selectedElements, elementsMap, {
dragOffset: newDragOffset, dragOffset: newDragOffset,
}), }),
appState, appState,
@@ -1204,6 +1214,7 @@ export const snapNewElement = (
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
origin: Vector2D, origin: Vector2D,
dragOffset: Vector2D, dragOffset: Vector2D,
elementsMap: ElementsMap,
) => { ) => {
if ( if (
!isSnappingEnabled({ event, selectedElements: [draggingElement], appState }) !isSnappingEnabled({ event, selectedElements: [draggingElement], appState })
@@ -1248,7 +1259,7 @@ export const snapNewElement = (
nearestSnapsX.length = 0; nearestSnapsX.length = 0;
nearestSnapsY.length = 0; nearestSnapsY.length = 0;
const corners = getElementsCorners([draggingElement], { const corners = getElementsCorners([draggingElement], elementsMap, {
boundingBoxCorners: true, boundingBoxCorners: true,
omitCenter: true, omitCenter: true,
}); });
@@ -1276,6 +1287,7 @@ export const getSnapLinesAtPointer = (
appState: AppState, appState: AppState,
pointer: Vector2D, pointer: Vector2D,
event: KeyboardModifiersObject, event: KeyboardModifiersObject,
elementsMap: ElementsMap,
) => { ) => {
if (!isSnappingEnabled({ event, selectedElements: [], appState })) { if (!isSnappingEnabled({ event, selectedElements: [], appState })) {
return { return {
@@ -1288,6 +1300,7 @@ export const getSnapLinesAtPointer = (
elements, elements,
[], [],
appState, appState,
elementsMap,
); );
const snapDistance = getSnapDistance(appState.zoom.value); const snapDistance = getSnapDistance(appState.zoom.value);
@@ -1301,7 +1314,7 @@ export const getSnapLinesAtPointer = (
const verticalSnapLines: PointerSnapLine[] = []; const verticalSnapLines: PointerSnapLine[] = [];
for (const referenceElement of referenceElements) { for (const referenceElement of referenceElements) {
const corners = getElementsCorners([referenceElement]); const corners = getElementsCorners([referenceElement], elementsMap);
for (const corner of corners) { for (const corner of corners) {
const offsetX = corner[0] - pointer.x; const offsetX = corner[0] - pointer.x;
@@ -387,6 +387,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"angle": 0, "angle": 0,
"backgroundColor": "red", "backgroundColor": "red",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -421,6 +422,7 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
"angle": 0, "angle": 0,
"backgroundColor": "red", "backgroundColor": "red",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -584,6 +586,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -643,6 +646,7 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -786,6 +790,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -818,6 +823,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -877,6 +883,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -920,6 +927,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -949,6 +957,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -992,6 +1001,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1021,6 +1031,7 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1164,6 +1175,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1196,6 +1208,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1255,6 +1268,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1298,6 +1312,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1327,6 +1342,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1370,6 +1386,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1399,6 +1416,7 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1544,6 +1562,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1603,6 +1622,7 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1744,6 +1764,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1803,6 +1824,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1844,6 +1866,7 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -1987,6 +2010,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2019,6 +2043,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2078,6 +2103,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2121,6 +2147,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2150,6 +2177,7 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2298,6 +2326,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -2332,6 +2361,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -2393,6 +2423,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2436,6 +2467,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2465,6 +2497,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2511,6 +2544,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -2542,6 +2576,7 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -2689,6 +2724,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2721,6 +2757,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2780,6 +2817,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2823,6 +2861,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2852,6 +2891,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2895,6 +2935,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2924,6 +2965,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2967,6 +3009,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -2996,6 +3039,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3039,6 +3083,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3068,6 +3113,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3111,6 +3157,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3140,6 +3187,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3183,6 +3231,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3212,6 +3261,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3255,6 +3305,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3284,6 +3335,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3327,6 +3379,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3356,6 +3409,7 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
"angle": 0, "angle": 0,
"backgroundColor": "#a5d8ff", "backgroundColor": "#a5d8ff",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3499,6 +3553,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3531,6 +3586,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3590,6 +3646,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3633,6 +3690,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3662,6 +3720,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3705,6 +3764,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3734,6 +3794,7 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3877,6 +3938,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3909,6 +3971,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -3968,6 +4031,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4011,6 +4075,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4040,6 +4105,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4083,6 +4149,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4112,6 +4179,7 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4258,6 +4326,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4290,6 +4359,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4349,6 +4419,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4392,6 +4463,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4421,6 +4493,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4467,6 +4540,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -4498,6 +4572,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -4544,6 +4619,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4573,6 +4649,7 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -4992,6 +5069,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5024,6 +5102,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5083,6 +5162,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5126,6 +5206,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5155,6 +5236,7 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5576,6 +5658,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -5610,6 +5693,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -5671,6 +5755,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5714,6 +5799,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5743,6 +5829,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5789,6 +5876,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -5820,6 +5908,7 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -6872,6 +6961,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -6904,6 +6994,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"angle": 0, "angle": 0,
"backgroundColor": "red", "backgroundColor": "red",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -6936,6 +7027,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] el
"angle": 0, "angle": 0,
"backgroundColor": "red", "backgroundColor": "red",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -6995,6 +7087,7 @@ exports[`contextMenu element > shows context menu for element > [end of test] hi
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -7,6 +7,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -56,6 +57,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -90,6 +92,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -122,6 +125,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -171,6 +175,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -5,6 +5,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 5`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -37,6 +38,7 @@ exports[`duplicate element on move when ALT is clicked > rectangle 6`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -69,6 +71,7 @@ exports[`move element > rectangle 5`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -106,6 +109,7 @@ exports[`move element > rectangles with binding arrow 5`] = `
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -143,6 +147,7 @@ exports[`move element > rectangles with binding arrow 6`] = `
"type": "arrow", "type": "arrow",
}, },
], ],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -175,6 +180,7 @@ exports[`move element > rectangles with binding arrow 7`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": { "endBinding": {
"elementId": "id1", "elementId": "id1",
@@ -5,6 +5,7 @@ exports[`multi point mode in linear elements > arrow 3`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -59,6 +60,7 @@ exports[`multi point mode in linear elements > line 3`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
File diff suppressed because it is too large Load Diff
@@ -5,6 +5,7 @@ exports[`select single element on the scene > arrow 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": "arrow", "endArrowhead": "arrow",
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -52,6 +53,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -99,6 +101,7 @@ exports[`select single element on the scene > diamond 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -131,6 +134,7 @@ exports[`select single element on the scene > ellipse 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -163,6 +167,7 @@ exports[`select single element on the scene > rectangle 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": null, "boundElements": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
+7 -2
View File
@@ -5,6 +5,7 @@ import { getTransformHandles } from "../element/transformHandles";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { actionWrapTextInContainer } from "../actions/actionBoundText"; import { actionWrapTextInContainer } from "../actions/actionBoundText";
import { arrayToMap } from "../utils";
const { h } = window; const { h } = window;
@@ -91,8 +92,12 @@ describe("element binding", () => {
expect(arrow.startBinding?.elementId).toBe(rectLeft.id); expect(arrow.startBinding?.elementId).toBe(rectLeft.id);
expect(arrow.endBinding?.elementId).toBe(rectRight.id); expect(arrow.endBinding?.elementId).toBe(rectRight.id);
const rotation = getTransformHandles(arrow, h.state.zoom, "mouse") const rotation = getTransformHandles(
.rotation!; arrow,
h.state.zoom,
arrayToMap(h.elements),
"mouse",
).rotation!;
const rotationHandleX = rotation[0] + rotation[2] / 2; const rotationHandleX = rotation[0] + rotation[2] / 2;
const rotationHandleY = rotation[1] + rotation[3] / 2; const rotationHandleY = rotation[1] + rotation[3] / 2;
mouse.down(rotationHandleX, rotationHandleY); mouse.down(rotationHandleX, rotationHandleY);
+9 -4
View File
@@ -12,6 +12,7 @@ import { getElementBounds } from "../element";
import { NormalizedZoomValue } from "../types"; import { NormalizedZoomValue } from "../types";
import { API } from "./helpers/api"; import { API } from "./helpers/api";
import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard"; import { createPasteEvent, serializeAsClipboardJSON } from "../clipboard";
import { arrayToMap } from "../utils";
const { h } = window; const { h } = window;
@@ -138,6 +139,8 @@ describe("paste text as single lines", () => {
}); });
it("should space items correctly", async () => { it("should space items correctly", async () => {
const elementsMap = arrayToMap(h.elements);
const text = "hkhkjhki\njgkjhffjh\njgkjhffjh"; const text = "hkhkjhki\njgkjhffjh\njgkjhffjh";
const lineHeightPx = const lineHeightPx =
getLineHeightInPx( getLineHeightInPx(
@@ -149,16 +152,17 @@ describe("paste text as single lines", () => {
pasteWithCtrlCmdV(text); pasteWithCtrlCmdV(text);
await waitFor(async () => { await waitFor(async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, firstElY] = getElementBounds(h.elements[0]); const [fx, firstElY] = getElementBounds(h.elements[0], elementsMap);
for (let i = 1; i < h.elements.length; i++) { for (let i = 1; i < h.elements.length; i++) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, elY] = getElementBounds(h.elements[i]); const [fx, elY] = getElementBounds(h.elements[i], elementsMap);
expect(elY).toEqual(firstElY + lineHeightPx * i); expect(elY).toEqual(firstElY + lineHeightPx * i);
} }
}); });
}); });
it("should leave a space for blank new lines", async () => { it("should leave a space for blank new lines", async () => {
const elementsMap = arrayToMap(h.elements);
const text = "hkhkjhki\n\njgkjhffjh"; const text = "hkhkjhki\n\njgkjhffjh";
const lineHeightPx = const lineHeightPx =
getLineHeightInPx( getLineHeightInPx(
@@ -168,11 +172,12 @@ describe("paste text as single lines", () => {
10 / h.app.state.zoom.value; 10 / h.app.state.zoom.value;
mouse.moveTo(100, 100); mouse.moveTo(100, 100);
pasteWithCtrlCmdV(text); pasteWithCtrlCmdV(text);
await waitFor(async () => { await waitFor(async () => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [fx, firstElY] = getElementBounds(h.elements[0]); const [fx, firstElY] = getElementBounds(h.elements[0], elementsMap);
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const [lx, lastElY] = getElementBounds(h.elements[1]); const [lx, lastElY] = getElementBounds(h.elements[1], elementsMap);
expect(lastElY).toEqual(firstElY + lineHeightPx * 2); expect(lastElY).toEqual(firstElY + lineHeightPx * 2);
}); });
}); });
@@ -5,6 +5,7 @@ exports[`restoreElements > should restore arrow element correctly 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [], "boundElements": [],
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -52,6 +53,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"angle": 0, "angle": 0,
"backgroundColor": "blue", "backgroundColor": "blue",
"boundElements": [], "boundElements": [],
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -88,6 +90,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"angle": 0, "angle": 0,
"backgroundColor": "blue", "backgroundColor": "blue",
"boundElements": [], "boundElements": [],
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -124,6 +127,7 @@ exports[`restoreElements > should restore correctly with rectangle, ellipse and
"angle": 0, "angle": 0,
"backgroundColor": "blue", "backgroundColor": "blue",
"boundElements": [], "boundElements": [],
"customData": undefined,
"fillStyle": "cross-hatch", "fillStyle": "cross-hatch",
"frameId": null, "frameId": null,
"groupIds": [ "groupIds": [
@@ -160,6 +164,7 @@ exports[`restoreElements > should restore freedraw element correctly 1`] = `
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [], "boundElements": [],
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"frameId": null, "frameId": null,
"groupIds": [], "groupIds": [],
@@ -196,6 +201,7 @@ exports[`restoreElements > should restore line and draw elements correctly 1`] =
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [], "boundElements": [],
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -243,6 +249,7 @@ exports[`restoreElements > should restore line and draw elements correctly 2`] =
"angle": 0, "angle": 0,
"backgroundColor": "transparent", "backgroundColor": "transparent",
"boundElements": [], "boundElements": [],
"customData": undefined,
"endArrowhead": null, "endArrowhead": null,
"endBinding": null, "endBinding": null,
"fillStyle": "solid", "fillStyle": "solid",
@@ -292,6 +299,7 @@ exports[`restoreElements > should restore text element correctly passing value f
"baseline": 0, "baseline": 0,
"boundElements": [], "boundElements": [],
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 14, "fontSize": 14,
@@ -333,6 +341,7 @@ exports[`restoreElements > should restore text element correctly with unknown fo
"baseline": 0, "baseline": 0,
"boundElements": [], "boundElements": [],
"containerId": null, "containerId": null,
"customData": undefined,
"fillStyle": "solid", "fillStyle": "solid",
"fontFamily": 1, "fontFamily": 1,
"fontSize": 10, "fontSize": 10,
@@ -303,7 +303,7 @@ describe("<Excalidraw/>", () => {
}); });
describe("Test name prop", () => { describe("Test name prop", () => {
it('should allow editing name when the name prop is "undefined"', async () => { it("should allow editing name", async () => {
const { container } = await render(<Excalidraw />); const { container } = await render(<Excalidraw />);
//open menu //open menu
toggleMenu(container); toggleMenu(container);
@@ -315,7 +315,7 @@ describe("<Excalidraw/>", () => {
expect(textInput?.nodeName).toBe("INPUT"); expect(textInput?.nodeName).toBe("INPUT");
}); });
it('should set the name and not allow editing when the name prop is present"', async () => { it('should set the name when the name prop is present"', async () => {
const name = "test"; const name = "test";
const { container } = await render(<Excalidraw name={name} />); const { container } = await render(<Excalidraw name={name} />);
//open menu //open menu
@@ -326,7 +326,6 @@ describe("<Excalidraw/>", () => {
) as HTMLInputElement; ) as HTMLInputElement;
expect(textInput?.value).toEqual(name); expect(textInput?.value).toEqual(name);
expect(textInput?.nodeName).toBe("INPUT"); expect(textInput?.nodeName).toBe("INPUT");
expect(textInput?.disabled).toBe(true);
}); });
}); });
+9 -4
View File
@@ -27,7 +27,7 @@ import * as blob from "../data/blob";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { getBoundTextElementPosition } from "../element/textElement"; import { getBoundTextElementPosition } from "../element/textElement";
import { createPasteEvent } from "../clipboard"; import { createPasteEvent } from "../clipboard";
import { cloneJSON } from "../utils"; import { arrayToMap, cloneJSON } from "../utils";
const { h } = window; const { h } = window;
const mouse = new Pointer("mouse"); const mouse = new Pointer("mouse");
@@ -194,9 +194,10 @@ const checkElementsBoundingBox = async (
element2: ExcalidrawElement, element2: ExcalidrawElement,
toleranceInPx: number = 0, toleranceInPx: number = 0,
) => { ) => {
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1); const elementsMap = arrayToMap([element1, element2]);
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element1, elementsMap);
const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2); const [x12, y12, x22, y22] = getElementAbsoluteCoords(element2, elementsMap);
await waitFor(() => { await waitFor(() => {
// Check if width and height did not change // Check if width and height did not change
@@ -853,7 +854,11 @@ describe("mutliple elements", () => {
h.app.actionManager.executeAction(actionFlipVertical); h.app.actionManager.executeAction(actionFlipVertical);
const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer; const arrowText = h.elements[1] as ExcalidrawTextElementWithContainer;
const arrowTextPos = getBoundTextElementPosition(arrow.get(), arrowText)!; const arrowTextPos = getBoundTextElementPosition(
arrow.get(),
arrowText,
arrayToMap(h.elements),
)!;
const rectText = h.elements[3] as ExcalidrawTextElementWithContainer; const rectText = h.elements[3] as ExcalidrawTextElementWithContainer;
expect(arrow.x).toBeCloseTo(180); expect(arrow.x).toBeCloseTo(180);
+3
View File
@@ -31,8 +31,11 @@ import { getSelectedElements } from "../../scene/selection";
import { isLinearElementType } from "../../element/typeChecks"; import { isLinearElementType } from "../../element/typeChecks";
import { Mutable } from "../../utility-types"; import { Mutable } from "../../utility-types";
import { assertNever } from "../../utils"; import { assertNever } from "../../utils";
import { createTestHook } from "../../components/App";
const readFile = util.promisify(fs.readFile); const readFile = util.promisify(fs.readFile);
// so that window.h is available when App.tsx is not imported as well.
createTestHook();
const { h } = window; const { h } = window;
+11 -4
View File
@@ -32,6 +32,11 @@ import {
import { getCommonBounds, getElementPointsCoords } from "../../element/bounds"; import { getCommonBounds, getElementPointsCoords } from "../../element/bounds";
import { rotatePoint } from "../../math"; import { rotatePoint } from "../../math";
import { getTextEditor } from "../queries/dom"; import { getTextEditor } from "../queries/dom";
import { arrayToMap } from "../../utils";
import { createTestHook } from "../../components/App";
// so that window.h is available when App.tsx is not imported as well.
createTestHook();
const { h } = window; const { h } = window;
@@ -286,9 +291,12 @@ const transform = (
let handleCoords: TransformHandle | undefined; let handleCoords: TransformHandle | undefined;
if (elements.length === 1) { if (elements.length === 1) {
handleCoords = getTransformHandles(elements[0], h.state.zoom, "mouse")[ handleCoords = getTransformHandles(
handle elements[0],
]; h.state.zoom,
arrayToMap(h.elements),
"mouse",
)[handle];
} else { } else {
const [x1, y1, x2, y2] = getCommonBounds(elements); const [x1, y1, x2, y2] = getCommonBounds(elements);
const isFrameSelected = elements.some(isFrameLikeElement); const isFrameSelected = elements.some(isFrameLikeElement);
@@ -456,7 +464,6 @@ export class UI {
mouse.reset(); mouse.reset();
mouse.up(x + width, y + height); mouse.up(x + width, y + height);
} }
const origElement = h.elements[h.elements.length - 1] as any; const origElement = h.elements[h.elements.length - 1] as any;
if (angle !== 0) { if (angle !== 0) {
@@ -343,6 +343,8 @@ describe("Test Linear Elements", () => {
}); });
it("should update all the midpoints when element position changed", async () => { it("should update all the midpoints when element position changed", async () => {
const elementsMap = arrayToMap(h.elements);
createThreePointerLinearElement("line", { createThreePointerLinearElement("line", {
type: ROUNDNESS.PROPORTIONAL_RADIUS, type: ROUNDNESS.PROPORTIONAL_RADIUS,
}); });
@@ -351,7 +353,10 @@ describe("Test Linear Elements", () => {
expect(line.points.length).toEqual(3); expect(line.points.length).toEqual(3);
enterLineEditingMode(line); enterLineEditingMode(line);
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
expect([line.x, line.y]).toEqual(points[0]); expect([line.x, line.y]).toEqual(points[0]);
const midPoints = LinearElementEditor.getEditorMidPoints( const midPoints = LinearElementEditor.getEditorMidPoints(
@@ -465,7 +470,11 @@ describe("Test Linear Elements", () => {
}); });
it("should update only the first segment midpoint when its point is dragged", async () => { it("should update only the first segment midpoint when its point is dragged", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const elementsMap = arrayToMap(h.elements);
const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
const midPoints = LinearElementEditor.getEditorMidPoints( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@@ -482,7 +491,10 @@ describe("Test Linear Elements", () => {
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] - delta, points[0][0] - delta,
points[0][1] - delta, points[0][1] - delta,
@@ -499,7 +511,11 @@ describe("Test Linear Elements", () => {
}); });
it("should hide midpoints in the segment when points moved close", async () => { it("should hide midpoints in the segment when points moved close", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const elementsMap = arrayToMap(h.elements);
const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
const midPoints = LinearElementEditor.getEditorMidPoints( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@@ -516,7 +532,10 @@ describe("Test Linear Elements", () => {
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] + delta, points[0][0] + delta,
points[0][1] + delta, points[0][1] + delta,
@@ -535,7 +554,10 @@ describe("Test Linear Elements", () => {
it("should remove the midpoint when one of the points in the segment is deleted", async () => { it("should remove the midpoint when one of the points in the segment is deleted", async () => {
const line = h.elements[0] as ExcalidrawLinearElement; const line = h.elements[0] as ExcalidrawLinearElement;
enterLineEditingMode(line); enterLineEditingMode(line);
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
arrayToMap(h.elements),
);
// dragging line from last segment midpoint // dragging line from last segment midpoint
drag(lastSegmentMidpoint, [ drag(lastSegmentMidpoint, [
@@ -637,7 +659,11 @@ describe("Test Linear Elements", () => {
}); });
it("should update all the midpoints when its point is dragged", async () => { it("should update all the midpoints when its point is dragged", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const elementsMap = arrayToMap(h.elements);
const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
const midPoints = LinearElementEditor.getEditorMidPoints( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@@ -649,7 +675,10 @@ describe("Test Linear Elements", () => {
// Drag from first point // Drag from first point
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]); drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] - delta, points[0][0] - delta,
points[0][1] - delta, points[0][1] - delta,
@@ -678,7 +707,11 @@ describe("Test Linear Elements", () => {
}); });
it("should hide midpoints in the segment when points moved close", async () => { it("should hide midpoints in the segment when points moved close", async () => {
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const elementsMap = arrayToMap(h.elements);
const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
const midPoints = LinearElementEditor.getEditorMidPoints( const midPoints = LinearElementEditor.getEditorMidPoints(
line, line,
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
@@ -695,7 +728,10 @@ describe("Test Linear Elements", () => {
); );
expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`); expect(renderStaticScene.mock.calls.length).toMatchInlineSnapshot(`8`);
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line); const newPoints = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
expect([newPoints[0][0], newPoints[0][1]]).toEqual([ expect([newPoints[0][0], newPoints[0][1]]).toEqual([
points[0][0] + delta, points[0][0] + delta,
points[0][1] + delta, points[0][1] + delta,
@@ -712,6 +748,8 @@ describe("Test Linear Elements", () => {
}); });
it("should update all the midpoints when a point is deleted", async () => { it("should update all the midpoints when a point is deleted", async () => {
const elementsMap = arrayToMap(h.elements);
drag(lastSegmentMidpoint, [ drag(lastSegmentMidpoint, [
lastSegmentMidpoint[0] + delta, lastSegmentMidpoint[0] + delta,
lastSegmentMidpoint[1] + delta, lastSegmentMidpoint[1] + delta,
@@ -723,7 +761,10 @@ describe("Test Linear Elements", () => {
h.app.scene.getNonDeletedElementsMap(), h.app.scene.getNonDeletedElementsMap(),
h.state, h.state,
); );
const points = LinearElementEditor.getPointsGlobalCoordinates(line); const points = LinearElementEditor.getPointsGlobalCoordinates(
line,
elementsMap,
);
// delete 3rd point // delete 3rd point
deletePoint(points[2]); deletePoint(points[2]);
@@ -837,6 +878,7 @@ describe("Test Linear Elements", () => {
const position = LinearElementEditor.getBoundTextElementPosition( const position = LinearElementEditor.getBoundTextElementPosition(
container, container,
textElement, textElement,
arrayToMap(h.elements),
); );
expect(position).toMatchInlineSnapshot(` expect(position).toMatchInlineSnapshot(`
{ {
@@ -859,6 +901,7 @@ describe("Test Linear Elements", () => {
const position = LinearElementEditor.getBoundTextElementPosition( const position = LinearElementEditor.getBoundTextElementPosition(
container, container,
textElement, textElement,
arrayToMap(h.elements),
); );
expect(position).toMatchInlineSnapshot(` expect(position).toMatchInlineSnapshot(`
{ {
@@ -893,6 +936,7 @@ describe("Test Linear Elements", () => {
const position = LinearElementEditor.getBoundTextElementPosition( const position = LinearElementEditor.getBoundTextElementPosition(
container, container,
textElement, textElement,
arrayToMap(h.elements),
); );
expect(position).toMatchInlineSnapshot(` expect(position).toMatchInlineSnapshot(`
{ {
@@ -1012,8 +1056,13 @@ describe("Test Linear Elements", () => {
); );
expect(container.width).toBe(70); expect(container.width).toBe(70);
expect(container.height).toBe(50); expect(container.height).toBe(50);
expect(getBoundTextElementPosition(container, textElement)) expect(
.toMatchInlineSnapshot(` getBoundTextElementPosition(
container,
textElement,
arrayToMap(h.elements),
),
).toMatchInlineSnapshot(`
{ {
"x": 75, "x": 75,
"y": 60, "y": 60,
@@ -1051,8 +1100,13 @@ describe("Test Linear Elements", () => {
} }
`); `);
expect(getBoundTextElementPosition(container, textElement)) expect(
.toMatchInlineSnapshot(` getBoundTextElementPosition(
container,
textElement,
arrayToMap(h.elements),
),
).toMatchInlineSnapshot(`
{ {
"x": 271.11716195150507, "x": 271.11716195150507,
"y": 45, "y": 45,
@@ -1090,7 +1144,8 @@ describe("Test Linear Elements", () => {
arrow, arrow,
); );
expect(container.width).toBe(40); expect(container.width).toBe(40);
expect(getBoundTextElementPosition(container, textElement)) const elementsMap = arrayToMap(h.elements);
expect(getBoundTextElementPosition(container, textElement, elementsMap))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
{ {
"x": 25, "x": 25,
@@ -1102,7 +1157,10 @@ describe("Test Linear Elements", () => {
collaboration made collaboration made
easy" easy"
`); `);
const points = LinearElementEditor.getPointsGlobalCoordinates(container); const points = LinearElementEditor.getPointsGlobalCoordinates(
container,
elementsMap,
);
// Drag from last point // Drag from last point
drag(points[1], [points[1][0] + 300, points[1][1]]); drag(points[1], [points[1][0] + 300, points[1][1]]);
@@ -1115,7 +1173,7 @@ describe("Test Linear Elements", () => {
} }
`); `);
expect(getBoundTextElementPosition(container, textElement)) expect(getBoundTextElementPosition(container, textElement, elementsMap))
.toMatchInlineSnapshot(` .toMatchInlineSnapshot(`
{ {
"x": 75, "x": 75,
+2 -2
View File
@@ -1,4 +1,3 @@
import React from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import { render, fireEvent } from "./test-utils"; import { render, fireEvent } from "./test-utils";
import { Excalidraw } from "../index"; import { Excalidraw } from "../index";
@@ -75,12 +74,13 @@ describe("move element", () => {
const rectA = UI.createElement("rectangle", { size: 100 }); const rectA = UI.createElement("rectangle", { size: 100 });
const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 }); const rectB = UI.createElement("rectangle", { x: 200, y: 0, size: 300 });
const line = UI.createElement("line", { x: 110, y: 50, size: 80 }); const line = UI.createElement("line", { x: 110, y: 50, size: 80 });
const elementsMap = h.app.scene.getNonDeletedElementsMap();
// bind line to two rectangles // bind line to two rectangles
bindOrUnbindLinearElement( bindOrUnbindLinearElement(
line.get() as NonDeleted<ExcalidrawLinearElement>, line.get() as NonDeleted<ExcalidrawLinearElement>,
rectA.get() as ExcalidrawRectangleElement, rectA.get() as ExcalidrawRectangleElement,
rectB.get() as ExcalidrawRectangleElement, rectB.get() as ExcalidrawRectangleElement,
elementsMap,
); );
// select the second rectangles // select the second rectangles
+13 -3
View File
@@ -13,6 +13,7 @@ import { API } from "./helpers/api";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
import { isLinearElement } from "../element/typeChecks"; import { isLinearElement } from "../element/typeChecks";
import { LinearElementEditor } from "../element/linearElementEditor"; import { LinearElementEditor } from "../element/linearElementEditor";
import { arrayToMap } from "../utils";
ReactDOM.unmountComponentAtNode(document.getElementById("root")!); ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
@@ -301,10 +302,12 @@ describe("arrow element", () => {
], ],
}); });
const label = await UI.editText(arrow, "Hello"); const label = await UI.editText(arrow, "Hello");
const elementsMap = arrayToMap(h.elements);
UI.resize(arrow, "se", [50, 30]); UI.resize(arrow, "se", [50, 30]);
let labelPos = LinearElementEditor.getBoundTextElementPosition( let labelPos = LinearElementEditor.getBoundTextElementPosition(
arrow, arrow,
label, label,
elementsMap,
); );
expect(labelPos.x + label.width / 2).toBeCloseTo( expect(labelPos.x + label.width / 2).toBeCloseTo(
@@ -317,7 +320,11 @@ describe("arrow element", () => {
expect(label.fontSize).toEqual(20); expect(label.fontSize).toEqual(20);
UI.resize(arrow, "w", [20, 0]); UI.resize(arrow, "w", [20, 0]);
labelPos = LinearElementEditor.getBoundTextElementPosition(arrow, label); labelPos = LinearElementEditor.getBoundTextElementPosition(
arrow,
label,
elementsMap,
);
expect(labelPos.x + label.width / 2).toBeCloseTo( expect(labelPos.x + label.width / 2).toBeCloseTo(
arrow.x + arrow.points[2][0], arrow.x + arrow.points[2][0],
@@ -743,15 +750,17 @@ describe("multiple selection", () => {
const selectionTop = 20 - topArrowLabel.height / 2; const selectionTop = 20 - topArrowLabel.height / 2;
const move = [80, 0] as [number, number]; const move = [80, 0] as [number, number];
const scale = move[0] / selectionWidth + 1; const scale = move[0] / selectionWidth + 1;
const elementsMap = arrayToMap(h.elements);
UI.resize([topArrow.get(), bottomArrow.get()], "se", move); UI.resize([topArrow.get(), bottomArrow.get()], "se", move);
const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( const topArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
topArrow, topArrow,
topArrowLabel, topArrowLabel,
elementsMap,
); );
const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition( const bottomArrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
bottomArrow, bottomArrow,
bottomArrowLabel, bottomArrowLabel,
elementsMap,
); );
expect(topArrow.x).toBeCloseTo(0); expect(topArrow.x).toBeCloseTo(0);
@@ -944,12 +953,13 @@ describe("multiple selection", () => {
const scaleX = move[0] / selectionWidth + 1; const scaleX = move[0] / selectionWidth + 1;
const scaleY = -scaleX; const scaleY = -scaleX;
const lineOrigBounds = getBoundsFromPoints(line); const lineOrigBounds = getBoundsFromPoints(line);
const elementsMap = arrayToMap(h.elements);
UI.resize([line, image, rectangle, boundArrow], "se", move); UI.resize([line, image, rectangle, boundArrow], "se", move);
const lineNewBounds = getBoundsFromPoints(line); const lineNewBounds = getBoundsFromPoints(line);
const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition( const arrowLabelPos = LinearElementEditor.getBoundTextElementPosition(
boundArrow, boundArrow,
arrowLabel, arrowLabel,
elementsMap,
); );
expect(line.x).toBeCloseTo(60 * scaleX); expect(line.x).toBeCloseTo(60 * scaleX);
+5 -2
View File
@@ -247,7 +247,7 @@ export interface AppState {
scrollY: number; scrollY: number;
cursorButton: "up" | "down"; cursorButton: "up" | "down";
scrolledOutside: boolean; scrolledOutside: boolean;
name: string; name: string | null;
isResizing: boolean; isResizing: boolean;
isRotating: boolean; isRotating: boolean;
zoom: Zoom; zoom: Zoom;
@@ -435,6 +435,7 @@ export interface ExcalidrawProps {
objectsSnapModeEnabled?: boolean; objectsSnapModeEnabled?: boolean;
libraryReturnUrl?: string; libraryReturnUrl?: string;
theme?: Theme; theme?: Theme;
// @TODO come with better API before v0.18.0
name?: string; name?: string;
renderCustomStats?: ( renderCustomStats?: (
elements: readonly NonDeletedExcalidrawElement[], elements: readonly NonDeletedExcalidrawElement[],
@@ -577,6 +578,7 @@ export type AppClassProperties = {
setOpenDialog: App["setOpenDialog"]; setOpenDialog: App["setOpenDialog"];
insertEmbeddableElement: App["insertEmbeddableElement"]; insertEmbeddableElement: App["insertEmbeddableElement"];
onMagicframeToolSelect: App["onMagicframeToolSelect"]; onMagicframeToolSelect: App["onMagicframeToolSelect"];
getName: App["getName"];
}; };
export type PointerDownState = Readonly<{ export type PointerDownState = Readonly<{
@@ -651,10 +653,11 @@ export type ExcalidrawImperativeAPI = {
history: { history: {
clear: InstanceType<typeof App>["resetHistory"]; clear: InstanceType<typeof App>["resetHistory"];
}; };
scrollToContent: InstanceType<typeof App>["scrollToContent"];
getSceneElements: InstanceType<typeof App>["getSceneElements"]; getSceneElements: InstanceType<typeof App>["getSceneElements"];
getAppState: () => InstanceType<typeof App>["state"]; getAppState: () => InstanceType<typeof App>["state"];
getFiles: () => InstanceType<typeof App>["files"]; getFiles: () => InstanceType<typeof App>["files"];
getName: InstanceType<typeof App>["getName"];
scrollToContent: InstanceType<typeof App>["scrollToContent"];
registerAction: (action: Action) => void; registerAction: (action: Action) => void;
refresh: InstanceType<typeof App>["refresh"]; refresh: InstanceType<typeof App>["refresh"];
setToast: InstanceType<typeof App>["setToast"]; setToast: InstanceType<typeof App>["setToast"];
+2 -1
View File
@@ -14,6 +14,7 @@ import {
import { isValueInRange, rotatePoint } from "../excalidraw/math"; import { isValueInRange, rotatePoint } from "../excalidraw/math";
import type { Point } from "../excalidraw/types"; import type { Point } from "../excalidraw/types";
import { Bounds, getElementBounds } from "../excalidraw/element/bounds"; import { Bounds, getElementBounds } from "../excalidraw/element/bounds";
import { arrayToMap } from "../excalidraw/utils";
type Element = NonDeletedExcalidrawElement; type Element = NonDeletedExcalidrawElement;
type Elements = readonly NonDeletedExcalidrawElement[]; type Elements = readonly NonDeletedExcalidrawElement[];
@@ -158,7 +159,7 @@ export const elementsOverlappingBBox = ({
type: "overlap" | "contain" | "inside"; type: "overlap" | "contain" | "inside";
}) => { }) => {
if (isExcalidrawElement(bounds)) { if (isExcalidrawElement(bounds)) {
bounds = getElementBounds(bounds); bounds = getElementBounds(bounds, arrayToMap(elements));
} }
const adjustedBBox: Bounds = [ const adjustedBBox: Bounds = [
bounds[0] - errorMargin, bounds[0] - errorMargin,