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