Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec7801a31 | |||
| 2e719ff671 | |||
| 79d9dc2f8f | |||
| 9013c84524 | |||
| 47f87f4ecb | |||
| 73bf50e8a8 | |||
| 48c3465b19 | |||
| adc4c9f484 | |||
| def1df2c68 |
+25
-21
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -128,7 +128,11 @@ export const actionPasteStyles = register({
|
||||
element.id === newElement.containerId,
|
||||
) || null;
|
||||
}
|
||||
redrawTextBoundingBox(newElement, container);
|
||||
redrawTextBoundingBox(
|
||||
newElement,
|
||||
container,
|
||||
app.scene.getNonDeletedElementsMap(),
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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
-1
@@ -1,4 +1,4 @@
|
||||
@import "../css/variables.module.scss";
|
||||
@import "../../css/variables.module.scss";
|
||||
|
||||
.excalidraw-hyperlinkContainer {
|
||||
display: flex;
|
||||
+46
-156
@@ -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]);
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user