Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6349e3fad | ||
|
|
120b11119b | ||
|
|
f42dda6769 | ||
|
|
5ceb97e6b5 | ||
|
|
0dab7c3a68 | ||
|
|
708048ec61 | ||
|
|
2dfcc6f0ce | ||
|
|
3f5fdec04e | ||
|
|
278cd35772 | ||
|
|
43fa4b5602 | ||
|
|
2e1a529c67 | ||
|
|
b1c6bfcf40 | ||
|
|
1caec99b29 |
@@ -1,4 +1,4 @@
|
|||||||
FROM node:18-bullseye
|
FROM node:24-bullseye
|
||||||
|
|
||||||
# Vite wants to open the browser using `open`, so we
|
# Vite wants to open the browser using `open`, so we
|
||||||
# need to install those utils.
|
# need to install those utils.
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- banner:
|
|
||||||
- heading "Excalidraw" [level=1] [ref=e2]
|
|
||||||
- generic [active] [ref=e5]:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- button [ref=e9] [cursor=pointer]:
|
|
||||||
- img [ref=e10]
|
|
||||||
- region "Shapes":
|
|
||||||
- generic [ref=e16]:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- text: To move canvas, hold
|
|
||||||
- generic: Scroll wheel
|
|
||||||
- text: or
|
|
||||||
- generic: Space
|
|
||||||
- text: while dragging, or use the hand tool
|
|
||||||
- heading "Shapes" [level=2] [ref=e17]
|
|
||||||
- generic [ref=e18]:
|
|
||||||
- generic "Keep selected tool active after drawing — Q" [ref=e19] [cursor=pointer]:
|
|
||||||
- checkbox "Keep selected tool active after drawing"
|
|
||||||
- img [ref=e21]
|
|
||||||
- generic "Hand (panning tool) — H or null" [ref=e29] [cursor=pointer]:
|
|
||||||
- radio "Hand (panning tool)"
|
|
||||||
- img [ref=e31]
|
|
||||||
- generic "Selection — V or 1" [ref=e38] [cursor=pointer]:
|
|
||||||
- radio "Selection" [checked]
|
|
||||||
- generic [ref=e39]:
|
|
||||||
- img [ref=e40]
|
|
||||||
- generic [ref=e45]: "1"
|
|
||||||
- generic "Rectangle — R or 2" [ref=e46] [cursor=pointer]:
|
|
||||||
- radio "Rectangle"
|
|
||||||
- generic [ref=e47]:
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic [ref=e52]: "2"
|
|
||||||
- generic "Diamond — D or 3" [ref=e53] [cursor=pointer]:
|
|
||||||
- radio "Diamond"
|
|
||||||
- generic [ref=e54]:
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic [ref=e59]: "3"
|
|
||||||
- generic "Ellipse — O or 4" [ref=e60] [cursor=pointer]:
|
|
||||||
- radio "Ellipse"
|
|
||||||
- generic [ref=e61]:
|
|
||||||
- img [ref=e62]
|
|
||||||
- generic [ref=e66]: "4"
|
|
||||||
- generic "Arrow — A or 5" [ref=e67] [cursor=pointer]:
|
|
||||||
- radio "Arrow"
|
|
||||||
- generic [ref=e68]:
|
|
||||||
- img [ref=e69]
|
|
||||||
- generic [ref=e74]: "5"
|
|
||||||
- generic "Line — L or 6" [ref=e75] [cursor=pointer]:
|
|
||||||
- radio "Line"
|
|
||||||
- generic [ref=e76]:
|
|
||||||
- img [ref=e77]
|
|
||||||
- generic [ref=e78]: "6"
|
|
||||||
- generic "Draw — P or 7" [ref=e79] [cursor=pointer]:
|
|
||||||
- radio "Draw"
|
|
||||||
- generic [ref=e80]:
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic [ref=e85]: "7"
|
|
||||||
- generic "Text — T or 8" [ref=e86] [cursor=pointer]:
|
|
||||||
- radio "Text"
|
|
||||||
- generic [ref=e87]:
|
|
||||||
- img [ref=e88]
|
|
||||||
- generic [ref=e93]: "8"
|
|
||||||
- generic "Insert image — 9" [ref=e94] [cursor=pointer]:
|
|
||||||
- radio "Insert image"
|
|
||||||
- generic [ref=e95]:
|
|
||||||
- img [ref=e96]
|
|
||||||
- generic [ref=e101]: "9"
|
|
||||||
- generic "Eraser — E or 0" [ref=e102] [cursor=pointer]:
|
|
||||||
- radio "Eraser"
|
|
||||||
- generic [ref=e103]:
|
|
||||||
- img [ref=e104]
|
|
||||||
- generic [ref=e109]: "0"
|
|
||||||
- button "More tools" [ref=e112] [cursor=pointer]:
|
|
||||||
- img [ref=e113]
|
|
||||||
- generic:
|
|
||||||
- generic [ref=e119]:
|
|
||||||
- link "Excalidraw+" [ref=e120] [cursor=pointer]:
|
|
||||||
- /url: https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=guestBanner#excalidraw-redirect
|
|
||||||
- button "Live collaboration..." [ref=e121] [cursor=pointer]:
|
|
||||||
- img [ref=e122]
|
|
||||||
- generic "Library" [ref=e124]:
|
|
||||||
- checkbox "Library"
|
|
||||||
- img [ref=e127] [cursor=pointer]
|
|
||||||
- contentinfo:
|
|
||||||
- region "Canvas actions" [ref=e132]:
|
|
||||||
- heading "Canvas actions" [level=2] [ref=e133]
|
|
||||||
- generic [ref=e135]:
|
|
||||||
- button "Zoom out" [ref=e136] [cursor=pointer]:
|
|
||||||
- img [ref=e138]
|
|
||||||
- button "Reset zoom" [ref=e140] [cursor=pointer]: 100%
|
|
||||||
- button "Zoom in" [ref=e141] [cursor=pointer]:
|
|
||||||
- img [ref=e143]
|
|
||||||
- generic [ref=e145]:
|
|
||||||
- button "Undo" [disabled] [ref=e148]:
|
|
||||||
- img [ref=e150]
|
|
||||||
- button "Redo" [disabled] [ref=e154]:
|
|
||||||
- img [ref=e156]
|
|
||||||
- generic [ref=e158]:
|
|
||||||
- generic [ref=e159]:
|
|
||||||
- button "lock scroll" [ref=e160] [cursor=pointer]
|
|
||||||
- generic [ref=e161]:
|
|
||||||
- text: overscroll
|
|
||||||
- spinbutton "overscroll" [ref=e162]: "0.2"
|
|
||||||
- generic [ref=e163]:
|
|
||||||
- text: zoom factor
|
|
||||||
- spinbutton "zoom factor" [ref=e164]: "0.2"
|
|
||||||
- generic [ref=e165]:
|
|
||||||
- checkbox "lock zoom" [ref=e166]
|
|
||||||
- text: lock zoom
|
|
||||||
- generic [ref=e167]:
|
|
||||||
- checkbox "animate" [ref=e168]
|
|
||||||
- text: animate
|
|
||||||
- link "Blog post on end-to-end encryption in Excalidraw" [ref=e169] [cursor=pointer]:
|
|
||||||
- /url: https://plus.excalidraw.com/blog/end-to-end-encryption
|
|
||||||
- img [ref=e171]
|
|
||||||
- button "Help" [ref=e174] [cursor=pointer]:
|
|
||||||
- img [ref=e175]
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- generic [ref=e180]: Drawing canvas
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
- generic [ref=e1]:
|
|
||||||
- banner:
|
|
||||||
- heading "Excalidraw" [level=1] [ref=e2]
|
|
||||||
- generic [ref=e5]:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- img
|
|
||||||
- generic:
|
|
||||||
- text: Your drawings are saved in your browser's storage.
|
|
||||||
- text: Browser storage can be cleared unexpectedly.
|
|
||||||
- text: Save your work to a file regularly to avoid losing it.
|
|
||||||
- generic:
|
|
||||||
- button "Open Ctrl+O" [ref=e181] [cursor=pointer]:
|
|
||||||
- img [ref=e183]
|
|
||||||
- generic [ref=e185]: Open
|
|
||||||
- generic [ref=e186]: Ctrl+O
|
|
||||||
- button "Help ?" [ref=e187] [cursor=pointer]:
|
|
||||||
- img [ref=e189]
|
|
||||||
- generic [ref=e194]: Help
|
|
||||||
- generic [ref=e195]: "?"
|
|
||||||
- button "Live collaboration..." [ref=e196] [cursor=pointer]:
|
|
||||||
- img [ref=e198]
|
|
||||||
- generic [ref=e205]: Live collaboration...
|
|
||||||
- link "Sign up" [ref=e206] [cursor=pointer]:
|
|
||||||
- /url: https://plus.excalidraw.com/plus?utm_source=excalidraw&utm_medium=app&utm_content=welcomeScreenGuest
|
|
||||||
- img [ref=e208]
|
|
||||||
- generic [ref=e214]: Sign up
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- button [ref=e9] [cursor=pointer]:
|
|
||||||
- img [ref=e10]
|
|
||||||
- region "Shapes":
|
|
||||||
- generic [ref=e16]:
|
|
||||||
- generic:
|
|
||||||
- generic:
|
|
||||||
- text: To move canvas, hold
|
|
||||||
- generic: Scroll wheel
|
|
||||||
- text: or
|
|
||||||
- generic: Space
|
|
||||||
- text: while dragging, or use the hand tool
|
|
||||||
- heading "Shapes" [level=2] [ref=e17]
|
|
||||||
- generic [ref=e18]:
|
|
||||||
- generic "Keep selected tool active after drawing — Q" [ref=e19] [cursor=pointer]:
|
|
||||||
- checkbox "Keep selected tool active after drawing"
|
|
||||||
- img [ref=e21]
|
|
||||||
- generic "Hand (panning tool) — H or null" [ref=e29] [cursor=pointer]:
|
|
||||||
- radio "Hand (panning tool)"
|
|
||||||
- img [ref=e31]
|
|
||||||
- generic "Selection" [ref=e215] [cursor=pointer]:
|
|
||||||
- radio "Selection" [checked]
|
|
||||||
- img [ref=e217]
|
|
||||||
- generic "Rectangle — R or 2" [ref=e46] [cursor=pointer]:
|
|
||||||
- radio "Rectangle"
|
|
||||||
- img [ref=e48]
|
|
||||||
- generic "Diamond — D or 3" [ref=e53] [cursor=pointer]:
|
|
||||||
- radio "Diamond"
|
|
||||||
- img [ref=e55]
|
|
||||||
- generic "Ellipse — O or 4" [ref=e60] [cursor=pointer]:
|
|
||||||
- radio "Ellipse"
|
|
||||||
- img [ref=e62]
|
|
||||||
- generic "Arrow — A or 5" [ref=e67] [cursor=pointer]:
|
|
||||||
- radio "Arrow"
|
|
||||||
- img [ref=e69]
|
|
||||||
- generic "Line — L or 6" [ref=e75] [cursor=pointer]:
|
|
||||||
- radio "Line"
|
|
||||||
- img [ref=e77]
|
|
||||||
- generic "Draw — P or 7" [ref=e79] [cursor=pointer]:
|
|
||||||
- radio "Draw"
|
|
||||||
- img [ref=e81]
|
|
||||||
- generic "Text — T or 8" [ref=e86] [cursor=pointer]:
|
|
||||||
- radio "Text"
|
|
||||||
- img [ref=e88]
|
|
||||||
- generic "Insert image — 9" [ref=e94] [cursor=pointer]:
|
|
||||||
- radio "Insert image"
|
|
||||||
- img [ref=e96]
|
|
||||||
- generic "Eraser — E or 0" [ref=e102] [cursor=pointer]:
|
|
||||||
- radio "Eraser"
|
|
||||||
- img [ref=e104]
|
|
||||||
- button "More tools" [ref=e112] [cursor=pointer]:
|
|
||||||
- img [ref=e113]
|
|
||||||
- generic:
|
|
||||||
- button "Live collaboration..." [ref=e121] [cursor=pointer]:
|
|
||||||
- img [ref=e122]
|
|
||||||
- generic "Library" [ref=e124]:
|
|
||||||
- checkbox "Library"
|
|
||||||
- img [ref=e127] [cursor=pointer]
|
|
||||||
- contentinfo:
|
|
||||||
- region "Canvas actions" [ref=e132]:
|
|
||||||
- heading "Canvas actions" [level=2] [ref=e133]
|
|
||||||
- generic [ref=e135]:
|
|
||||||
- button "Zoom out" [ref=e136] [cursor=pointer]:
|
|
||||||
- img [ref=e138]
|
|
||||||
- button "Reset zoom" [disabled] [ref=e140]: 100%
|
|
||||||
- button "Zoom in" [ref=e141] [cursor=pointer]:
|
|
||||||
- img [ref=e143]
|
|
||||||
- generic [ref=e145]:
|
|
||||||
- button "Undo" [disabled] [ref=e148]:
|
|
||||||
- img [ref=e150]
|
|
||||||
- button "Redo" [disabled] [ref=e154]:
|
|
||||||
- img [ref=e156]
|
|
||||||
- generic [ref=e158]:
|
|
||||||
- generic [ref=e159]:
|
|
||||||
- button "disable scroll lock" [active] [ref=e222] [cursor=pointer]
|
|
||||||
- generic [ref=e161]:
|
|
||||||
- text: overscroll
|
|
||||||
- spinbutton "overscroll" [ref=e162]: "0.2"
|
|
||||||
- generic [ref=e163]:
|
|
||||||
- text: zoom factor
|
|
||||||
- spinbutton "zoom factor" [ref=e164]: "0.2"
|
|
||||||
- generic [ref=e165]:
|
|
||||||
- checkbox "lock zoom" [ref=e166]
|
|
||||||
- text: lock zoom
|
|
||||||
- generic [ref=e167]:
|
|
||||||
- checkbox "animate" [ref=e168]
|
|
||||||
- text: animate
|
|
||||||
- link "Blog post on end-to-end encryption in Excalidraw" [ref=e169] [cursor=pointer]:
|
|
||||||
- /url: https://plus.excalidraw.com/blog/end-to-end-encryption
|
|
||||||
- img [ref=e171]
|
|
||||||
- button "Help" [ref=e174] [cursor=pointer]:
|
|
||||||
- img [ref=e175]
|
|
||||||
- generic:
|
|
||||||
- img
|
|
||||||
- generic [ref=e180]: Drawing canvas
|
|
||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM --platform=${BUILDPLATFORM} node:18 AS build
|
FROM --platform=${BUILDPLATFORM} node:24 AS build
|
||||||
|
|
||||||
WORKDIR /opt/node_app
|
WORKDIR /opt/node_app
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ ARG NODE_ENV=production
|
|||||||
|
|
||||||
RUN npm_config_target_arch=${TARGETARCH} yarn build:app:docker
|
RUN npm_config_target_arch=${TARGETARCH} yarn build:app:docker
|
||||||
|
|
||||||
FROM --platform=${TARGETPLATFORM} nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
COPY --from=build /opt/node_app/excalidraw-app/build /usr/share/nginx/html
|
||||||
|
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ const config = {
|
|||||||
href: "https://discord.gg/UexuTaE",
|
href: "https://discord.gg/UexuTaE",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Twitter",
|
label: "𝕏",
|
||||||
href: "https://twitter.com/excalidraw",
|
href: "https://x.com/excalidraw",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Linkedin",
|
label: "Linkedin",
|
||||||
|
|||||||
@@ -4,20 +4,8 @@ import { Excalidraw } from "@excalidraw/excalidraw";
|
|||||||
|
|
||||||
import "@excalidraw/excalidraw/index.css";
|
import "@excalidraw/excalidraw/index.css";
|
||||||
|
|
||||||
import type { ScrollConstraints } from "@excalidraw/excalidraw/types";
|
|
||||||
|
|
||||||
import App from "../../with-script-in-browser/components/ExampleApp";
|
import App from "../../with-script-in-browser/components/ExampleApp";
|
||||||
|
|
||||||
const scrollConstraints: ScrollConstraints = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 850,
|
|
||||||
height: 400,
|
|
||||||
lockZoom: true,
|
|
||||||
overscrollAllowance: 0,
|
|
||||||
viewportZoomFactor: 1,
|
|
||||||
};
|
|
||||||
|
|
||||||
const ExcalidrawWrapper: React.FC = () => {
|
const ExcalidrawWrapper: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -26,7 +14,7 @@ const ExcalidrawWrapper: React.FC = () => {
|
|||||||
useCustom={(api: any, args?: any[]) => {}}
|
useCustom={(api: any, args?: any[]) => {}}
|
||||||
excalidrawLib={excalidrawLib}
|
excalidrawLib={excalidrawLib}
|
||||||
>
|
>
|
||||||
<Excalidraw scrollConstraints={scrollConstraints} />
|
<Excalidraw />
|
||||||
</App>
|
</App>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -178,8 +178,7 @@ export default function ExampleApp({
|
|||||||
const newElement = cloneElement(
|
const newElement = cloneElement(
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
{
|
{
|
||||||
onExcalidrawAPI: (api: ExcalidrawImperativeAPI | null) =>
|
excalidrawAPI: (api: ExcalidrawImperativeAPI) => setExcalidrawAPI(api),
|
||||||
setExcalidrawAPI(api),
|
|
||||||
initialData: initialStatePromiseRef.current.promise,
|
initialData: initialStatePromiseRef.current.promise,
|
||||||
onChange: (
|
onChange: (
|
||||||
elements: NonDeletedExcalidrawElement[],
|
elements: NonDeletedExcalidrawElement[],
|
||||||
@@ -821,48 +820,6 @@ export default function ExampleApp({
|
|||||||
/>
|
/>
|
||||||
Export with embed scene
|
Export with embed scene
|
||||||
</label>
|
</label>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (!excalidrawAPI) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = excalidrawAPI.getSceneElements();
|
|
||||||
excalidrawAPI.scrollToContent(elements[0], {
|
|
||||||
animate: true,
|
|
||||||
fitToViewport: true,
|
|
||||||
viewportZoomFactor: 0.9,
|
|
||||||
scrollLock: {
|
|
||||||
lockZoom: true,
|
|
||||||
overscrollAllowance: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Fit and lock first element
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
if (!excalidrawAPI) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const elements = excalidrawAPI.getSceneElements();
|
|
||||||
excalidrawAPI.scrollToContent(elements[1], {
|
|
||||||
animate: true,
|
|
||||||
fitToViewport: true,
|
|
||||||
viewportZoomFactor: 0.9,
|
|
||||||
scrollLock: {
|
|
||||||
lockZoom: true,
|
|
||||||
overscrollAllowance: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Fit and lock second element
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!excalidrawAPI) {
|
if (!excalidrawAPI) {
|
||||||
|
|||||||
@@ -1015,10 +1015,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
</OverwriteConfirmDialog.Action>
|
</OverwriteConfirmDialog.Action>
|
||||||
)}
|
)}
|
||||||
</OverwriteConfirmDialog>
|
</OverwriteConfirmDialog>
|
||||||
<AppFooter
|
<AppFooter onChange={() => excalidrawAPI?.refresh()} />
|
||||||
excalidrawAPI={excalidrawAPI}
|
|
||||||
onChange={() => excalidrawAPI?.refresh()}
|
|
||||||
/>
|
|
||||||
{excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}
|
{excalidrawAPI && <AIComponents excalidrawAPI={excalidrawAPI} />}
|
||||||
|
|
||||||
<TTDDialogTrigger />
|
<TTDDialogTrigger />
|
||||||
|
|||||||
@@ -1,178 +1,13 @@
|
|||||||
import { Footer } from "@excalidraw/excalidraw/index";
|
import { Footer } from "@excalidraw/excalidraw/index";
|
||||||
import {
|
import React from "react";
|
||||||
getCommonBounds,
|
|
||||||
getSelectedElements,
|
|
||||||
getVisibleSceneBounds,
|
|
||||||
} from "@excalidraw/element";
|
|
||||||
import React, { useCallback, useState } from "react";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ExcalidrawImperativeAPI,
|
|
||||||
ScrollConstraints,
|
|
||||||
} from "@excalidraw/excalidraw/types";
|
|
||||||
|
|
||||||
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
import { isExcalidrawPlusSignedUser } from "../app_constants";
|
||||||
|
|
||||||
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
import { DebugFooter, isVisualDebuggerEnabled } from "./DebugCanvas";
|
||||||
import { EncryptedIcon } from "./EncryptedIcon";
|
import { EncryptedIcon } from "./EncryptedIcon";
|
||||||
|
|
||||||
type ScrollConstraintOptions = Pick<
|
|
||||||
ScrollConstraints,
|
|
||||||
"lockZoom" | "overscrollAllowance" | "viewportZoomFactor"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const ScrollConstraintsDebugFooter = ({
|
|
||||||
excalidrawAPI,
|
|
||||||
}: {
|
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
|
||||||
}) => {
|
|
||||||
const [activeLock, setActiveLock] = useState<ScrollConstraints | null>(null);
|
|
||||||
const [options, setOptions] = useState<ScrollConstraintOptions>({
|
|
||||||
lockZoom: false,
|
|
||||||
overscrollAllowance: 0.2,
|
|
||||||
viewportZoomFactor: 0.2,
|
|
||||||
});
|
|
||||||
|
|
||||||
const setLock = useCallback(
|
|
||||||
(nextLock: ScrollConstraints) => {
|
|
||||||
excalidrawAPI?.setScrollConstraints(nextLock);
|
|
||||||
setActiveLock(nextLock);
|
|
||||||
},
|
|
||||||
[excalidrawAPI],
|
|
||||||
);
|
|
||||||
|
|
||||||
const toggleScrollLock = useCallback(() => {
|
|
||||||
if (!excalidrawAPI) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeLock) {
|
|
||||||
excalidrawAPI.setScrollConstraints(null);
|
|
||||||
setActiveLock(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectedElements = getSelectedElements(
|
|
||||||
excalidrawAPI.getSceneElements(),
|
|
||||||
excalidrawAPI.getAppState(),
|
|
||||||
{
|
|
||||||
includeBoundTextElement: true,
|
|
||||||
includeElementsInFrames: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const [x1, y1, x2, y2] = selectedElements.length
|
|
||||||
? getCommonBounds(
|
|
||||||
selectedElements,
|
|
||||||
excalidrawAPI.getSceneElementsMapIncludingDeleted(),
|
|
||||||
)
|
|
||||||
: getVisibleSceneBounds(excalidrawAPI.getAppState());
|
|
||||||
|
|
||||||
setLock({
|
|
||||||
x: x1,
|
|
||||||
y: y1,
|
|
||||||
width: x2 - x1,
|
|
||||||
height: y2 - y1,
|
|
||||||
...options,
|
|
||||||
});
|
|
||||||
}, [activeLock, excalidrawAPI, options, setLock]);
|
|
||||||
|
|
||||||
const updateOptions = useCallback(
|
|
||||||
(nextOptions: ScrollConstraintOptions) => {
|
|
||||||
setOptions(nextOptions);
|
|
||||||
|
|
||||||
if (activeLock) {
|
|
||||||
setLock({ ...activeLock, ...nextOptions });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[activeLock, setLock],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateNumberOption = useCallback(
|
|
||||||
(option: "overscrollAllowance" | "viewportZoomFactor", value: string) => {
|
|
||||||
updateOptions({
|
|
||||||
...options,
|
|
||||||
[option]: value === "" ? undefined : Number(value),
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[options, updateOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const updateBooleanOption = useCallback(
|
|
||||||
(option: "lockZoom", value: boolean) => {
|
|
||||||
updateOptions({
|
|
||||||
...options,
|
|
||||||
[option]: value,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
[options, updateOptions],
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
gap: ".45rem",
|
|
||||||
alignItems: "center",
|
|
||||||
padding: "0 .35rem",
|
|
||||||
fontSize: 12,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="ToolIcon_type_button"
|
|
||||||
type="button"
|
|
||||||
onClick={toggleScrollLock}
|
|
||||||
>
|
|
||||||
{activeLock ? "disable scroll lock" : "lock scroll"}
|
|
||||||
</button>
|
|
||||||
<label style={{ display: "flex", gap: ".25rem", alignItems: "center" }}>
|
|
||||||
overscroll
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
max={1}
|
|
||||||
step={0.05}
|
|
||||||
value={options.overscrollAllowance ?? ""}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateNumberOption("overscrollAllowance", event.target.value)
|
|
||||||
}
|
|
||||||
style={{ width: 56 }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label style={{ display: "flex", gap: ".25rem", alignItems: "center" }}>
|
|
||||||
zoom factor
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0.1}
|
|
||||||
step={0.1}
|
|
||||||
value={options.viewportZoomFactor ?? ""}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateNumberOption("viewportZoomFactor", event.target.value)
|
|
||||||
}
|
|
||||||
style={{ width: 56 }}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label style={{ display: "flex", gap: ".2rem", alignItems: "center" }}>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={!!options.lockZoom}
|
|
||||||
onChange={(event) =>
|
|
||||||
updateBooleanOption("lockZoom", event.target.checked)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
lock zoom
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const AppFooter = React.memo(
|
export const AppFooter = React.memo(
|
||||||
({
|
({ onChange }: { onChange: () => void }) => {
|
||||||
excalidrawAPI,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
|
||||||
onChange: () => void;
|
|
||||||
}) => {
|
|
||||||
return (
|
return (
|
||||||
<Footer>
|
<Footer>
|
||||||
<div
|
<div
|
||||||
@@ -182,7 +17,6 @@ export const AppFooter = React.memo(
|
|||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollConstraintsDebugFooter excalidrawAPI={excalidrawAPI} />
|
|
||||||
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
{isVisualDebuggerEnabled() && <DebugFooter onChange={onChange} />}
|
||||||
{!isExcalidrawPlusSignedUser && <EncryptedIcon />}
|
{!isExcalidrawPlusSignedUser && <EncryptedIcon />}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1943,9 +1943,9 @@ export const calculateFixedPointForElbowArrowBinding = (
|
|||||||
return {
|
return {
|
||||||
fixedPoint: normalizeFixedPoint([
|
fixedPoint: normalizeFixedPoint([
|
||||||
(nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) /
|
(nonRotatedSnappedGlobalPoint[0] - hoveredElement.x) /
|
||||||
hoveredElement.width,
|
Math.max(hoveredElement.width, PRECISION),
|
||||||
(nonRotatedSnappedGlobalPoint[1] - hoveredElement.y) /
|
(nonRotatedSnappedGlobalPoint[1] - hoveredElement.y) /
|
||||||
hoveredElement.height,
|
Math.max(hoveredElement.height, PRECISION),
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1976,9 +1976,11 @@ export const calculateFixedPointForNonElbowArrowBinding = (
|
|||||||
|
|
||||||
// Calculate the ratio relative to the element's bounds
|
// Calculate the ratio relative to the element's bounds
|
||||||
const fixedPointX =
|
const fixedPointX =
|
||||||
(nonRotatedPoint[0] - hoveredElement.x) / hoveredElement.width;
|
(nonRotatedPoint[0] - hoveredElement.x) /
|
||||||
|
Math.max(hoveredElement.width, PRECISION);
|
||||||
const fixedPointY =
|
const fixedPointY =
|
||||||
(nonRotatedPoint[1] - hoveredElement.y) / hoveredElement.height;
|
(nonRotatedPoint[1] - hoveredElement.y) /
|
||||||
|
Math.max(hoveredElement.height, PRECISION);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
fixedPoint: normalizeFixedPoint([fixedPointX, fixedPointY]),
|
fixedPoint: normalizeFixedPoint([fixedPointX, fixedPointY]),
|
||||||
|
|||||||
@@ -250,6 +250,9 @@ export const duplicateElements = (
|
|||||||
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
elementsWithDuplicates.splice(index + 1, 0, ...castArray(elements));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// main
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
const frameIdsToDuplicate = new Set(
|
const frameIdsToDuplicate = new Set(
|
||||||
elements
|
elements
|
||||||
.filter(
|
.filter(
|
||||||
|
|||||||
@@ -2124,8 +2124,8 @@ const normalizeArrowElementUpdate = (
|
|||||||
offsetY < -MAX_POS ||
|
offsetY < -MAX_POS ||
|
||||||
offsetY > MAX_POS ||
|
offsetY > MAX_POS ||
|
||||||
offsetX + points[points.length - 1][0] < -MAX_POS ||
|
offsetX + points[points.length - 1][0] < -MAX_POS ||
|
||||||
offsetY + points[points.length - 1][0] > MAX_POS ||
|
offsetX + points[points.length - 1][0] > MAX_POS ||
|
||||||
offsetX + points[points.length - 1][1] < -MAX_POS ||
|
offsetY + points[points.length - 1][1] < -MAX_POS ||
|
||||||
offsetY + points[points.length - 1][1] > MAX_POS
|
offsetY + points[points.length - 1][1] > MAX_POS
|
||||||
) {
|
) {
|
||||||
console.error(
|
console.error(
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
getElementAbsoluteCoords,
|
getElementAbsoluteCoords,
|
||||||
doBoundsIntersect,
|
doBoundsIntersect,
|
||||||
getElementBounds,
|
getElementBounds,
|
||||||
|
boundsContainBounds,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
@@ -101,8 +102,9 @@ export const isElementContainingFrame = (
|
|||||||
frame: ExcalidrawFrameLikeElement,
|
frame: ExcalidrawFrameLikeElement,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) => {
|
) => {
|
||||||
return getElementsWithinSelection([frame], element, elementsMap).some(
|
return boundsContainBounds(
|
||||||
(e) => e.id === frame.id,
|
getElementBounds(element, elementsMap),
|
||||||
|
getElementBounds(frame, elementsMap),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
getContainingFrame,
|
getContainingFrame,
|
||||||
getFrameChildren,
|
getFrameChildren,
|
||||||
isElementIntersectingFrame,
|
|
||||||
} from "./frame";
|
} from "./frame";
|
||||||
|
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
@@ -170,7 +169,7 @@ export const getElementsWithinSelection = (
|
|||||||
const associatedFrame = getContainingFrame(element, elementsMap);
|
const associatedFrame = getContainingFrame(element, elementsMap);
|
||||||
if (
|
if (
|
||||||
associatedFrame &&
|
associatedFrame &&
|
||||||
isElementIntersectingFrame(element, associatedFrame, elementsMap)
|
elementOverlapsWithFrame(element, associatedFrame, elementsMap)
|
||||||
) {
|
) {
|
||||||
const frameAABB = getElementBounds(associatedFrame, elementsMap);
|
const frameAABB = getElementBounds(associatedFrame, elementsMap);
|
||||||
elementAABB = [
|
elementAABB = [
|
||||||
@@ -209,10 +208,9 @@ export const getElementsWithinSelection = (
|
|||||||
if (boundsContainBounds(selectionBounds, commonAABB)) {
|
if (boundsContainBounds(selectionBounds, commonAABB)) {
|
||||||
if (framesInSelection && isFrameLikeElement(element)) {
|
if (framesInSelection && isFrameLikeElement(element)) {
|
||||||
framesInSelection.add(element.id);
|
framesInSelection.add(element.id);
|
||||||
} else {
|
|
||||||
elementsInSelection.push(element);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
elementsInSelection.push(element);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Handle the case where the label is overlapped by the selection box
|
// 2. Handle the case where the label is overlapped by the selection box
|
||||||
|
|||||||
@@ -1,59 +1,56 @@
|
|||||||
import { arrayToMapWithIndex } from "@excalidraw/common";
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "./types";
|
import type { ExcalidrawElement } from "./types";
|
||||||
|
|
||||||
const normalizeGroupElementOrder = (elements: readonly ExcalidrawElement[]) => {
|
const defragmentGroups = (elements: readonly ExcalidrawElement[]) => {
|
||||||
const origElements: ExcalidrawElement[] = elements.slice();
|
const groupIdAtLevel = (element: ExcalidrawElement, level: number) => {
|
||||||
const sortedElements = new Set<ExcalidrawElement>();
|
return element.groupIds[element.groupIds.length - level - 1];
|
||||||
|
|
||||||
const orderInnerGroups = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
): ExcalidrawElement[] => {
|
|
||||||
const firstGroupSig = elements[0]?.groupIds?.join("");
|
|
||||||
const aGroup: ExcalidrawElement[] = [elements[0]];
|
|
||||||
const bGroup: ExcalidrawElement[] = [];
|
|
||||||
for (const element of elements.slice(1)) {
|
|
||||||
if (element.groupIds?.join("") === firstGroupSig) {
|
|
||||||
aGroup.push(element);
|
|
||||||
} else {
|
|
||||||
bGroup.push(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return bGroup.length ? [...aGroup, ...orderInnerGroups(bGroup)] : aGroup;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const groupHandledElements = new Map<string, true>();
|
const orderLevel = (
|
||||||
|
levelElements: readonly ExcalidrawElement[],
|
||||||
|
level: number,
|
||||||
|
): ExcalidrawElement[] => {
|
||||||
|
const buckets = new Map<string, ExcalidrawElement[]>();
|
||||||
|
// Slots preserve first-occurrence order: a groupId reserves its slot
|
||||||
|
// the first time one of its members is seen; loose elements occupy
|
||||||
|
// their own slot. Groups are then expanded (and recursed into) in place.
|
||||||
|
const slots: (ExcalidrawElement | string)[] = [];
|
||||||
|
|
||||||
origElements.forEach((element, idx) => {
|
for (const element of levelElements) {
|
||||||
if (groupHandledElements.has(element.id)) {
|
const groupId = groupIdAtLevel(element, level);
|
||||||
return;
|
if (groupId === undefined) {
|
||||||
}
|
slots.push(element);
|
||||||
if (element.groupIds?.length) {
|
continue;
|
||||||
const topGroup = element.groupIds[element.groupIds.length - 1];
|
|
||||||
const groupElements = origElements.slice(idx).filter((element) => {
|
|
||||||
const ret = element?.groupIds?.some((id) => id === topGroup);
|
|
||||||
if (ret) {
|
|
||||||
groupHandledElements.set(element!.id, true);
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const elem of orderInnerGroups(groupElements)) {
|
|
||||||
sortedElements.add(elem);
|
|
||||||
}
|
}
|
||||||
} else {
|
let bucket = buckets.get(groupId);
|
||||||
sortedElements.add(element);
|
if (!bucket) {
|
||||||
|
bucket = [];
|
||||||
|
buckets.set(groupId, bucket);
|
||||||
|
slots.push(groupId);
|
||||||
|
}
|
||||||
|
bucket.push(element);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
return slots.flatMap((slot) =>
|
||||||
|
typeof slot === "string"
|
||||||
|
? orderLevel(buckets.get(slot)!, level + 1)
|
||||||
|
: [slot],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// `groupIds` is stored innermost-first, so the outermost group is the
|
||||||
|
// last entry. We recurse from level 0 (outermost) inward.
|
||||||
|
const sortedElements = orderLevel(elements, 0);
|
||||||
|
|
||||||
// if there's a bug which resulted in losing some of the elements, return
|
// if there's a bug which resulted in losing some of the elements, return
|
||||||
// original instead as that's better than losing data
|
// original instead as that's better than losing data
|
||||||
if (sortedElements.size !== elements.length) {
|
if (sortedElements.length !== elements.length) {
|
||||||
console.error("normalizeGroupElementOrder: lost some elements... bailing!");
|
console.error("defragmentGroups: lost some elements... bailing!");
|
||||||
return elements;
|
return elements;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [...sortedElements];
|
return sortedElements;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -68,39 +65,40 @@ const normalizeGroupElementOrder = (elements: readonly ExcalidrawElement[]) => {
|
|||||||
const normalizeBoundElementsOrder = (
|
const normalizeBoundElementsOrder = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
const elementsMap = arrayToMapWithIndex(elements);
|
const elementsMap = arrayToMap(elements);
|
||||||
|
|
||||||
const origElements: (ExcalidrawElement | null)[] = elements.slice();
|
|
||||||
const sortedElements = new Set<ExcalidrawElement>();
|
const sortedElements = new Set<ExcalidrawElement>();
|
||||||
|
|
||||||
origElements.forEach((element, idx) => {
|
for (const element of elements) {
|
||||||
if (!element) {
|
if (sortedElements.has(element)) {
|
||||||
return;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (element.boundElements?.length) {
|
if (element.boundElements?.length) {
|
||||||
sortedElements.add(element);
|
sortedElements.add(element);
|
||||||
origElements[idx] = null;
|
for (const boundElement of element.boundElements) {
|
||||||
element.boundElements.forEach((boundElement) => {
|
|
||||||
const child = elementsMap.get(boundElement.id);
|
const child = elementsMap.get(boundElement.id);
|
||||||
if (child && boundElement.type === "text") {
|
if (child && boundElement.type === "text") {
|
||||||
sortedElements.add(child[0]);
|
sortedElements.add(child);
|
||||||
origElements[child[1]] = null;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
} else if (element.type === "text" && element.containerId) {
|
|
||||||
const parent = elementsMap.get(element.containerId);
|
|
||||||
if (!parent?.[0].boundElements?.find((x) => x.id === element.id)) {
|
|
||||||
sortedElements.add(element);
|
|
||||||
origElements[idx] = null;
|
|
||||||
|
|
||||||
// if element has a container and container lists it, skip this element
|
|
||||||
// as it'll be taken care of by the container
|
|
||||||
}
|
}
|
||||||
} else {
|
continue;
|
||||||
sortedElements.add(element);
|
|
||||||
origElements[idx] = null;
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
// if element has a container and container lists it, skip this element
|
||||||
|
// as it'll be taken care of by the container
|
||||||
|
if (
|
||||||
|
element.type === "text" &&
|
||||||
|
element.containerId &&
|
||||||
|
elementsMap
|
||||||
|
.get(element.containerId)
|
||||||
|
?.boundElements?.some((el) => el.id === element.id)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
sortedElements.add(element);
|
||||||
|
}
|
||||||
|
|
||||||
// if there's a bug which resulted in losing some of the elements, return
|
// if there's a bug which resulted in losing some of the elements, return
|
||||||
// original instead as that's better than losing data
|
// original instead as that's better than losing data
|
||||||
@@ -117,5 +115,5 @@ const normalizeBoundElementsOrder = (
|
|||||||
export const normalizeElementOrder = (
|
export const normalizeElementOrder = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
) => {
|
) => {
|
||||||
return normalizeBoundElementsOrder(normalizeGroupElementOrder(elements));
|
return normalizeBoundElementsOrder(defragmentGroups(elements));
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import {
|
|||||||
convertToExcalidrawElements,
|
convertToExcalidrawElements,
|
||||||
Excalidraw,
|
Excalidraw,
|
||||||
} from "@excalidraw/excalidraw";
|
} from "@excalidraw/excalidraw";
|
||||||
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
|
||||||
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
import { API } from "@excalidraw/excalidraw/tests/helpers/api";
|
||||||
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
import { Keyboard, Pointer } from "@excalidraw/excalidraw/tests/helpers/ui";
|
||||||
@@ -10,7 +11,12 @@ import {
|
|||||||
render,
|
render,
|
||||||
} from "@excalidraw/excalidraw/tests/test-utils";
|
} from "@excalidraw/excalidraw/tests/test-utils";
|
||||||
|
|
||||||
import type { ExcalidrawElement } from "../src/types";
|
import { elementOverlapsWithFrame } from "../src/frame";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ExcalidrawElement,
|
||||||
|
ExcalidrawFrameLikeElement,
|
||||||
|
} from "../src/types";
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
@@ -125,6 +131,26 @@ describe("adding elements to frames", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should treat an element fully containing a frame as overlapping the frame", () => {
|
||||||
|
const containingRect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: -50,
|
||||||
|
y: -50,
|
||||||
|
width: 250,
|
||||||
|
height: 250,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([containingRect, frame]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
elementOverlapsWithFrame(
|
||||||
|
containingRect,
|
||||||
|
frame as ExcalidrawFrameLikeElement,
|
||||||
|
arrayToMap(h.elements),
|
||||||
|
),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
const commonTestCases = async (
|
const commonTestCases = async (
|
||||||
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
func: typeof resizeFrameOverElement | typeof dragElementIntoFrame,
|
||||||
) => {
|
) => {
|
||||||
@@ -415,6 +441,22 @@ describe("adding elements to frames", () => {
|
|||||||
describe("dragging elements into the frame", async () => {
|
describe("dragging elements into the frame", async () => {
|
||||||
await commonTestCases(dragElementIntoFrame);
|
await commonTestCases(dragElementIntoFrame);
|
||||||
|
|
||||||
|
it("should add a dragged element fully containing the frame", () => {
|
||||||
|
const containingRect = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 220,
|
||||||
|
y: 20,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame, containingRect]);
|
||||||
|
|
||||||
|
dragElementIntoFrame(frame, containingRect);
|
||||||
|
|
||||||
|
expect(API.getElement(containingRect).frameId).toBe(frame.id);
|
||||||
|
});
|
||||||
|
|
||||||
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
it.skip("should drag element inside, duplicate it and keep it in frame", () => {
|
||||||
API.setElements([frame, rect2]);
|
API.setElements([frame, rect2]);
|
||||||
|
|
||||||
|
|||||||
@@ -326,19 +326,59 @@ describe("normalizeElementsOrder", () => {
|
|||||||
]),
|
]),
|
||||||
[
|
[
|
||||||
"BA_rect1",
|
"BA_rect1",
|
||||||
|
"CBA_rect3",
|
||||||
|
"CBA_rect7",
|
||||||
"BA_rect5",
|
"BA_rect5",
|
||||||
"BA_rect6",
|
"BA_rect6",
|
||||||
"A_rect2",
|
"A_rect2",
|
||||||
"A_rect5",
|
"A_rect5",
|
||||||
"CBA_rect3",
|
|
||||||
"CBA_rect7",
|
|
||||||
"rect4",
|
"rect4",
|
||||||
"X_rect8",
|
"X_rect8",
|
||||||
"X_rect11",
|
|
||||||
"YX_rect10",
|
"YX_rect10",
|
||||||
|
"X_rect11",
|
||||||
"rect9",
|
"rect9",
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "CBA_rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["C", "B", "A"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "A_rect3",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["A"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["A_rect1", "CBA_rect2", "A_rect3"],
|
||||||
|
);
|
||||||
|
assertOrder(
|
||||||
|
normalizeElementOrder([
|
||||||
|
API.createElement({
|
||||||
|
id: "abcT_rect1",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["ab", "c", "T"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "abcT_rect2",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["a", "bc", "T"],
|
||||||
|
}),
|
||||||
|
API.createElement({
|
||||||
|
id: "abcT_rect3",
|
||||||
|
type: "rectangle",
|
||||||
|
groupIds: ["ab", "c", "T"],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
["abcT_rect1", "abcT_rect3", "abcT_rect2"],
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ import {
|
|||||||
ZoomResetIcon,
|
ZoomResetIcon,
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { setCursor } from "../cursor";
|
import { setCursor } from "../cursor";
|
||||||
import { constrainScrollState } from "../scene/scrollConstraints";
|
|
||||||
|
|
||||||
import { t } from "../i18n";
|
import { t } from "../i18n";
|
||||||
import { getNormalizedZoom } from "../scene";
|
import { getNormalizedZoom } from "../scene";
|
||||||
@@ -52,8 +51,6 @@ import { register } from "./register";
|
|||||||
|
|
||||||
import type { AppState, Offsets } from "../types";
|
import type { AppState, Offsets } from "../types";
|
||||||
|
|
||||||
const canUseZoomActions = (appState: AppState) => !appState.scrollConstraints;
|
|
||||||
|
|
||||||
export const actionChangeViewBackgroundColor = register<Partial<AppState>>({
|
export const actionChangeViewBackgroundColor = register<Partial<AppState>>({
|
||||||
name: "changeViewBackgroundColor",
|
name: "changeViewBackgroundColor",
|
||||||
label: "labels.canvasBackground",
|
label: "labels.canvasBackground",
|
||||||
@@ -142,7 +139,7 @@ export const actionZoomIn = register({
|
|||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (_elements, appState, _, app) => {
|
perform: (_elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState: constrainScrollState({
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...getStateForZoom(
|
...getStateForZoom(
|
||||||
{
|
{
|
||||||
@@ -153,7 +150,7 @@ export const actionZoomIn = register({
|
|||||||
appState,
|
appState,
|
||||||
),
|
),
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
}),
|
},
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -183,7 +180,7 @@ export const actionZoomOut = register({
|
|||||||
trackEvent: { category: "canvas" },
|
trackEvent: { category: "canvas" },
|
||||||
perform: (_elements, appState, _, app) => {
|
perform: (_elements, appState, _, app) => {
|
||||||
return {
|
return {
|
||||||
appState: constrainScrollState({
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
...getStateForZoom(
|
...getStateForZoom(
|
||||||
{
|
{
|
||||||
@@ -194,7 +191,7 @@ export const actionZoomOut = register({
|
|||||||
appState,
|
appState,
|
||||||
),
|
),
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
}),
|
},
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
@@ -246,7 +243,6 @@ export const actionResetZoom = register({
|
|||||||
className="reset-zoom-button zoom-button"
|
className="reset-zoom-button zoom-button"
|
||||||
title={t("buttons.resetZoom")}
|
title={t("buttons.resetZoom")}
|
||||||
aria-label={t("buttons.resetZoom")}
|
aria-label={t("buttons.resetZoom")}
|
||||||
disabled={!canUseZoomActions(appState)}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
updateData(null);
|
updateData(null);
|
||||||
}}
|
}}
|
||||||
@@ -255,7 +251,6 @@ export const actionResetZoom = register({
|
|||||||
</ToolButton>
|
</ToolButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
predicate: (_elements, appState) => canUseZoomActions(appState),
|
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
(event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) &&
|
(event.code === CODES.ZERO || event.code === CODES.NUM_ZERO) &&
|
||||||
(event[KEYS.CTRL_OR_CMD] || event.shiftKey),
|
(event[KEYS.CTRL_OR_CMD] || event.shiftKey),
|
||||||
@@ -279,7 +274,7 @@ const zoomValueToFitBoundsOnViewport = (
|
|||||||
return Math.min(adjustedZoomValue, 1);
|
return Math.min(adjustedZoomValue, 1);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function resolveViewportForBounds({
|
export const zoomToFitBounds = ({
|
||||||
bounds,
|
bounds,
|
||||||
appState,
|
appState,
|
||||||
canvasOffsets,
|
canvasOffsets,
|
||||||
@@ -297,7 +292,7 @@ export function resolveViewportForBounds({
|
|||||||
viewportZoomFactor?: number;
|
viewportZoomFactor?: number;
|
||||||
minZoom?: number;
|
minZoom?: number;
|
||||||
maxZoom?: number;
|
maxZoom?: number;
|
||||||
}) {
|
}) => {
|
||||||
viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM);
|
viewportZoomFactor = clamp(viewportZoomFactor, MIN_ZOOM, MAX_ZOOM);
|
||||||
|
|
||||||
const [x1, y1, x2, y2] = bounds;
|
const [x1, y1, x2, y2] = bounds;
|
||||||
@@ -350,58 +345,12 @@ export function resolveViewportForBounds({
|
|||||||
zoom: { value: newZoomValue },
|
zoom: { value: newZoomValue },
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
|
||||||
bounds,
|
|
||||||
scrollX: centerScroll.scrollX,
|
|
||||||
scrollY: centerScroll.scrollY,
|
|
||||||
zoom: { value: newZoomValue },
|
|
||||||
viewportZoomFactor,
|
|
||||||
viewportDimensions: {
|
|
||||||
width: appState.width,
|
|
||||||
height: appState.height,
|
|
||||||
},
|
|
||||||
effectiveViewportDimensions: {
|
|
||||||
width: effectiveCanvasWidth,
|
|
||||||
height: effectiveCanvasHeight,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const zoomToFitBounds = ({
|
|
||||||
bounds,
|
|
||||||
appState,
|
|
||||||
canvasOffsets,
|
|
||||||
fitToViewport = false,
|
|
||||||
viewportZoomFactor = 1,
|
|
||||||
minZoom = -Infinity,
|
|
||||||
maxZoom = Infinity,
|
|
||||||
}: {
|
|
||||||
bounds: SceneBounds;
|
|
||||||
canvasOffsets?: Offsets;
|
|
||||||
appState: Readonly<AppState>;
|
|
||||||
/** whether to fit content to viewport (beyond >100%) */
|
|
||||||
fitToViewport: boolean;
|
|
||||||
/** zoom content to cover X of the viewport, when fitToViewport=true */
|
|
||||||
viewportZoomFactor?: number;
|
|
||||||
minZoom?: number;
|
|
||||||
maxZoom?: number;
|
|
||||||
}) => {
|
|
||||||
const resolvedViewport = resolveViewportForBounds({
|
|
||||||
bounds,
|
|
||||||
appState,
|
|
||||||
canvasOffsets,
|
|
||||||
fitToViewport,
|
|
||||||
viewportZoomFactor,
|
|
||||||
minZoom,
|
|
||||||
maxZoom,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: {
|
appState: {
|
||||||
...appState,
|
...appState,
|
||||||
scrollX: resolvedViewport.scrollX,
|
scrollX: centerScroll.scrollX,
|
||||||
scrollY: resolvedViewport.scrollY,
|
scrollY: centerScroll.scrollY,
|
||||||
zoom: resolvedViewport.zoom,
|
zoom: { value: newZoomValue },
|
||||||
},
|
},
|
||||||
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
captureUpdate: CaptureUpdateAction.EVENTUALLY,
|
||||||
};
|
};
|
||||||
@@ -459,7 +408,6 @@ export const actionZoomToFitSelectionInViewport = register({
|
|||||||
canvasOffsets: app.getEditorUIOffsets(),
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
predicate: (_elements, appState) => canUseZoomActions(appState),
|
|
||||||
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
// NOTE shift-2 should have been assigned actionZoomToFitSelection.
|
||||||
// TBD on how proceed
|
// TBD on how proceed
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
@@ -486,7 +434,6 @@ export const actionZoomToFitSelection = register({
|
|||||||
canvasOffsets: app.getEditorUIOffsets(),
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
predicate: (_elements, appState) => canUseZoomActions(appState),
|
|
||||||
// NOTE this action should use shift-2 per figma, alas
|
// NOTE this action should use shift-2 per figma, alas
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.THREE &&
|
event.code === CODES.THREE &&
|
||||||
@@ -511,7 +458,6 @@ export const actionZoomToFit = register({
|
|||||||
fitToViewport: false,
|
fitToViewport: false,
|
||||||
canvasOffsets: app.getEditorUIOffsets(),
|
canvasOffsets: app.getEditorUIOffsets(),
|
||||||
}),
|
}),
|
||||||
predicate: (_elements, appState) => canUseZoomActions(appState),
|
|
||||||
keyTest: (event) =>
|
keyTest: (event) =>
|
||||||
event.code === CODES.ONE &&
|
event.code === CODES.ONE &&
|
||||||
event.shiftKey &&
|
event.shiftKey &&
|
||||||
|
|||||||
@@ -329,7 +329,6 @@ export const actionFinalize = register<FormData>({
|
|||||||
selectionElement: null,
|
selectionElement: null,
|
||||||
multiElement: null,
|
multiElement: null,
|
||||||
editingTextElement: null,
|
editingTextElement: null,
|
||||||
startBoundElement: null,
|
|
||||||
suggestedBinding: null,
|
suggestedBinding: null,
|
||||||
selectedElementIds:
|
selectedElementIds:
|
||||||
element &&
|
element &&
|
||||||
|
|||||||
@@ -49,18 +49,9 @@ import { register } from "./register";
|
|||||||
|
|
||||||
import type { AppClassProperties, AppState } from "../types";
|
import type { AppClassProperties, AppState } from "../types";
|
||||||
|
|
||||||
const allElementsInSameGroup = (
|
const allElementsInSameGroup = (elements: readonly ExcalidrawElement[]) => {
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
editingGroupId: AppState["editingGroupId"],
|
|
||||||
) => {
|
|
||||||
if (elements.length >= 2) {
|
if (elements.length >= 2) {
|
||||||
const editingGroupIndex = editingGroupId
|
const groupIds = elements[0].groupIds;
|
||||||
? elements[0].groupIds.indexOf(editingGroupId)
|
|
||||||
: -1;
|
|
||||||
const groupIds =
|
|
||||||
editingGroupIndex > -1
|
|
||||||
? elements[0].groupIds.slice(0, editingGroupIndex)
|
|
||||||
: elements[0].groupIds;
|
|
||||||
for (const groupId of groupIds) {
|
for (const groupId of groupIds) {
|
||||||
if (
|
if (
|
||||||
elements.reduce(
|
elements.reduce(
|
||||||
@@ -87,7 +78,7 @@ const enableActionGroup = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
selectedElements.length >= 2 &&
|
selectedElements.length >= 2 &&
|
||||||
!allElementsInSameGroup(selectedElements, appState.editingGroupId) &&
|
!allElementsInSameGroup(selectedElements) &&
|
||||||
!frameAndChildrenSelectedTogether(selectedElements)
|
!frameAndChildrenSelectedTogether(selectedElements)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,5 +53,8 @@ export const actionToggleSearchMenu = register({
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
checked: (appState: AppState) => appState.gridModeEnabled,
|
checked: (appState: AppState) => appState.gridModeEnabled,
|
||||||
|
predicate: (element, appState, props) => {
|
||||||
|
return props.gridModeEnabled === undefined;
|
||||||
|
},
|
||||||
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F,
|
keyTest: (event) => event[KEYS.CTRL_OR_CMD] && event.key === KEYS.F,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -95,13 +95,6 @@ export class ActionManager {
|
|||||||
(action.name in canvasActions
|
(action.name in canvasActions
|
||||||
? canvasActions[action.name as keyof typeof canvasActions]
|
? canvasActions[action.name as keyof typeof canvasActions]
|
||||||
: true) &&
|
: true) &&
|
||||||
(!action.predicate ||
|
|
||||||
action.predicate(
|
|
||||||
this.getElementsIncludingDeleted(),
|
|
||||||
this.getAppState(),
|
|
||||||
this.app.props,
|
|
||||||
this.app,
|
|
||||||
)) &&
|
|
||||||
action.keyTest &&
|
action.keyTest &&
|
||||||
action.keyTest(
|
action.keyTest(
|
||||||
event,
|
event,
|
||||||
|
|||||||
@@ -99,7 +99,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
open: false,
|
open: false,
|
||||||
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
panels: STATS_PANELS.generalStats | STATS_PANELS.elementProperties,
|
||||||
},
|
},
|
||||||
startBoundElement: null,
|
|
||||||
suggestedBinding: null,
|
suggestedBinding: null,
|
||||||
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
frameRendering: { enabled: true, clip: true, name: true, outline: true },
|
||||||
frameToHighlight: null,
|
frameToHighlight: null,
|
||||||
@@ -122,7 +121,6 @@ export const getDefaultAppState = (): Omit<
|
|||||||
objectsSnapModeEnabled: false,
|
objectsSnapModeEnabled: false,
|
||||||
userToFollow: null,
|
userToFollow: null,
|
||||||
followedBy: new Set(),
|
followedBy: new Set(),
|
||||||
scrollConstraints: null,
|
|
||||||
isCropping: false,
|
isCropping: false,
|
||||||
croppingElementId: null,
|
croppingElementId: null,
|
||||||
searchMatches: null,
|
searchMatches: null,
|
||||||
@@ -232,7 +230,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
selectionElement: { browser: false, export: false, server: false },
|
selectionElement: { browser: false, export: false, server: false },
|
||||||
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
shouldCacheIgnoreZoom: { browser: true, export: false, server: false },
|
||||||
stats: { browser: true, export: false, server: false },
|
stats: { browser: true, export: false, server: false },
|
||||||
startBoundElement: { browser: false, export: false, server: false },
|
|
||||||
suggestedBinding: { browser: false, export: false, server: false },
|
suggestedBinding: { browser: false, export: false, server: false },
|
||||||
frameRendering: { browser: false, export: false, server: false },
|
frameRendering: { browser: false, export: false, server: false },
|
||||||
frameToHighlight: { browser: false, export: false, server: false },
|
frameToHighlight: { browser: false, export: false, server: false },
|
||||||
@@ -251,7 +248,6 @@ const APP_STATE_STORAGE_CONF = (<
|
|||||||
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
objectsSnapModeEnabled: { browser: true, export: false, server: false },
|
||||||
userToFollow: { browser: false, export: false, server: false },
|
userToFollow: { browser: false, export: false, server: false },
|
||||||
followedBy: { browser: false, export: false, server: false },
|
followedBy: { browser: false, export: false, server: false },
|
||||||
scrollConstraints: { browser: true, export: false, server: false },
|
|
||||||
isCropping: { browser: false, export: false, server: false },
|
isCropping: { browser: false, export: false, server: false },
|
||||||
croppingElementId: { browser: false, export: false, server: false },
|
croppingElementId: { browser: false, export: false, server: false },
|
||||||
searchMatches: { browser: false, export: false, server: false },
|
searchMatches: { browser: false, export: false, server: false },
|
||||||
|
|||||||
@@ -326,10 +326,7 @@ import {
|
|||||||
actionToggleCropEditor,
|
actionToggleCropEditor,
|
||||||
} from "../actions";
|
} from "../actions";
|
||||||
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
||||||
import {
|
import { actionToggleHandTool, zoomToFit } from "../actions/actionCanvas";
|
||||||
actionToggleHandTool,
|
|
||||||
resolveViewportForBounds,
|
|
||||||
} from "../actions/actionCanvas";
|
|
||||||
import { actionPaste } from "../actions/actionClipboard";
|
import { actionPaste } from "../actions/actionClipboard";
|
||||||
import { actionCopyElementLink } from "../actions/actionElementLink";
|
import { actionCopyElementLink } from "../actions/actionElementLink";
|
||||||
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
import { actionUnlockAllElements } from "../actions/actionElementLock";
|
||||||
@@ -373,7 +370,7 @@ import {
|
|||||||
hasBackground,
|
hasBackground,
|
||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import { getConstrainedZoomAnchor, getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import {
|
import {
|
||||||
dataURLToString,
|
dataURLToString,
|
||||||
generateIdFromFile,
|
generateIdFromFile,
|
||||||
@@ -429,12 +426,6 @@ import { isMaybeMermaidDefinition } from "../mermaid";
|
|||||||
|
|
||||||
import { LassoTrail } from "../lasso";
|
import { LassoTrail } from "../lasso";
|
||||||
|
|
||||||
import {
|
|
||||||
constrainScrollState,
|
|
||||||
calculateConstrainedScrollCenter,
|
|
||||||
areCanvasTranslatesClose,
|
|
||||||
getScrollConstraintsForBounds,
|
|
||||||
} from "../scene/scrollConstraints";
|
|
||||||
import { EraserTrail } from "../eraser";
|
import { EraserTrail } from "../eraser";
|
||||||
|
|
||||||
import { getShortcutKey } from "../shortcut";
|
import { getShortcutKey } from "../shortcut";
|
||||||
@@ -497,9 +488,6 @@ import type {
|
|||||||
CollaboratorPointer,
|
CollaboratorPointer,
|
||||||
ToolType,
|
ToolType,
|
||||||
OnUserFollowedPayload,
|
OnUserFollowedPayload,
|
||||||
ScrollConstraints,
|
|
||||||
AnimateTranslateCanvasValues,
|
|
||||||
ScrollToContentOptions,
|
|
||||||
UnsubscribeCallback,
|
UnsubscribeCallback,
|
||||||
EmbedsValidationStatus,
|
EmbedsValidationStatus,
|
||||||
ElementsPendingErasure,
|
ElementsPendingErasure,
|
||||||
@@ -552,7 +540,6 @@ const ExcalidrawAppStateContext = React.createContext<AppState>({
|
|||||||
height: 0,
|
height: 0,
|
||||||
offsetLeft: 0,
|
offsetLeft: 0,
|
||||||
offsetTop: 0,
|
offsetTop: 0,
|
||||||
scrollConstraints: null,
|
|
||||||
});
|
});
|
||||||
ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
ExcalidrawAppStateContext.displayName = "ExcalidrawAppStateContext";
|
||||||
|
|
||||||
@@ -607,20 +594,6 @@ let isDraggingScrollBar: boolean = false;
|
|||||||
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
|
let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
|
||||||
let touchTimeout = 0;
|
let touchTimeout = 0;
|
||||||
let invalidateContextMenu = false;
|
let invalidateContextMenu = false;
|
||||||
let scrollConstraintsAnimationTimeout: ReturnType<typeof setTimeout> | null =
|
|
||||||
null;
|
|
||||||
|
|
||||||
const areScrollConstraintsEqual = (
|
|
||||||
left: ScrollConstraints | null | undefined,
|
|
||||||
right: ScrollConstraints | null | undefined,
|
|
||||||
) => {
|
|
||||||
return left === right || (!!left && !!right && isShallowEqual(left, right));
|
|
||||||
};
|
|
||||||
|
|
||||||
const SET_SCROLL_CONSTRAINTS_CONTROLLED_WARNING =
|
|
||||||
"Excalidraw: `setScrollConstraints()` is ignored when the `scrollConstraints` prop is controlled.";
|
|
||||||
const SCROLL_TO_CONTENT_SCROLL_LOCK_CONTROLLED_WARNING =
|
|
||||||
"Excalidraw: `scrollToContent()` with `scrollLock` is ignored when the `scrollConstraints` prop is controlled.";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map of youtube embed video states
|
* Map of youtube embed video states
|
||||||
@@ -630,6 +603,8 @@ const YOUTUBE_VIDEO_STATES = new Map<
|
|||||||
ValueOf<typeof YOUTUBE_STATES>
|
ValueOf<typeof YOUTUBE_STATES>
|
||||||
>();
|
>();
|
||||||
|
|
||||||
|
const MAX_EMBEDDABLE_VIEWPORT_SCALE = 4;
|
||||||
|
|
||||||
let IS_PLAIN_PASTE = false;
|
let IS_PLAIN_PASTE = false;
|
||||||
let IS_PLAIN_PASTE_TIMER = 0;
|
let IS_PLAIN_PASTE_TIMER = 0;
|
||||||
let PLAIN_PASTE_TOAST_SHOWN = false;
|
let PLAIN_PASTE_TOAST_SHOWN = false;
|
||||||
@@ -808,8 +783,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
onUserFollow: (cb) => this.onUserFollowEmitter.on(cb),
|
onUserFollow: (cb) => this.onUserFollowEmitter.on(cb),
|
||||||
onStateChange: this.onStateChange,
|
onStateChange: this.onStateChange,
|
||||||
onEvent: this.onEvent,
|
onEvent: this.onEvent,
|
||||||
app: this,
|
|
||||||
setScrollConstraints: this.setScrollConstraints,
|
|
||||||
};
|
};
|
||||||
return api;
|
return api;
|
||||||
}
|
}
|
||||||
@@ -824,7 +797,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
objectsSnapModeEnabled = false,
|
objectsSnapModeEnabled = false,
|
||||||
theme = defaultAppState.theme,
|
theme = defaultAppState.theme,
|
||||||
name = `${t("labels.untitled")}-${getDateTime()}`,
|
name = `${t("labels.untitled")}-${getDateTime()}`,
|
||||||
scrollConstraints,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
@@ -840,7 +812,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
name,
|
name,
|
||||||
width: window.innerWidth,
|
width: window.innerWidth,
|
||||||
height: window.innerHeight,
|
height: window.innerHeight,
|
||||||
scrollConstraints: scrollConstraints ?? null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
this.refreshEditorInterface();
|
this.refreshEditorInterface();
|
||||||
@@ -1766,6 +1737,18 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.activeEmbeddable?.element === el &&
|
this.state.activeEmbeddable?.element === el &&
|
||||||
this.state.activeEmbeddable?.state === "hover";
|
this.state.activeEmbeddable?.state === "hover";
|
||||||
|
|
||||||
|
// scale video embeds based on zoom (capped) so that smaller embeds
|
||||||
|
// on canvas when zoomed are still of legible quality
|
||||||
|
// (note: for some embed types like gdrive, the quality is poor when
|
||||||
|
// scaling mid playback and works only when you initially start the
|
||||||
|
// playback at the higher zoom level)
|
||||||
|
const shouldScaleEmbeddableViewport = src?.type === "video";
|
||||||
|
const embeddableViewportScale = clamp(
|
||||||
|
shouldScaleEmbeddableViewport ? scale : 1,
|
||||||
|
0.75,
|
||||||
|
MAX_EMBEDDABLE_VIEWPORT_SCALE,
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={el.id}
|
key={el.id}
|
||||||
@@ -1832,31 +1815,42 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
padding: `${el.strokeWidth}px`,
|
padding: `${el.strokeWidth}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(isEmbeddableElement(el)
|
<div
|
||||||
? this.props.renderEmbeddable?.(el, this.state)
|
className="excalidraw__embeddable__content"
|
||||||
: null) ?? (
|
style={{
|
||||||
<iframe
|
width: `${embeddableViewportScale * 100}%`,
|
||||||
ref={(ref) => this.cacheEmbeddableRef(el, ref)}
|
height: `${embeddableViewportScale * 100}%`,
|
||||||
className="excalidraw__embeddable"
|
transform: `scale(${1 / embeddableViewportScale})`,
|
||||||
srcDoc={
|
}}
|
||||||
src?.type === "document"
|
>
|
||||||
? src.srcdoc(this.state.theme)
|
{(isEmbeddableElement(el)
|
||||||
: undefined
|
? this.props.renderEmbeddable?.(el, this.state)
|
||||||
}
|
: null) ?? (
|
||||||
src={
|
<iframe
|
||||||
src?.type !== "document" ? src?.link ?? "" : undefined
|
ref={(ref) => this.cacheEmbeddableRef(el, ref)}
|
||||||
}
|
className="excalidraw__embeddable"
|
||||||
// https://stackoverflow.com/q/18470015
|
srcDoc={
|
||||||
scrolling="no"
|
src?.type === "document"
|
||||||
referrerPolicy="no-referrer-when-downgrade"
|
? src.srcdoc(this.state.theme)
|
||||||
title="Excalidraw Embedded Content"
|
: undefined
|
||||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
}
|
||||||
allowFullScreen={true}
|
src={
|
||||||
sandbox={`${
|
src?.type !== "document" ? src?.link ?? "" : undefined
|
||||||
src?.sandbox?.allowSameOrigin ? "allow-same-origin" : ""
|
}
|
||||||
} allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads`}
|
// https://stackoverflow.com/q/18470015
|
||||||
/>
|
scrolling="no"
|
||||||
)}
|
referrerPolicy="no-referrer-when-downgrade"
|
||||||
|
title="Excalidraw Embedded Content"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen={true}
|
||||||
|
sandbox={`${
|
||||||
|
src?.sandbox?.allowSameOrigin
|
||||||
|
? "allow-same-origin"
|
||||||
|
: ""
|
||||||
|
} allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-presentation allow-downloads`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2976,13 +2970,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
toast: this.state.toast,
|
toast: this.state.toast,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (this.props.scrollConstraints) {
|
if (initialData?.scrollToContent) {
|
||||||
restoredAppState = {
|
|
||||||
...restoredAppState,
|
|
||||||
scrollConstraints: this.props.scrollConstraints,
|
|
||||||
...calculateConstrainedScrollCenter(this.state, restoredAppState),
|
|
||||||
};
|
|
||||||
} else if (initialData?.scrollToContent) {
|
|
||||||
restoredAppState = {
|
restoredAppState = {
|
||||||
...restoredAppState,
|
...restoredAppState,
|
||||||
...calculateScrollCenter(restoredElements, {
|
...calculateScrollCenter(restoredElements, {
|
||||||
@@ -2991,18 +2979,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
height: this.state.height,
|
height: this.state.height,
|
||||||
offsetTop: this.state.offsetTop,
|
offsetTop: this.state.offsetTop,
|
||||||
offsetLeft: this.state.offsetLeft,
|
offsetLeft: this.state.offsetLeft,
|
||||||
scrollConstraints: this.state.scrollConstraints,
|
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialData?.scrollX != null) {
|
|
||||||
restoredAppState.scrollX = initialData.scrollX;
|
|
||||||
}
|
|
||||||
if (initialData?.scrollY != null) {
|
|
||||||
restoredAppState.scrollY = initialData.scrollY;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.resetStore();
|
this.resetStore();
|
||||||
this.resetHistory();
|
this.resetHistory();
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
@@ -3253,11 +3233,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
.forEach((element) => ShapeCache.delete(element));
|
.forEach((element) => ShapeCache.delete(element));
|
||||||
this.refreshEditorInterface();
|
this.refreshEditorInterface();
|
||||||
this.updateDOMRect();
|
this.updateDOMRect();
|
||||||
if (this.state.scrollConstraints) {
|
this.setState({});
|
||||||
this.setState((state) => constrainScrollState(state));
|
|
||||||
} else {
|
|
||||||
this.setState({});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/** generally invoked only if fullscreen was invoked programmatically */
|
/** generally invoked only if fullscreen was invoked programmatically */
|
||||||
@@ -3507,15 +3483,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ viewModeEnabled: !!this.props.viewModeEnabled });
|
this.setState({ viewModeEnabled: !!this.props.viewModeEnabled });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
!areScrollConstraintsEqual(
|
|
||||||
prevProps.scrollConstraints,
|
|
||||||
this.props.scrollConstraints,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this.syncScrollConstraints(this.props.scrollConstraints ?? null);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
if (prevState.viewModeEnabled !== this.state.viewModeEnabled) {
|
||||||
this.addEventListeners();
|
this.addEventListeners();
|
||||||
this.deselectElements();
|
this.deselectElements();
|
||||||
@@ -3576,33 +3543,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.props.onChange?.(elements, this.state, this.files);
|
this.props.onChange?.(elements, this.state, this.files);
|
||||||
this.onChangeEmitter.trigger(elements, this.state, this.files);
|
this.onChangeEmitter.trigger(elements, this.state, this.files);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.scrollConstraints?.animateOnNextUpdate) {
|
|
||||||
const newState = constrainScrollState(this.state, "rigid");
|
|
||||||
const fromValues = {
|
|
||||||
scrollX: this.state.scrollX,
|
|
||||||
scrollY: this.state.scrollY,
|
|
||||||
zoom: this.state.zoom.value,
|
|
||||||
};
|
|
||||||
const toValues = {
|
|
||||||
scrollX: newState.scrollX,
|
|
||||||
scrollY: newState.scrollY,
|
|
||||||
zoom: newState.zoom.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (areCanvasTranslatesClose(fromValues, toValues)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scrollConstraintsAnimationTimeout) {
|
|
||||||
clearTimeout(scrollConstraintsAnimationTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollConstraintsAnimationTimeout = setTimeout(() => {
|
|
||||||
this.cancelInProgressAnimation?.();
|
|
||||||
this.animateToConstrainedArea(fromValues, toValues);
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderInteractiveSceneCallback = ({
|
private renderInteractiveSceneCallback = ({
|
||||||
@@ -4370,8 +4310,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
*/
|
*/
|
||||||
value: number,
|
value: number,
|
||||||
) => {
|
) => {
|
||||||
this.setState(
|
this.setState({
|
||||||
getStateForZoom(
|
...getStateForZoom(
|
||||||
{
|
{
|
||||||
viewportX: this.state.width / 2 + this.state.offsetLeft,
|
viewportX: this.state.width / 2 + this.state.offsetLeft,
|
||||||
viewportY: this.state.height / 2 + this.state.offsetTop,
|
viewportY: this.state.height / 2 + this.state.offsetTop,
|
||||||
@@ -4379,13 +4319,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
},
|
},
|
||||||
this.state,
|
this.state,
|
||||||
),
|
),
|
||||||
);
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private cancelInProgressAnimation: (() => void) | null = null;
|
private cancelInProgressAnimation: (() => void) | null = null;
|
||||||
|
|
||||||
private scrollToContentRequestId = 0;
|
|
||||||
|
|
||||||
scrollToContent = (
|
scrollToContent = (
|
||||||
/**
|
/**
|
||||||
* target to scroll to
|
* target to scroll to
|
||||||
@@ -4397,13 +4335,30 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
| string
|
| string
|
||||||
| ExcalidrawElement
|
| ExcalidrawElement
|
||||||
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
| readonly ExcalidrawElement[] = this.scene.getNonDeletedElements(),
|
||||||
opts?: ScrollToContentOptions,
|
opts?: (
|
||||||
|
| {
|
||||||
|
fitToContent?: boolean;
|
||||||
|
fitToViewport?: never;
|
||||||
|
viewportZoomFactor?: number;
|
||||||
|
animate?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
fitToContent?: never;
|
||||||
|
fitToViewport?: boolean;
|
||||||
|
/** when fitToViewport=true, how much screen should the content cover,
|
||||||
|
* between 0.1 (10%) and 1 (100%)
|
||||||
|
*/
|
||||||
|
viewportZoomFactor?: number;
|
||||||
|
animate?: boolean;
|
||||||
|
duration?: number;
|
||||||
|
}
|
||||||
|
) & {
|
||||||
|
minZoom?: number;
|
||||||
|
maxZoom?: number;
|
||||||
|
canvasOffsets?: Offsets;
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
if (opts?.scrollLock && this.props.scrollConstraints !== undefined) {
|
|
||||||
console.warn(SCROLL_TO_CONTENT_SCROLL_LOCK_CONTROLLED_WARNING);
|
|
||||||
opts = { ...opts, scrollLock: undefined };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof target === "string") {
|
if (typeof target === "string") {
|
||||||
let id: string | null;
|
let id: string | null;
|
||||||
if (isElementLink(target)) {
|
if (isElementLink(target)) {
|
||||||
@@ -4415,29 +4370,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const elements = this.scene.getElementsFromId(id);
|
const elements = this.scene.getElementsFromId(id);
|
||||||
|
|
||||||
if (elements?.length) {
|
if (elements?.length) {
|
||||||
if (opts?.fitToViewport) {
|
this.scrollToContent(elements, {
|
||||||
this.scrollToContent(elements, {
|
fitToContent: opts?.fitToContent ?? true,
|
||||||
fitToViewport: true,
|
animate: opts?.animate ?? true,
|
||||||
viewportZoomFactor: opts.viewportZoomFactor,
|
});
|
||||||
animate: opts.animate ?? true,
|
|
||||||
duration: opts.duration,
|
|
||||||
scrollLock: opts.scrollLock,
|
|
||||||
minZoom: opts.minZoom,
|
|
||||||
maxZoom: opts.maxZoom,
|
|
||||||
canvasOffsets: opts.canvasOffsets,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this.scrollToContent(elements, {
|
|
||||||
fitToContent: opts?.fitToContent ?? true,
|
|
||||||
viewportZoomFactor: opts?.viewportZoomFactor,
|
|
||||||
animate: opts?.animate ?? true,
|
|
||||||
duration: opts?.duration,
|
|
||||||
scrollLock: opts?.scrollLock,
|
|
||||||
minZoom: opts?.minZoom,
|
|
||||||
maxZoom: opts?.maxZoom,
|
|
||||||
canvasOffsets: opts?.canvasOffsets,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (isElementLink(target)) {
|
} else if (isElementLink(target)) {
|
||||||
this.setState({
|
this.setState({
|
||||||
toast: {
|
toast: {
|
||||||
@@ -4452,45 +4388,27 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.cancelInProgressAnimation?.();
|
this.cancelInProgressAnimation?.();
|
||||||
this.scrollToContentRequestId += 1;
|
|
||||||
this.debounceConstrainScrollState.cancel();
|
|
||||||
|
|
||||||
if (scrollConstraintsAnimationTimeout) {
|
|
||||||
clearTimeout(scrollConstraintsAnimationTimeout);
|
|
||||||
scrollConstraintsAnimationTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts?.scrollLock != null && this.state.scrollConstraints) {
|
|
||||||
// Clear the previous lock before starting the next locked transition so
|
|
||||||
// stale constraint enforcement cannot snap us back mid-flight.
|
|
||||||
this.setState({ scrollConstraints: null });
|
|
||||||
}
|
|
||||||
|
|
||||||
// convert provided target into ExcalidrawElement[] if necessary
|
// convert provided target into ExcalidrawElement[] if necessary
|
||||||
const targetElements = Array.isArray(target) ? target : [target];
|
const targetElements = Array.isArray(target) ? target : [target];
|
||||||
|
|
||||||
if (!targetElements.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let zoom = this.state.zoom;
|
let zoom = this.state.zoom;
|
||||||
let scrollX = this.state.scrollX;
|
let scrollX = this.state.scrollX;
|
||||||
let scrollY = this.state.scrollY;
|
let scrollY = this.state.scrollY;
|
||||||
const targetBounds = getCommonBounds(targetElements);
|
|
||||||
|
|
||||||
if (opts?.fitToContent || opts?.fitToViewport) {
|
if (opts?.fitToContent || opts?.fitToViewport) {
|
||||||
const resolvedViewport = resolveViewportForBounds({
|
const { appState } = zoomToFit({
|
||||||
bounds: targetBounds,
|
|
||||||
canvasOffsets: opts.canvasOffsets,
|
canvasOffsets: opts.canvasOffsets,
|
||||||
|
targetElements,
|
||||||
appState: this.state,
|
appState: this.state,
|
||||||
fitToViewport: !!opts?.fitToViewport,
|
fitToViewport: !!opts?.fitToViewport,
|
||||||
viewportZoomFactor: opts?.viewportZoomFactor,
|
viewportZoomFactor: opts?.viewportZoomFactor,
|
||||||
minZoom: opts?.minZoom,
|
minZoom: opts?.minZoom,
|
||||||
maxZoom: opts?.maxZoom,
|
maxZoom: opts?.maxZoom,
|
||||||
});
|
});
|
||||||
zoom = resolvedViewport.zoom;
|
zoom = appState.zoom;
|
||||||
scrollX = resolvedViewport.scrollX;
|
scrollX = appState.scrollX;
|
||||||
scrollY = resolvedViewport.scrollY;
|
scrollY = appState.scrollY;
|
||||||
} else {
|
} else {
|
||||||
// compute only the viewport location, without any zoom adjustment
|
// compute only the viewport location, without any zoom adjustment
|
||||||
const scroll = calculateScrollCenter(targetElements, this.state);
|
const scroll = calculateScrollCenter(targetElements, this.state);
|
||||||
@@ -4498,72 +4416,51 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scrollY = scroll.scrollY;
|
scrollY = scroll.scrollY;
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollConstraints = opts?.scrollLock
|
|
||||||
? getScrollConstraintsForBounds({
|
|
||||||
bounds: targetBounds,
|
|
||||||
zoom,
|
|
||||||
viewportDimensions: {
|
|
||||||
width: this.state.width,
|
|
||||||
height: this.state.height,
|
|
||||||
},
|
|
||||||
scrollLock: opts.scrollLock,
|
|
||||||
})
|
|
||||||
: null;
|
|
||||||
const requestId = this.scrollToContentRequestId;
|
|
||||||
|
|
||||||
const installScrollConstraints = () => {
|
|
||||||
if (!scrollConstraints || requestId !== this.scrollToContentRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
scrollConstraints,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// when animating, we use RequestAnimationFrame to prevent the animation
|
// when animating, we use RequestAnimationFrame to prevent the animation
|
||||||
// from slowing down other processes
|
// from slowing down other processes
|
||||||
if (opts?.animate) {
|
if (opts?.animate) {
|
||||||
const fromValues = {
|
const origScrollX = this.state.scrollX;
|
||||||
scrollX: this.state.scrollX,
|
const origScrollY = this.state.scrollY;
|
||||||
scrollY: this.state.scrollY,
|
const origZoom = this.state.zoom.value;
|
||||||
zoom: this.state.zoom.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
const toValues = { scrollX, scrollY, zoom: zoom.value };
|
const cancel = easeToValuesRAF({
|
||||||
|
fromValues: {
|
||||||
this.animateTranslateCanvas({
|
scrollX: origScrollX,
|
||||||
fromValues,
|
scrollY: origScrollY,
|
||||||
toValues,
|
zoom: origZoom,
|
||||||
duration: opts?.duration ?? 500,
|
},
|
||||||
|
toValues: { scrollX, scrollY, zoom: zoom.value },
|
||||||
|
interpolateValue: (from, to, progress, key) => {
|
||||||
|
// for zoom, use different easing
|
||||||
|
if (key === "zoom") {
|
||||||
|
return from * Math.pow(to / from, easeOut(progress));
|
||||||
|
}
|
||||||
|
// handle using default
|
||||||
|
return undefined;
|
||||||
|
},
|
||||||
|
onStep: ({ scrollX, scrollY, zoom }) => {
|
||||||
|
this.setState({
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
zoom: { value: zoom },
|
||||||
|
});
|
||||||
|
},
|
||||||
onStart: () => {
|
onStart: () => {
|
||||||
this.setState({ shouldCacheIgnoreZoom: true });
|
this.setState({ shouldCacheIgnoreZoom: true });
|
||||||
},
|
},
|
||||||
onEnd: () => {
|
onEnd: () => {
|
||||||
if (requestId !== this.scrollToContentRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(
|
|
||||||
{ shouldCacheIgnoreZoom: false },
|
|
||||||
installScrollConstraints,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
if (requestId !== this.scrollToContentRequestId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ shouldCacheIgnoreZoom: false });
|
this.setState({ shouldCacheIgnoreZoom: false });
|
||||||
},
|
},
|
||||||
|
onCancel: () => {
|
||||||
|
this.setState({ shouldCacheIgnoreZoom: false });
|
||||||
|
},
|
||||||
|
duration: opts?.duration ?? 500,
|
||||||
});
|
});
|
||||||
} else if (scrollConstraints) {
|
|
||||||
this.setState({
|
this.cancelInProgressAnimation = () => {
|
||||||
scrollX,
|
cancel();
|
||||||
scrollY,
|
this.cancelInProgressAnimation = null;
|
||||||
zoom,
|
};
|
||||||
scrollConstraints,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
this.setState({ scrollX, scrollY, zoom });
|
this.setState({ scrollX, scrollY, zoom });
|
||||||
}
|
}
|
||||||
@@ -4577,165 +4474,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
/** use when changing scrollX/scrollY/zoom based on user interaction */
|
/** use when changing scrollX/scrollY/zoom based on user interaction */
|
||||||
private translateCanvas: React.Component<any, AppState>["setState"] = (
|
private translateCanvas: React.Component<any, AppState>["setState"] = (
|
||||||
stateUpdate,
|
state,
|
||||||
) => {
|
) => {
|
||||||
this.cancelInProgressAnimation?.();
|
this.cancelInProgressAnimation?.();
|
||||||
this.maybeUnfollowRemoteUser();
|
this.maybeUnfollowRemoteUser();
|
||||||
|
this.setState(state);
|
||||||
if (scrollConstraintsAnimationTimeout) {
|
|
||||||
clearTimeout(scrollConstraintsAnimationTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
const partialNewState =
|
|
||||||
typeof stateUpdate === "function"
|
|
||||||
? (
|
|
||||||
stateUpdate as (
|
|
||||||
prevState: Readonly<AppState>,
|
|
||||||
props: Readonly<AppProps>,
|
|
||||||
) => AppState
|
|
||||||
)(this.state, this.props)
|
|
||||||
: stateUpdate;
|
|
||||||
|
|
||||||
const newState: AppState = {
|
|
||||||
...this.state,
|
|
||||||
...partialNewState,
|
|
||||||
...(this.state.scrollConstraints && {
|
|
||||||
// manually reset if setState in onCancel wasn't committed yet
|
|
||||||
shouldCacheIgnoreZoom: false,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
// RULE: cannot go below the minimum zoom level if zoom lock is enabled
|
|
||||||
const constrainedState =
|
|
||||||
newState.scrollConstraints && newState.scrollConstraints.lockZoom
|
|
||||||
? constrainScrollState(newState, "elastic")
|
|
||||||
: newState;
|
|
||||||
if (constrainedState.zoom.value > newState.zoom.value) {
|
|
||||||
newState.zoom = constrainedState.zoom;
|
|
||||||
newState.scrollX = constrainedState.scrollX;
|
|
||||||
newState.scrollY = constrainedState.scrollY;
|
|
||||||
|
|
||||||
this.debounceConstrainScrollState(newState);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState(newState);
|
|
||||||
if (this.state.scrollConstraints) {
|
|
||||||
if (newState.zoom.value < this.state.zoom.value) {
|
|
||||||
// zoom-out: debounce to allow centering on user's cursor position before constraining
|
|
||||||
this.debounceConstrainScrollState(newState);
|
|
||||||
} else {
|
|
||||||
// zoom-in or pan: valid range only expands on zoom-in, constrain immediately
|
|
||||||
this.setState(constrainScrollState(newState));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private debounceConstrainScrollState = debounce((state: AppState) => {
|
|
||||||
const newState = constrainScrollState(state, "rigid");
|
|
||||||
|
|
||||||
const fromValues = {
|
|
||||||
scrollX: this.state.scrollX,
|
|
||||||
scrollY: this.state.scrollY,
|
|
||||||
zoom: this.state.zoom.value,
|
|
||||||
};
|
|
||||||
const toValues = {
|
|
||||||
scrollX: newState.scrollX,
|
|
||||||
scrollY: newState.scrollY,
|
|
||||||
zoom: newState.zoom.value,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (areCanvasTranslatesClose(fromValues, toValues)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cancelInProgressAnimation?.();
|
|
||||||
this.animateToConstrainedArea(fromValues, toValues);
|
|
||||||
}, 200);
|
|
||||||
|
|
||||||
private animateToConstrainedArea = (
|
|
||||||
fromValues: AnimateTranslateCanvasValues,
|
|
||||||
toValues: AnimateTranslateCanvasValues,
|
|
||||||
) => {
|
|
||||||
const cleanUp = () => {
|
|
||||||
this.setState((state) => ({
|
|
||||||
shouldCacheIgnoreZoom: false,
|
|
||||||
scrollConstraints: {
|
|
||||||
...state.scrollConstraints!,
|
|
||||||
animateOnNextUpdate: false,
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
this.animateTranslateCanvas({
|
|
||||||
fromValues,
|
|
||||||
toValues,
|
|
||||||
duration: 200,
|
|
||||||
onStart: () => {
|
|
||||||
this.setState((state) => {
|
|
||||||
return {
|
|
||||||
shouldCacheIgnoreZoom: true,
|
|
||||||
scrollConstraints: {
|
|
||||||
...state.scrollConstraints!,
|
|
||||||
animateOnNextUpdate: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onEnd: cleanUp,
|
|
||||||
onCancel: cleanUp,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
private animateTranslateCanvas = ({
|
|
||||||
fromValues,
|
|
||||||
toValues,
|
|
||||||
duration,
|
|
||||||
onStart,
|
|
||||||
onEnd,
|
|
||||||
onCancel,
|
|
||||||
}: {
|
|
||||||
fromValues: AnimateTranslateCanvasValues;
|
|
||||||
toValues: AnimateTranslateCanvasValues;
|
|
||||||
duration: number;
|
|
||||||
onStart: () => void;
|
|
||||||
onEnd: () => void;
|
|
||||||
onCancel: () => void;
|
|
||||||
}) => {
|
|
||||||
const cancel = easeToValuesRAF({
|
|
||||||
fromValues,
|
|
||||||
toValues,
|
|
||||||
interpolateValue: (from, to, progress, key) => {
|
|
||||||
// for zoom, use different easing
|
|
||||||
if (key === "zoom") {
|
|
||||||
return from * Math.pow(to / from, easeOut(progress));
|
|
||||||
}
|
|
||||||
// handle using default
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
onStep: ({ scrollX, scrollY, zoom }) => {
|
|
||||||
this.setState({
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
zoom: { value: zoom },
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onStart,
|
|
||||||
onEnd: () => {
|
|
||||||
this.cancelInProgressAnimation = null;
|
|
||||||
onEnd();
|
|
||||||
},
|
|
||||||
onCancel: () => {
|
|
||||||
this.cancelInProgressAnimation = null;
|
|
||||||
onCancel();
|
|
||||||
},
|
|
||||||
duration,
|
|
||||||
});
|
|
||||||
|
|
||||||
this.cancelInProgressAnimation = () => {
|
|
||||||
cancel();
|
|
||||||
this.cancelInProgressAnimation = null;
|
|
||||||
};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setToast = (toast: AppState["toast"]) => {
|
setToast = (toast: AppState["toast"]) => {
|
||||||
@@ -5916,22 +5659,16 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const initialScale = gesture.initialScale;
|
const initialScale = gesture.initialScale;
|
||||||
if (initialScale) {
|
if (initialScale) {
|
||||||
this.setState((state) =>
|
this.setState((state) => ({
|
||||||
constrainScrollState(
|
...getStateForZoom(
|
||||||
{
|
{
|
||||||
...state,
|
viewportX: this.lastViewportPosition.x,
|
||||||
...getStateForZoom(
|
viewportY: this.lastViewportPosition.y,
|
||||||
{
|
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
||||||
viewportX: this.lastViewportPosition.x,
|
|
||||||
viewportY: this.lastViewportPosition.y,
|
|
||||||
nextZoom: getNormalizedZoom(initialScale * event.scale),
|
|
||||||
},
|
|
||||||
state,
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
"loose",
|
state,
|
||||||
),
|
),
|
||||||
);
|
}));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -7210,7 +6947,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
y: scenePointerY,
|
y: scenePointerY,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
this.setState({ suggestedBinding: null, startBoundElement: null });
|
this.setState({ suggestedBinding: null });
|
||||||
if (!this.state.activeTool.locked) {
|
if (!this.state.activeTool.locked) {
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
@@ -7854,7 +7591,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
appState: {
|
appState: {
|
||||||
newElement: null,
|
newElement: null,
|
||||||
editingTextElement: null,
|
editingTextElement: null,
|
||||||
startBoundElement: null,
|
|
||||||
suggestedBinding: null,
|
suggestedBinding: null,
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
Object.keys(this.state.selectedElementIds)
|
Object.keys(this.state.selectedElementIds)
|
||||||
@@ -9142,18 +8878,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const boundElement = getHoveredElementForBinding(
|
|
||||||
pointFrom<GlobalPoint>(
|
|
||||||
pointerDownState.origin.x,
|
|
||||||
pointerDownState.origin.y,
|
|
||||||
),
|
|
||||||
this.scene.getNonDeletedElements(),
|
|
||||||
this.scene.getNonDeletedElementsMap(),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
newElement: element,
|
newElement: element,
|
||||||
startBoundElement: boundElement,
|
|
||||||
suggestedBinding: null,
|
suggestedBinding: null,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -11006,7 +10732,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
sceneCoords,
|
sceneCoords,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.setState({ suggestedBinding: null, startBoundElement: null });
|
this.setState({ suggestedBinding: null });
|
||||||
if (!activeTool.locked) {
|
if (!activeTool.locked) {
|
||||||
resetCursor(this.interactiveCanvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
@@ -11027,9 +10753,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
this.setState((prevState) => ({
|
this.setState({
|
||||||
newElement: null,
|
newElement: null,
|
||||||
}));
|
});
|
||||||
}
|
}
|
||||||
// so that the scene gets rendered again to display the newly drawn linear as well
|
// so that the scene gets rendered again to display the newly drawn linear as well
|
||||||
this.scene.triggerUpdate();
|
this.scene.triggerUpdate();
|
||||||
@@ -12908,21 +12634,17 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
// reduced amplification for small deltas (small movements on a trackpad)
|
// reduced amplification for small deltas (small movements on a trackpad)
|
||||||
Math.min(1, absDelta / 20);
|
Math.min(1, absDelta / 20);
|
||||||
|
|
||||||
this.translateCanvas((state) => {
|
this.translateCanvas((state) => ({
|
||||||
const nextZoom = getNormalizedZoom(newZoom);
|
...getStateForZoom(
|
||||||
const anchor = getConstrainedZoomAnchor(
|
|
||||||
{
|
{
|
||||||
viewportX: this.lastViewportPosition.x,
|
viewportX: this.lastViewportPosition.x,
|
||||||
viewportY: this.lastViewportPosition.y,
|
viewportY: this.lastViewportPosition.y,
|
||||||
nextZoom,
|
nextZoom: getNormalizedZoom(newZoom),
|
||||||
},
|
},
|
||||||
state,
|
state,
|
||||||
);
|
),
|
||||||
return {
|
shouldCacheIgnoreZoom: true,
|
||||||
...getStateForZoom({ ...anchor, nextZoom }, state),
|
}));
|
||||||
shouldCacheIgnoreZoom: true,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
this.resetShouldCacheIgnoreZoomDebounced();
|
this.resetShouldCacheIgnoreZoomDebounced();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -13080,71 +12802,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
await setLanguage(currentLang);
|
await setLanguage(currentLang);
|
||||||
this.setAppState({});
|
this.setAppState({});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the scroll constraints of the application state.
|
|
||||||
*
|
|
||||||
* @param scrollConstraints - The new scroll constraints.
|
|
||||||
*/
|
|
||||||
public setScrollConstraints = (
|
|
||||||
scrollConstraints: ScrollConstraints | null,
|
|
||||||
) => {
|
|
||||||
if (this.props.scrollConstraints !== undefined) {
|
|
||||||
console.warn(SET_SCROLL_CONSTRAINTS_CONTROLLED_WARNING);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.syncScrollConstraints(scrollConstraints);
|
|
||||||
};
|
|
||||||
|
|
||||||
private syncScrollConstraints = (
|
|
||||||
scrollConstraints: ScrollConstraints | null,
|
|
||||||
) => {
|
|
||||||
this.debounceConstrainScrollState.cancel();
|
|
||||||
|
|
||||||
if (scrollConstraintsAnimationTimeout) {
|
|
||||||
clearTimeout(scrollConstraintsAnimationTimeout);
|
|
||||||
scrollConstraintsAnimationTimeout = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.cancelInProgressAnimation?.();
|
|
||||||
|
|
||||||
if (scrollConstraints) {
|
|
||||||
this.setState(
|
|
||||||
{
|
|
||||||
scrollConstraints,
|
|
||||||
shouldCacheIgnoreZoom: false,
|
|
||||||
},
|
|
||||||
() => {
|
|
||||||
const newState = constrainScrollState(
|
|
||||||
{
|
|
||||||
...this.state,
|
|
||||||
scrollConstraints,
|
|
||||||
},
|
|
||||||
"rigid",
|
|
||||||
);
|
|
||||||
|
|
||||||
this.animateToConstrainedArea(
|
|
||||||
{
|
|
||||||
scrollX: this.state.scrollX,
|
|
||||||
scrollY: this.state.scrollY,
|
|
||||||
zoom: this.state.zoom.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
scrollX: newState.scrollX,
|
|
||||||
scrollY: newState.scrollY,
|
|
||||||
zoom: newState.zoom.value,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.setState({
|
|
||||||
scrollConstraints: null,
|
|
||||||
shouldCacheIgnoreZoom: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -650,8 +650,7 @@ const LayerUI = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
|
const stripIrrelevantAppStateProps = (appState: AppState): UIAppState => {
|
||||||
const { startBoundElement, cursorButton, scrollX, scrollY, ...ret } =
|
const { cursorButton, scrollX, scrollY, ...ret } = appState;
|
||||||
appState;
|
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -814,6 +814,14 @@ body.excalidraw-cursor-resize * {
|
|||||||
.excalidraw__embeddable__outer {
|
.excalidraw__embeddable__outer {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.excalidraw__embeddable__content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform-origin: top left;
|
||||||
|
|
||||||
|
&,
|
||||||
& > * {
|
& > * {
|
||||||
border-radius: var(--embeddable-radius);
|
border-radius: var(--embeddable-radius);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,8 @@ type RestoredAppState = Omit<
|
|||||||
"offsetTop" | "offsetLeft" | "width" | "height"
|
"offsetTop" | "offsetLeft" | "width" | "height"
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
const MAX_ARROW_PX = 75_000;
|
||||||
|
|
||||||
export const AllowedExcalidrawActiveTools: Record<
|
export const AllowedExcalidrawActiveTools: Record<
|
||||||
AppState["activeTool"]["type"],
|
AppState["activeTool"]["type"],
|
||||||
boolean
|
boolean
|
||||||
@@ -467,8 +469,8 @@ export const restoreElement = (
|
|||||||
element.endArrowhead === undefined
|
element.endArrowhead === undefined
|
||||||
? "arrow"
|
? "arrow"
|
||||||
: normalizeArrowhead(element.endArrowhead);
|
: normalizeArrowhead(element.endArrowhead);
|
||||||
const x: number | undefined = element.x;
|
const x = element.x as number | undefined;
|
||||||
const y: number | undefined = element.y;
|
const y = element.y as number | undefined;
|
||||||
const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
const points: readonly LocalPoint[] | undefined = // migrate old arrow model to new one
|
||||||
!Array.isArray(element.points) || element.points.length < 2
|
!Array.isArray(element.points) || element.points.length < 2
|
||||||
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
? [pointFrom(0, 0), pointFrom(element.width, element.height)]
|
||||||
@@ -493,8 +495,8 @@ export const restoreElement = (
|
|||||||
startArrowhead,
|
startArrowhead,
|
||||||
endArrowhead,
|
endArrowhead,
|
||||||
points,
|
points,
|
||||||
x,
|
x: x ?? 0,
|
||||||
y,
|
y: y ?? 0,
|
||||||
elbowed: (element as ExcalidrawArrowElement).elbowed,
|
elbowed: (element as ExcalidrawArrowElement).elbowed,
|
||||||
...getSizeFromPoints(points),
|
...getSizeFromPoints(points),
|
||||||
};
|
};
|
||||||
@@ -513,12 +515,44 @@ export const restoreElement = (
|
|||||||
})
|
})
|
||||||
: restoreElementWithProperties(element as ExcalidrawArrowElement, base);
|
: restoreElementWithProperties(element as ExcalidrawArrowElement, base);
|
||||||
|
|
||||||
return {
|
const normalizedRestoredElement = {
|
||||||
...restoredElement,
|
...restoredElement,
|
||||||
...LinearElementEditor.getNormalizeElementPointsAndCoords(
|
...LinearElementEditor.getNormalizeElementPointsAndCoords(
|
||||||
restoredElement,
|
restoredElement,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Last resort fix for extremely large arrows
|
||||||
|
if (
|
||||||
|
normalizedRestoredElement.width > MAX_ARROW_PX ||
|
||||||
|
normalizedRestoredElement.height > MAX_ARROW_PX
|
||||||
|
) {
|
||||||
|
console.error(
|
||||||
|
`Removing extremely large arrow ${
|
||||||
|
normalizedRestoredElement.id
|
||||||
|
} (type: ${
|
||||||
|
isElbowArrow(normalizedRestoredElement) ? "elbow" : "simple"
|
||||||
|
}, width: ${normalizedRestoredElement.width}, height: ${
|
||||||
|
normalizedRestoredElement.height
|
||||||
|
}, x: ${normalizedRestoredElement.x}, y: ${
|
||||||
|
normalizedRestoredElement.y
|
||||||
|
})`,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
...normalizedRestoredElement,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
points: [
|
||||||
|
pointFrom<LocalPoint>(0, 0),
|
||||||
|
pointFrom<LocalPoint>(100, 100),
|
||||||
|
],
|
||||||
|
isDeleted: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedRestoredElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
// generic elements
|
// generic elements
|
||||||
@@ -666,6 +700,7 @@ export const restoreElements = <T extends ExcalidrawElement>(
|
|||||||
const existingElementsMap = existingElements
|
const existingElementsMap = existingElements
|
||||||
? arrayToMap(existingElements)
|
? arrayToMap(existingElements)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const restoredElements = syncInvalidIndices(
|
const restoredElements = syncInvalidIndices(
|
||||||
(targetElements || []).reduce((elements, element) => {
|
(targetElements || []).reduce((elements, element) => {
|
||||||
// filtering out selection, which is legacy, no longer kept in elements,
|
// filtering out selection, which is legacy, no longer kept in elements,
|
||||||
@@ -762,7 +797,7 @@ export const restoreElements = <T extends ExcalidrawElement>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE (mtolmacs): Temporary fix for extremely large arrows
|
// NOTE (mtolmacs): Temporary fix for invalid/self-bound elbow arrows
|
||||||
// Need to iterate again so we have attached text nodes in elementsMap
|
// Need to iterate again so we have attached text nodes in elementsMap
|
||||||
return restoredElements.map((element) => {
|
return restoredElements.map((element) => {
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
aiEnabled,
|
aiEnabled,
|
||||||
showDeprecatedFonts,
|
showDeprecatedFonts,
|
||||||
renderScrollbars,
|
renderScrollbars,
|
||||||
scrollConstraints,
|
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const canvasActions = props.UIOptions?.canvasActions;
|
const canvasActions = props.UIOptions?.canvasActions;
|
||||||
@@ -209,7 +208,6 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
|
|||||||
aiEnabled={aiEnabled !== false}
|
aiEnabled={aiEnabled !== false}
|
||||||
showDeprecatedFonts={showDeprecatedFonts}
|
showDeprecatedFonts={showDeprecatedFonts}
|
||||||
renderScrollbars={renderScrollbars}
|
renderScrollbars={renderScrollbars}
|
||||||
scrollConstraints={scrollConstraints}
|
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</App>
|
</App>
|
||||||
|
|||||||
@@ -19,8 +19,6 @@ export const renderSnaps = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// in dark mode, we need to adjust the color to account for color inversion.
|
// in dark mode, we need to adjust the color to account for color inversion.
|
||||||
// Don't change if zen mode, because we draw only crosses, we want the
|
|
||||||
// colors to be more visible
|
|
||||||
const snapColor =
|
const snapColor =
|
||||||
appState.theme === THEME.LIGHT || appState.zenModeEnabled
|
appState.theme === THEME.LIGHT || appState.zenModeEnabled
|
||||||
? SNAP_COLOR_LIGHT
|
? SNAP_COLOR_LIGHT
|
||||||
|
|||||||
@@ -1,590 +0,0 @@
|
|||||||
import { MAX_ZOOM } from "@excalidraw/common";
|
|
||||||
|
|
||||||
import { isShallowEqual } from "@excalidraw/common";
|
|
||||||
|
|
||||||
import { clamp } from "@excalidraw/math";
|
|
||||||
|
|
||||||
import { getNormalizedZoom } from "./normalize";
|
|
||||||
|
|
||||||
import type {
|
|
||||||
AnimateTranslateCanvasValues,
|
|
||||||
AppState,
|
|
||||||
ScrollConstraints,
|
|
||||||
ScrollToContentLockOptions,
|
|
||||||
} from "../types";
|
|
||||||
|
|
||||||
// Constants for viewport zoom factor and overscroll allowance
|
|
||||||
const MIN_VIEWPORT_ZOOM_FACTOR = 0.1;
|
|
||||||
const MAX_VIEWPORT_ZOOM_FACTOR = MAX_ZOOM;
|
|
||||||
const DEFAULT_VIEWPORT_ZOOM_FACTOR = 0.2;
|
|
||||||
const DEFAULT_OVERSCROLL_ALLOWANCE = 0.2;
|
|
||||||
|
|
||||||
// Memoization variable to cache constraints for performance optimization
|
|
||||||
let memoizedValues: {
|
|
||||||
previousState: Pick<
|
|
||||||
AppState,
|
|
||||||
"zoom" | "width" | "height" | "scrollConstraints"
|
|
||||||
>;
|
|
||||||
constraints: ReturnType<typeof calculateConstraints>;
|
|
||||||
allowOverscroll: boolean;
|
|
||||||
} | null = null;
|
|
||||||
|
|
||||||
type CanvasTranslate = Pick<AppState, "scrollX" | "scrollY" | "zoom">;
|
|
||||||
|
|
||||||
export const getScrollConstraintsForBounds = ({
|
|
||||||
bounds,
|
|
||||||
zoom,
|
|
||||||
viewportDimensions,
|
|
||||||
scrollLock,
|
|
||||||
}: {
|
|
||||||
bounds: readonly [number, number, number, number];
|
|
||||||
zoom: AppState["zoom"];
|
|
||||||
viewportDimensions: Pick<AppState, "width" | "height">;
|
|
||||||
scrollLock?: boolean | ScrollToContentLockOptions;
|
|
||||||
}): ScrollConstraints => {
|
|
||||||
const [x1, y1, x2, y2] = bounds;
|
|
||||||
const width = x2 - x1;
|
|
||||||
const height = y2 - y1;
|
|
||||||
const baseZoom = Math.min(
|
|
||||||
viewportDimensions.width / width,
|
|
||||||
viewportDimensions.height / height,
|
|
||||||
);
|
|
||||||
const lockOptions =
|
|
||||||
scrollLock && typeof scrollLock === "object" ? scrollLock : undefined;
|
|
||||||
const viewportZoomFactor =
|
|
||||||
lockOptions?.viewportZoomFactor ?? zoom.value / baseZoom;
|
|
||||||
|
|
||||||
return {
|
|
||||||
x: x1,
|
|
||||||
y: y1,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
animateOnNextUpdate: false,
|
|
||||||
lockZoom: !!lockOptions?.lockZoom,
|
|
||||||
overscrollAllowance: lockOptions?.overscrollAllowance,
|
|
||||||
viewportZoomFactor,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the zoom levels necessary to fit the constrained scrollable area within the viewport on the X and Y axes.
|
|
||||||
*
|
|
||||||
* The function considers the dimensions of the scrollable area, the dimensions of the viewport, the viewport zoom factor,
|
|
||||||
* and whether the zoom should be locked. It then calculates the necessary zoom levels for the X and Y axes separately.
|
|
||||||
* If the zoom should be locked, it calculates the maximum zoom level that fits the scrollable area within the viewport,
|
|
||||||
* factoring in the viewport zoom factor. If the zoom should not be locked, the maximum zoom level is set to null.
|
|
||||||
*
|
|
||||||
* @param scrollConstraints - The constraints of the scrollable area including width, height, and position.
|
|
||||||
* @param width - The width of the viewport.
|
|
||||||
* @param height - The height of the viewport.
|
|
||||||
* @returns An object containing the calculated zoom levels for the X and Y axes, and the initial zoom level.
|
|
||||||
*/
|
|
||||||
const calculateZoomLevel = (
|
|
||||||
scrollConstraints: ScrollConstraints,
|
|
||||||
width: AppState["width"],
|
|
||||||
height: AppState["height"],
|
|
||||||
) => {
|
|
||||||
const viewportZoomFactor = scrollConstraints.viewportZoomFactor
|
|
||||||
? clamp(
|
|
||||||
scrollConstraints.viewportZoomFactor,
|
|
||||||
MIN_VIEWPORT_ZOOM_FACTOR,
|
|
||||||
MAX_VIEWPORT_ZOOM_FACTOR,
|
|
||||||
)
|
|
||||||
: DEFAULT_VIEWPORT_ZOOM_FACTOR;
|
|
||||||
|
|
||||||
const scrollableWidth = scrollConstraints.width;
|
|
||||||
const scrollableHeight = scrollConstraints.height;
|
|
||||||
const zoomLevelX = width / scrollableWidth;
|
|
||||||
const zoomLevelY = height / scrollableHeight;
|
|
||||||
const initialZoomLevel = getNormalizedZoom(
|
|
||||||
Math.min(zoomLevelX, zoomLevelY) * viewportZoomFactor,
|
|
||||||
);
|
|
||||||
return { zoomLevelX, zoomLevelY, initialZoomLevel };
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the effective zoom level based on the scroll constraints and current zoom.
|
|
||||||
*
|
|
||||||
* @param params - Object containing scrollConstraints, width, height, and zoom.
|
|
||||||
* @returns An object with the effective zoom level, initial zoom level, and zoom levels for X and Y axes.
|
|
||||||
*/
|
|
||||||
const calculateZoom = ({
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
zoom,
|
|
||||||
}: {
|
|
||||||
scrollConstraints: ScrollConstraints;
|
|
||||||
width: AppState["width"];
|
|
||||||
height: AppState["height"];
|
|
||||||
zoom: AppState["zoom"];
|
|
||||||
}) => {
|
|
||||||
const { zoomLevelX, zoomLevelY, initialZoomLevel } = calculateZoomLevel(
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
);
|
|
||||||
const effectiveZoom = scrollConstraints.lockZoom
|
|
||||||
? Math.max(initialZoomLevel, zoom.value)
|
|
||||||
: zoom.value;
|
|
||||||
return {
|
|
||||||
effectiveZoom: getNormalizedZoom(effectiveZoom),
|
|
||||||
initialZoomLevel,
|
|
||||||
zoomLevelX,
|
|
||||||
zoomLevelY,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the scroll bounds (min and max scroll values) based on the scroll constraints and zoom level.
|
|
||||||
*
|
|
||||||
* @param params - Object containing scrollConstraints, width, height, effectiveZoom, zoomLevelX, zoomLevelY, and allowOverscroll.
|
|
||||||
* @returns An object with min and max scroll values for X and Y axes.
|
|
||||||
*/
|
|
||||||
const calculateScrollBounds = ({
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
effectiveZoom,
|
|
||||||
zoomLevelX,
|
|
||||||
zoomLevelY,
|
|
||||||
allowOverscroll,
|
|
||||||
}: {
|
|
||||||
scrollConstraints: ScrollConstraints;
|
|
||||||
width: AppState["width"];
|
|
||||||
height: AppState["height"];
|
|
||||||
effectiveZoom: number;
|
|
||||||
zoomLevelX: number;
|
|
||||||
zoomLevelY: number;
|
|
||||||
allowOverscroll: boolean;
|
|
||||||
}) => {
|
|
||||||
const overscrollAllowance =
|
|
||||||
scrollConstraints.overscrollAllowance ?? DEFAULT_OVERSCROLL_ALLOWANCE;
|
|
||||||
const validatedOverscroll = clamp(overscrollAllowance, 0, 1);
|
|
||||||
|
|
||||||
const calculateCenter = (zoom: number) => {
|
|
||||||
const centerX =
|
|
||||||
scrollConstraints.x + (scrollConstraints.width - width / zoom) / -2;
|
|
||||||
const centerY =
|
|
||||||
scrollConstraints.y + (scrollConstraints.height - height / zoom) / -2;
|
|
||||||
return { centerX, centerY };
|
|
||||||
};
|
|
||||||
|
|
||||||
const { centerX, centerY } = calculateCenter(effectiveZoom);
|
|
||||||
|
|
||||||
const overscrollValue = Math.min(
|
|
||||||
validatedOverscroll * scrollConstraints.width,
|
|
||||||
validatedOverscroll * scrollConstraints.height,
|
|
||||||
);
|
|
||||||
|
|
||||||
const fitsX = effectiveZoom <= zoomLevelX;
|
|
||||||
const fitsY = effectiveZoom <= zoomLevelY;
|
|
||||||
|
|
||||||
const getScrollRange = (
|
|
||||||
axis: "x" | "y",
|
|
||||||
fits: boolean,
|
|
||||||
constraint: ScrollConstraints,
|
|
||||||
viewportSize: number,
|
|
||||||
zoom: number,
|
|
||||||
overscroll: number,
|
|
||||||
) => {
|
|
||||||
const { pos, size } =
|
|
||||||
axis === "x"
|
|
||||||
? { pos: constraint.x, size: constraint.width }
|
|
||||||
: { pos: constraint.y, size: constraint.height };
|
|
||||||
const center = axis === "x" ? centerX : centerY;
|
|
||||||
if (allowOverscroll) {
|
|
||||||
return fits
|
|
||||||
? { min: center - overscroll, max: center + overscroll }
|
|
||||||
: {
|
|
||||||
min: pos - size + viewportSize / zoom - overscroll,
|
|
||||||
max: pos + overscroll,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return fits
|
|
||||||
? { min: center, max: center }
|
|
||||||
: { min: pos - size + viewportSize / zoom, max: pos };
|
|
||||||
};
|
|
||||||
|
|
||||||
const xRange = getScrollRange(
|
|
||||||
"x",
|
|
||||||
fitsX,
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
effectiveZoom,
|
|
||||||
overscrollValue,
|
|
||||||
);
|
|
||||||
const yRange = getScrollRange(
|
|
||||||
"y",
|
|
||||||
fitsY,
|
|
||||||
scrollConstraints,
|
|
||||||
height,
|
|
||||||
effectiveZoom,
|
|
||||||
overscrollValue,
|
|
||||||
);
|
|
||||||
|
|
||||||
return {
|
|
||||||
minScrollX: xRange.min,
|
|
||||||
maxScrollX: xRange.max,
|
|
||||||
minScrollY: yRange.min,
|
|
||||||
maxScrollY: yRange.max,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the scroll constraints including min and max scroll values and the effective zoom level.
|
|
||||||
*
|
|
||||||
* @param params - Object containing scrollConstraints, width, height, zoom, and allowOverscroll.
|
|
||||||
* @returns An object with min and max scroll values, effective zoom, and initial zoom level.
|
|
||||||
*/
|
|
||||||
const calculateConstraints = ({
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
zoom,
|
|
||||||
allowOverscroll,
|
|
||||||
}: {
|
|
||||||
scrollConstraints: ScrollConstraints;
|
|
||||||
width: AppState["width"];
|
|
||||||
height: AppState["height"];
|
|
||||||
zoom: AppState["zoom"];
|
|
||||||
allowOverscroll: boolean;
|
|
||||||
}) => {
|
|
||||||
const { effectiveZoom, initialZoomLevel, zoomLevelX, zoomLevelY } =
|
|
||||||
calculateZoom({ scrollConstraints, width, height, zoom });
|
|
||||||
const scrollBounds = calculateScrollBounds({
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
effectiveZoom,
|
|
||||||
zoomLevelX,
|
|
||||||
zoomLevelY,
|
|
||||||
allowOverscroll,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...scrollBounds,
|
|
||||||
effectiveZoom: { value: effectiveZoom },
|
|
||||||
initialZoomLevel,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constrains the scroll values within the provided min and max bounds.
|
|
||||||
*
|
|
||||||
* @param params - Object containing scrollX, scrollY, minScrollX, maxScrollX, minScrollY, maxScrollY, and constrainedZoom.
|
|
||||||
* @returns An object with constrained scrollX, scrollY, and zoom.
|
|
||||||
*/
|
|
||||||
const constrainScrollValues = ({
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
minScrollX,
|
|
||||||
maxScrollX,
|
|
||||||
minScrollY,
|
|
||||||
maxScrollY,
|
|
||||||
constrainedZoom,
|
|
||||||
}: {
|
|
||||||
scrollX: number;
|
|
||||||
scrollY: number;
|
|
||||||
minScrollX: number;
|
|
||||||
maxScrollX: number;
|
|
||||||
minScrollY: number;
|
|
||||||
maxScrollY: number;
|
|
||||||
constrainedZoom: AppState["zoom"];
|
|
||||||
}): CanvasTranslate => {
|
|
||||||
const constrainedScrollX = clamp(scrollX, minScrollX, maxScrollX);
|
|
||||||
const constrainedScrollY = clamp(scrollY, minScrollY, maxScrollY);
|
|
||||||
return {
|
|
||||||
scrollX: constrainedScrollX,
|
|
||||||
scrollY: constrainedScrollY,
|
|
||||||
zoom: constrainedZoom,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inverts the scroll constraints to align with the state scrollX and scrollY values, which are inverted.
|
|
||||||
* This is a temporary fix and should be removed once issue #5965 is resolved.
|
|
||||||
*
|
|
||||||
* @param originalScrollConstraints - The original scroll constraints.
|
|
||||||
* @returns The aligned scroll constraints with inverted x and y coordinates.
|
|
||||||
*/
|
|
||||||
const alignScrollConstraints = (
|
|
||||||
originalScrollConstraints: ScrollConstraints,
|
|
||||||
): ScrollConstraints => {
|
|
||||||
return {
|
|
||||||
...originalScrollConstraints,
|
|
||||||
x: originalScrollConstraints.x * -1,
|
|
||||||
y: originalScrollConstraints.y * -1,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determines whether the current viewport is outside the constrained area.
|
|
||||||
*
|
|
||||||
* @param state - The application state.
|
|
||||||
* @returns True if the viewport is outside the constrained area, false otherwise.
|
|
||||||
*/
|
|
||||||
const isViewportOutsideOfConstrainedArea = (state: AppState): boolean => {
|
|
||||||
if (!state.scrollConstraints) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
scrollConstraints: inverseScrollConstraints,
|
|
||||||
zoom,
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
const scrollConstraints = alignScrollConstraints(inverseScrollConstraints);
|
|
||||||
|
|
||||||
const adjustedWidth = width / zoom.value;
|
|
||||||
const adjustedHeight = height / zoom.value;
|
|
||||||
|
|
||||||
return (
|
|
||||||
scrollX > scrollConstraints.x ||
|
|
||||||
scrollX - adjustedWidth < scrollConstraints.x - scrollConstraints.width ||
|
|
||||||
scrollY > scrollConstraints.y ||
|
|
||||||
scrollY - adjustedHeight < scrollConstraints.y - scrollConstraints.height
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calculates the scroll center coordinates and the optimal zoom level to fit the constrained scrollable area within the viewport.
|
|
||||||
*
|
|
||||||
* @param state - The application state.
|
|
||||||
* @param scroll - Object containing current scrollX and scrollY.
|
|
||||||
* @returns An object with the calculated scrollX, scrollY, and zoom.
|
|
||||||
*/
|
|
||||||
export const calculateConstrainedScrollCenter = (
|
|
||||||
state: AppState,
|
|
||||||
{ scrollX, scrollY }: Pick<AppState, "scrollX" | "scrollY">,
|
|
||||||
): CanvasTranslate => {
|
|
||||||
const { width, height, scrollConstraints } = state;
|
|
||||||
if (!scrollConstraints) {
|
|
||||||
return { scrollX, scrollY, zoom: state.zoom };
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjustedConstraints = alignScrollConstraints(scrollConstraints);
|
|
||||||
const zoomLevels = calculateZoomLevel(adjustedConstraints, width, height);
|
|
||||||
const initialZoom = { value: zoomLevels.initialZoomLevel };
|
|
||||||
const constraints = calculateConstraints({
|
|
||||||
scrollConstraints: adjustedConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
zoom: initialZoom,
|
|
||||||
allowOverscroll: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
scrollX: constraints.minScrollX,
|
|
||||||
scrollY: constraints.minScrollY,
|
|
||||||
zoom: constraints.effectiveZoom,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes scroll constraints into a compact string.
|
|
||||||
*
|
|
||||||
* @param constraints - The scroll constraints to encode.
|
|
||||||
* @returns A compact encoded string representing the scroll constraints.
|
|
||||||
*/
|
|
||||||
export const encodeConstraints = (constraints: ScrollConstraints): string => {
|
|
||||||
const payload = {
|
|
||||||
x: constraints.x,
|
|
||||||
y: constraints.y,
|
|
||||||
w: constraints.width,
|
|
||||||
h: constraints.height,
|
|
||||||
a: !!constraints.animateOnNextUpdate,
|
|
||||||
l: !!constraints.lockZoom,
|
|
||||||
v: constraints.viewportZoomFactor ?? 1,
|
|
||||||
oa: constraints.overscrollAllowance ?? DEFAULT_OVERSCROLL_ALLOWANCE,
|
|
||||||
};
|
|
||||||
const serialized = JSON.stringify(payload);
|
|
||||||
return encodeURIComponent(window.btoa(serialized).replace(/=+/, ""));
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a compact string back into scroll constraints.
|
|
||||||
*
|
|
||||||
* @param encoded - The encoded string representing the scroll constraints.
|
|
||||||
* @returns The decoded scroll constraints object.
|
|
||||||
*/
|
|
||||||
export const decodeConstraints = (encoded: string): ScrollConstraints => {
|
|
||||||
try {
|
|
||||||
const decodedStr = window.atob(decodeURIComponent(encoded));
|
|
||||||
const parsed = JSON.parse(decodedStr) as {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
w: number;
|
|
||||||
h: number;
|
|
||||||
a: boolean;
|
|
||||||
l: boolean;
|
|
||||||
v: number;
|
|
||||||
oa: number;
|
|
||||||
};
|
|
||||||
return {
|
|
||||||
x: parsed.x || 0,
|
|
||||||
y: parsed.y || 0,
|
|
||||||
width: parsed.w || 0,
|
|
||||||
height: parsed.h || 0,
|
|
||||||
lockZoom: parsed.l || false,
|
|
||||||
viewportZoomFactor: parsed.v || 1,
|
|
||||||
animateOnNextUpdate: parsed.a || false,
|
|
||||||
overscrollAllowance: parsed.oa || DEFAULT_OVERSCROLL_ALLOWANCE,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
return {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
animateOnNextUpdate: false,
|
|
||||||
lockZoom: false,
|
|
||||||
viewportZoomFactor: 1,
|
|
||||||
overscrollAllowance: DEFAULT_OVERSCROLL_ALLOWANCE,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
type Options = { allowOverscroll: boolean; disableAnimation: boolean };
|
|
||||||
const DEFAULT_OPTION: Options = {
|
|
||||||
allowOverscroll: true,
|
|
||||||
disableAnimation: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constrains the AppState scroll values within the defined scroll constraints.
|
|
||||||
*
|
|
||||||
* constraintMode can be "elastic", "rigid", or "loose":
|
|
||||||
* - "elastic": snaps to constraints but allows overscroll
|
|
||||||
* - "rigid": snaps to constraints without overscroll
|
|
||||||
* - "loose": allows overscroll and disables animation/snapping to constraints
|
|
||||||
*
|
|
||||||
* @param state - The original AppState.
|
|
||||||
* @param options - Options for allowing overscroll and disabling animation.
|
|
||||||
* @returns A new AppState object with constrained scroll values.
|
|
||||||
*/
|
|
||||||
export const constrainScrollState = (
|
|
||||||
state: AppState,
|
|
||||||
constraintMode: "elastic" | "rigid" | "loose" = "elastic",
|
|
||||||
): AppState => {
|
|
||||||
if (!state.scrollConstraints) {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
const {
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
scrollConstraints: inverseScrollConstraints,
|
|
||||||
zoom,
|
|
||||||
} = state;
|
|
||||||
|
|
||||||
let allowOverscroll: boolean;
|
|
||||||
let disableAnimation: boolean;
|
|
||||||
|
|
||||||
switch (constraintMode) {
|
|
||||||
case "elastic":
|
|
||||||
({ allowOverscroll, disableAnimation } = DEFAULT_OPTION);
|
|
||||||
break;
|
|
||||||
case "rigid":
|
|
||||||
allowOverscroll = false;
|
|
||||||
disableAnimation = false;
|
|
||||||
break;
|
|
||||||
case "loose":
|
|
||||||
allowOverscroll = true;
|
|
||||||
disableAnimation = true;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
({ allowOverscroll, disableAnimation } = DEFAULT_OPTION);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const scrollConstraints = alignScrollConstraints(inverseScrollConstraints);
|
|
||||||
|
|
||||||
const canUseMemoizedValues =
|
|
||||||
memoizedValues &&
|
|
||||||
memoizedValues.previousState.scrollConstraints &&
|
|
||||||
memoizedValues.allowOverscroll === allowOverscroll &&
|
|
||||||
isShallowEqual(
|
|
||||||
state.scrollConstraints,
|
|
||||||
memoizedValues.previousState.scrollConstraints,
|
|
||||||
) &&
|
|
||||||
isShallowEqual(
|
|
||||||
{ zoom: zoom.value, width, height },
|
|
||||||
{
|
|
||||||
zoom: memoizedValues.previousState.zoom.value,
|
|
||||||
width: memoizedValues.previousState.width,
|
|
||||||
height: memoizedValues.previousState.height,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const constraints = canUseMemoizedValues
|
|
||||||
? memoizedValues!.constraints
|
|
||||||
: calculateConstraints({
|
|
||||||
scrollConstraints,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
zoom,
|
|
||||||
allowOverscroll,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!canUseMemoizedValues) {
|
|
||||||
memoizedValues = {
|
|
||||||
previousState: {
|
|
||||||
zoom: state.zoom,
|
|
||||||
width: state.width,
|
|
||||||
height: state.height,
|
|
||||||
scrollConstraints: state.scrollConstraints,
|
|
||||||
},
|
|
||||||
constraints,
|
|
||||||
allowOverscroll,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const constrainedValues =
|
|
||||||
zoom.value >= constraints.effectiveZoom.value
|
|
||||||
? constrainScrollValues({
|
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
minScrollX: constraints.minScrollX,
|
|
||||||
maxScrollX: constraints.maxScrollX,
|
|
||||||
minScrollY: constraints.minScrollY,
|
|
||||||
maxScrollY: constraints.maxScrollY,
|
|
||||||
constrainedZoom: constraints.effectiveZoom,
|
|
||||||
})
|
|
||||||
: calculateConstrainedScrollCenter(state, { scrollX, scrollY });
|
|
||||||
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
scrollConstraints: {
|
|
||||||
...state.scrollConstraints,
|
|
||||||
animateOnNextUpdate: disableAnimation
|
|
||||||
? false
|
|
||||||
: isViewportOutsideOfConstrainedArea(state),
|
|
||||||
},
|
|
||||||
...constrainedValues,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if two canvas translate values are close within a threshold.
|
|
||||||
*
|
|
||||||
* @param from - First set of canvas translate values.
|
|
||||||
* @param to - Second set of canvas translate values.
|
|
||||||
* @returns True if the values are close, false otherwise.
|
|
||||||
*/
|
|
||||||
export const areCanvasTranslatesClose = (
|
|
||||||
from: AnimateTranslateCanvasValues,
|
|
||||||
to: AnimateTranslateCanvasValues,
|
|
||||||
): boolean => {
|
|
||||||
const threshold = 0.1;
|
|
||||||
return (
|
|
||||||
Math.abs(from.scrollX - to.scrollX) < threshold &&
|
|
||||||
Math.abs(from.scrollY - to.scrollY) < threshold &&
|
|
||||||
Math.abs(from.zoom - to.zoom) < threshold
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,67 +1,5 @@
|
|||||||
import { constrainScrollState } from "./scrollConstraints";
|
|
||||||
|
|
||||||
import type { AppState, NormalizedZoomValue } from "../types";
|
import type { AppState, NormalizedZoomValue } from "../types";
|
||||||
|
|
||||||
/**
|
|
||||||
* When zooming out with scroll constraints active, the cursor-anchored zoom may
|
|
||||||
* produce a scroll position outside the valid bounds, causing a snap-back.
|
|
||||||
*
|
|
||||||
* This function adjusts the effective zoom anchor point so the resulting scroll
|
|
||||||
* stays within bounds — silently, without any snap. When no adjustment is needed
|
|
||||||
* (zoom-in, no constraints, or already within bounds) it returns the original
|
|
||||||
* viewport position unchanged.
|
|
||||||
*/
|
|
||||||
export const getConstrainedZoomAnchor = (
|
|
||||||
{
|
|
||||||
viewportX,
|
|
||||||
viewportY,
|
|
||||||
nextZoom,
|
|
||||||
}: { viewportX: number; viewportY: number; nextZoom: NormalizedZoomValue },
|
|
||||||
state: AppState,
|
|
||||||
): { viewportX: number; viewportY: number } => {
|
|
||||||
if (!state.scrollConstraints || nextZoom >= state.zoom.value) {
|
|
||||||
return { viewportX, viewportY };
|
|
||||||
}
|
|
||||||
|
|
||||||
const appLayerX = viewportX - state.offsetLeft;
|
|
||||||
const appLayerY = viewportY - state.offsetTop;
|
|
||||||
const factor = 1 / nextZoom - 1 / state.zoom.value;
|
|
||||||
|
|
||||||
const newScrollX = state.scrollX + appLayerX * factor;
|
|
||||||
const newScrollY = state.scrollY + appLayerY * factor;
|
|
||||||
|
|
||||||
const constrained = constrainScrollState(
|
|
||||||
{
|
|
||||||
...state,
|
|
||||||
scrollX: newScrollX,
|
|
||||||
scrollY: newScrollY,
|
|
||||||
zoom: { value: nextZoom },
|
|
||||||
},
|
|
||||||
"rigid",
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
constrained.scrollX === newScrollX &&
|
|
||||||
constrained.scrollY === newScrollY
|
|
||||||
) {
|
|
||||||
return { viewportX, viewportY };
|
|
||||||
}
|
|
||||||
|
|
||||||
const adjustedAppLayerX =
|
|
||||||
constrained.scrollX !== newScrollX
|
|
||||||
? (constrained.scrollX - state.scrollX) / factor
|
|
||||||
: appLayerX;
|
|
||||||
const adjustedAppLayerY =
|
|
||||||
constrained.scrollY !== newScrollY
|
|
||||||
? (constrained.scrollY - state.scrollY) / factor
|
|
||||||
: appLayerY;
|
|
||||||
|
|
||||||
return {
|
|
||||||
viewportX: adjustedAppLayerX + state.offsetLeft,
|
|
||||||
viewportY: adjustedAppLayerY + state.offsetTop,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStateForZoom = (
|
export const getStateForZoom = (
|
||||||
{
|
{
|
||||||
viewportX,
|
viewportX,
|
||||||
|
|||||||
+538
-84
@@ -39,6 +39,14 @@ import type {
|
|||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
const SNAP_DISTANCE = 8;
|
const SNAP_DISTANCE = 8;
|
||||||
|
// Keep snap candidates with effectively identical offsets together. This needs
|
||||||
|
// to be wider than the 6-decimal rounding quantum to absorb JS floating-point
|
||||||
|
// noise around values like 4 and 4.000001.
|
||||||
|
const SNAP_OFFSET_TOLERANCE = 0.00001;
|
||||||
|
// Point snaps can come from both nearby and distant references within snap
|
||||||
|
// distance. If their visual distances have a large break, prefer the nearest
|
||||||
|
// cluster before comparing offsets.
|
||||||
|
const SNAP_REFERENCE_CLUSTER_BREAK_DISTANCE = 200;
|
||||||
|
|
||||||
// do not comput more gaps per axis than this limit
|
// do not comput more gaps per axis than this limit
|
||||||
// TODO increase or remove once we optimize
|
// TODO increase or remove once we optimize
|
||||||
@@ -49,16 +57,51 @@ export const getSnapDistance = (zoomValue: number) => {
|
|||||||
return SNAP_DISTANCE / zoomValue;
|
return SNAP_DISTANCE / zoomValue;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps the cluster threshold visually stable across zoom levels.
|
||||||
|
*/
|
||||||
|
const getSnapReferenceClusterBreakDistance = (zoomValue: number) => {
|
||||||
|
return SNAP_REFERENCE_CLUSTER_BREAK_DISTANCE / zoomValue;
|
||||||
|
};
|
||||||
|
|
||||||
type Vector2D = {
|
type Vector2D = {
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PointPair = [GlobalPoint, GlobalPoint];
|
type PointPair = [GlobalPoint, GlobalPoint];
|
||||||
|
type SnapPointType = "outer" | "center";
|
||||||
|
type SnapSourceId = string;
|
||||||
|
|
||||||
|
const SELECTION_SNAP_SOURCE_ID = "selection";
|
||||||
|
|
||||||
|
// Snap source ids let us filter redundant snaplines only within one
|
||||||
|
// selection-to-reference relationship, without mixing separate reference groups.
|
||||||
|
// For example, "selection->rectA" may collapse to its outermost snaplines,
|
||||||
|
// while "selection->rectB" still keeps its own independent snaplines.
|
||||||
|
type SnapPoint = {
|
||||||
|
point: GlobalPoint;
|
||||||
|
type: SnapPointType;
|
||||||
|
snapSourceId: SnapSourceId;
|
||||||
|
};
|
||||||
|
|
||||||
|
const outerSnapPoint = (
|
||||||
|
point: GlobalPoint,
|
||||||
|
snapSourceId = SELECTION_SNAP_SOURCE_ID,
|
||||||
|
): SnapPoint => ({
|
||||||
|
point,
|
||||||
|
type: "outer",
|
||||||
|
snapSourceId,
|
||||||
|
});
|
||||||
|
|
||||||
export type PointSnap = {
|
export type PointSnap = {
|
||||||
type: "point";
|
type: "point";
|
||||||
points: PointPair;
|
points: PointPair;
|
||||||
|
pointTypes: [SnapPointType, SnapPointType];
|
||||||
|
snapSourceIds: [SnapSourceId, SnapSourceId];
|
||||||
|
// Distance along the rendered snapline, used to group visually nearby
|
||||||
|
// references before choosing which point snaps should win.
|
||||||
|
visualDistance: number;
|
||||||
offset: number;
|
offset: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -120,14 +163,14 @@ export type SnapLine = PointSnapLine | GapSnapLine | PointerSnapLine;
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
export class SnapCache {
|
export class SnapCache {
|
||||||
private static referenceSnapPoints: GlobalPoint[] | null = null;
|
private static referenceSnapPoints: SnapPoint[] | null = null;
|
||||||
|
|
||||||
private static visibleGaps: {
|
private static visibleGaps: {
|
||||||
verticalGaps: Gap[];
|
verticalGaps: Gap[];
|
||||||
horizontalGaps: Gap[];
|
horizontalGaps: Gap[];
|
||||||
} | null = null;
|
} | null = null;
|
||||||
|
|
||||||
public static setReferenceSnapPoints = (snapPoints: GlobalPoint[] | null) => {
|
public static setReferenceSnapPoints = (snapPoints: SnapPoint[] | null) => {
|
||||||
SnapCache.referenceSnapPoints = snapPoints;
|
SnapCache.referenceSnapPoints = snapPoints;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,6 +238,17 @@ export const areRoughlyEqual = (a: number, b: number, precision = 0.01) => {
|
|||||||
return Math.abs(a - b) <= precision;
|
return Math.abs(a - b) <= precision;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps nearly identical snap offsets in the same winner set. This prevents
|
||||||
|
* snapline flicker when rounded geometry differs by a tiny floating-point tail.
|
||||||
|
*/
|
||||||
|
const isWithinSnapOffset = (offset: number, minOffset: number) => {
|
||||||
|
return (
|
||||||
|
Math.abs(offset) <= minOffset ||
|
||||||
|
areRoughlyEqual(Math.abs(offset), minOffset, SNAP_OFFSET_TOLERANCE)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const getElementsCorners = (
|
export const getElementsCorners = (
|
||||||
elements: ExcalidrawElement[],
|
elements: ExcalidrawElement[],
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
@@ -312,18 +366,81 @@ export const getElementsCorners = (
|
|||||||
return result.map((p) => pointFrom(round(p[0]), round(p[1])));
|
return result.map((p) => pointFrom(round(p[0]), round(p[1])));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getElementsSnapPoints = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
elementsMap: ElementsMap,
|
||||||
|
options: Parameters<typeof getElementsCorners>[2] = {},
|
||||||
|
snapSourceId = SELECTION_SNAP_SOURCE_ID,
|
||||||
|
): SnapPoint[] => {
|
||||||
|
const points = getElementsCorners(elements, elementsMap, options);
|
||||||
|
// getElementsCorners() appends the center point last unless omitCenter is set.
|
||||||
|
const hasCenterPoint = !options.omitCenter && points.length > 0;
|
||||||
|
|
||||||
|
return points.map((point, index) => ({
|
||||||
|
point,
|
||||||
|
type: hasCenterPoint && index === points.length - 1 ? "center" : "outer",
|
||||||
|
snapSourceId,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a stable identity for one snap reference group. Sorting keeps grouped
|
||||||
|
* elements represented by the same snap source id regardless of element order.
|
||||||
|
*/
|
||||||
|
const getSnapSourceId = (elements: ExcalidrawElement[]) => {
|
||||||
|
return elements
|
||||||
|
.map((element) => element.id)
|
||||||
|
.sort()
|
||||||
|
.join(",");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds frames that the current selection already belongs to. Their children
|
||||||
|
* are still valid snap references; children of unrelated frames are not.
|
||||||
|
*/
|
||||||
|
const getSelectedFrameIdsForSnapping = (
|
||||||
|
selectedElements: readonly ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const selectedFrameIds = new Set<ExcalidrawElement["id"]>();
|
||||||
|
|
||||||
|
for (const element of selectedElements) {
|
||||||
|
if (element.frameId) {
|
||||||
|
selectedFrameIds.add(element.frameId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedFrameIds;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Frame children are only snap references when the dragged selection is inside
|
||||||
|
* the same frame. This avoids snapping external elements to internals of a
|
||||||
|
* framed diagram.
|
||||||
|
*/
|
||||||
|
const canUseElementAsSnapReference = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
selectedFrameIds: Set<ExcalidrawElement["id"]>,
|
||||||
|
) => {
|
||||||
|
return !element.frameId || selectedFrameIds.has(element.frameId);
|
||||||
|
};
|
||||||
|
|
||||||
const getReferenceElements = (
|
const getReferenceElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
selectedElements: NonDeletedExcalidrawElement[],
|
selectedElements: NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
elementsMap: ElementsMap,
|
elementsMap: ElementsMap,
|
||||||
) =>
|
) => {
|
||||||
getVisibleAndNonSelectedElements(
|
const selectedFrameIds = getSelectedFrameIdsForSnapping(selectedElements);
|
||||||
|
|
||||||
|
return getVisibleAndNonSelectedElements(
|
||||||
elements,
|
elements,
|
||||||
selectedElements,
|
selectedElements,
|
||||||
appState,
|
appState,
|
||||||
elementsMap,
|
elementsMap,
|
||||||
);
|
).filter((element) => {
|
||||||
|
return canUseElementAsSnapReference(element, selectedFrameIds);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const getVisibleGaps = (
|
export const getVisibleGaps = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
@@ -482,7 +599,10 @@ const getGapSnaps = (
|
|||||||
const centerOffset = round(gapMidX - centerX);
|
const centerOffset = round(gapMidX - centerX);
|
||||||
const gapIsLargerThanSelection = gap.length > maxX - minX;
|
const gapIsLargerThanSelection = gap.length > maxX - minX;
|
||||||
|
|
||||||
if (gapIsLargerThanSelection && Math.abs(centerOffset) <= minOffset.x) {
|
if (
|
||||||
|
gapIsLargerThanSelection &&
|
||||||
|
isWithinSnapOffset(centerOffset, minOffset.x)
|
||||||
|
) {
|
||||||
if (Math.abs(centerOffset) < minOffset.x) {
|
if (Math.abs(centerOffset) < minOffset.x) {
|
||||||
nearestSnapsX.length = 0;
|
nearestSnapsX.length = 0;
|
||||||
}
|
}
|
||||||
@@ -504,7 +624,7 @@ const getGapSnaps = (
|
|||||||
const distanceToEndElementX = minX - endMaxX;
|
const distanceToEndElementX = minX - endMaxX;
|
||||||
const sideOffsetRight = round(gap.length - distanceToEndElementX);
|
const sideOffsetRight = round(gap.length - distanceToEndElementX);
|
||||||
|
|
||||||
if (Math.abs(sideOffsetRight) <= minOffset.x) {
|
if (isWithinSnapOffset(sideOffsetRight, minOffset.x)) {
|
||||||
if (Math.abs(sideOffsetRight) < minOffset.x) {
|
if (Math.abs(sideOffsetRight) < minOffset.x) {
|
||||||
nearestSnapsX.length = 0;
|
nearestSnapsX.length = 0;
|
||||||
}
|
}
|
||||||
@@ -525,7 +645,7 @@ const getGapSnaps = (
|
|||||||
const distanceToStartElementX = startMinX - maxX;
|
const distanceToStartElementX = startMinX - maxX;
|
||||||
const sideOffsetLeft = round(distanceToStartElementX - gap.length);
|
const sideOffsetLeft = round(distanceToStartElementX - gap.length);
|
||||||
|
|
||||||
if (Math.abs(sideOffsetLeft) <= minOffset.x) {
|
if (isWithinSnapOffset(sideOffsetLeft, minOffset.x)) {
|
||||||
if (Math.abs(sideOffsetLeft) < minOffset.x) {
|
if (Math.abs(sideOffsetLeft) < minOffset.x) {
|
||||||
nearestSnapsX.length = 0;
|
nearestSnapsX.length = 0;
|
||||||
}
|
}
|
||||||
@@ -551,7 +671,10 @@ const getGapSnaps = (
|
|||||||
const centerOffset = round(gapMidY - centerY);
|
const centerOffset = round(gapMidY - centerY);
|
||||||
const gapIsLargerThanSelection = gap.length > maxY - minY;
|
const gapIsLargerThanSelection = gap.length > maxY - minY;
|
||||||
|
|
||||||
if (gapIsLargerThanSelection && Math.abs(centerOffset) <= minOffset.y) {
|
if (
|
||||||
|
gapIsLargerThanSelection &&
|
||||||
|
isWithinSnapOffset(centerOffset, minOffset.y)
|
||||||
|
) {
|
||||||
if (Math.abs(centerOffset) < minOffset.y) {
|
if (Math.abs(centerOffset) < minOffset.y) {
|
||||||
nearestSnapsY.length = 0;
|
nearestSnapsY.length = 0;
|
||||||
}
|
}
|
||||||
@@ -573,7 +696,7 @@ const getGapSnaps = (
|
|||||||
const distanceToStartElementY = startMinY - maxY;
|
const distanceToStartElementY = startMinY - maxY;
|
||||||
const sideOffsetTop = round(distanceToStartElementY - gap.length);
|
const sideOffsetTop = round(distanceToStartElementY - gap.length);
|
||||||
|
|
||||||
if (Math.abs(sideOffsetTop) <= minOffset.y) {
|
if (isWithinSnapOffset(sideOffsetTop, minOffset.y)) {
|
||||||
if (Math.abs(sideOffsetTop) < minOffset.y) {
|
if (Math.abs(sideOffsetTop) < minOffset.y) {
|
||||||
nearestSnapsY.length = 0;
|
nearestSnapsY.length = 0;
|
||||||
}
|
}
|
||||||
@@ -594,7 +717,7 @@ const getGapSnaps = (
|
|||||||
const distanceToEndElementY = round(minY - endMaxY);
|
const distanceToEndElementY = round(minY - endMaxY);
|
||||||
const sideOffsetBottom = gap.length - distanceToEndElementY;
|
const sideOffsetBottom = gap.length - distanceToEndElementY;
|
||||||
|
|
||||||
if (Math.abs(sideOffsetBottom) <= minOffset.y) {
|
if (isWithinSnapOffset(sideOffsetBottom, minOffset.y)) {
|
||||||
if (Math.abs(sideOffsetBottom) < minOffset.y) {
|
if (Math.abs(sideOffsetBottom) < minOffset.y) {
|
||||||
nearestSnapsY.length = 0;
|
nearestSnapsY.length = 0;
|
||||||
}
|
}
|
||||||
@@ -613,6 +736,102 @@ const getGapSnaps = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Narrows point snaps to the nearest visual cluster, then keeps the best offset
|
||||||
|
* inside that cluster. If all references look continuous, this preserves the
|
||||||
|
* old smallest-offset behavior.
|
||||||
|
*/
|
||||||
|
const filterPointSnapsToNearestCluster = (
|
||||||
|
snaps: Snaps,
|
||||||
|
clusterBreakDistance: number,
|
||||||
|
) => {
|
||||||
|
const pointSnaps = snaps.filter(
|
||||||
|
(snap): snap is PointSnap => snap.type === "point",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pointSnaps.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const snapsByOffset = new Map<number, PointSnap[]>();
|
||||||
|
|
||||||
|
for (const snap of pointSnaps) {
|
||||||
|
const offset = round(snap.offset);
|
||||||
|
const offsetSnaps = snapsByOffset.get(offset) ?? [];
|
||||||
|
offsetSnaps.push(snap);
|
||||||
|
snapsByOffset.set(offset, offsetSnaps);
|
||||||
|
}
|
||||||
|
|
||||||
|
const keptPointSnaps = new Set<PointSnap>();
|
||||||
|
const offsetGroups = Array.from(snapsByOffset.entries()).map(
|
||||||
|
([offset, offsetSnaps]) => ({
|
||||||
|
offset,
|
||||||
|
snaps: offsetSnaps,
|
||||||
|
visualDistance: Math.min(
|
||||||
|
...offsetSnaps.map((snap) => snap.visualDistance),
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const sortedOffsetGroups = offsetGroups
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.visualDistance - b.visualDistance);
|
||||||
|
let keepOffsetGroupsUntil = sortedOffsetGroups.length;
|
||||||
|
|
||||||
|
// Prefer a nearby reference cluster before comparing offset. If there is no
|
||||||
|
// clear distance break, this keeps all offsets and falls back to the old
|
||||||
|
// smallest-offset behavior.
|
||||||
|
for (let i = 1; i < sortedOffsetGroups.length; i++) {
|
||||||
|
const distanceGap =
|
||||||
|
sortedOffsetGroups[i].visualDistance -
|
||||||
|
sortedOffsetGroups[i - 1].visualDistance;
|
||||||
|
|
||||||
|
if (distanceGap > clusterBreakDistance) {
|
||||||
|
keepOffsetGroupsUntil = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const nearestOffsetGroups = sortedOffsetGroups.slice(
|
||||||
|
0,
|
||||||
|
keepOffsetGroupsUntil,
|
||||||
|
);
|
||||||
|
const minOffset = Math.min(
|
||||||
|
...nearestOffsetGroups.map(({ offset }) => Math.abs(offset)),
|
||||||
|
);
|
||||||
|
const selectedOffsets = nearestOffsetGroups
|
||||||
|
.filter(({ offset }) =>
|
||||||
|
areRoughlyEqual(Math.abs(offset), minOffset, SNAP_OFFSET_TOLERANCE),
|
||||||
|
)
|
||||||
|
.map(({ offset }) => offset);
|
||||||
|
|
||||||
|
for (const offsetSnaps of snapsByOffset.values()) {
|
||||||
|
if (
|
||||||
|
!selectedOffsets.some((offset) =>
|
||||||
|
areRoughlyEqual(
|
||||||
|
round(offsetSnaps[0].offset),
|
||||||
|
offset,
|
||||||
|
SNAP_OFFSET_TOLERANCE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
offsetSnaps.forEach((snap) => keptPointSnaps.add(snap));
|
||||||
|
}
|
||||||
|
|
||||||
|
let writeIndex = 0;
|
||||||
|
|
||||||
|
for (const snap of snaps) {
|
||||||
|
if (snap.type !== "point" || keptPointSnaps.has(snap)) {
|
||||||
|
snaps[writeIndex] = snap;
|
||||||
|
writeIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snaps.length = writeIndex;
|
||||||
|
};
|
||||||
|
|
||||||
export const getReferenceSnapPoints = (
|
export const getReferenceSnapPoints = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
@@ -630,12 +849,24 @@ export const getReferenceSnapPoints = (
|
|||||||
(elementsGroup) =>
|
(elementsGroup) =>
|
||||||
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
|
!(elementsGroup.length === 1 && isBoundToContainer(elementsGroup[0])),
|
||||||
)
|
)
|
||||||
.flatMap((elementGroup) => getElementsCorners(elementGroup, elementsMap));
|
.flatMap((elementGroup) =>
|
||||||
|
getElementsSnapPoints(
|
||||||
|
elementGroup,
|
||||||
|
elementsMap,
|
||||||
|
{},
|
||||||
|
getSnapSourceId(elementGroup),
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collects point snaps within snap distance. Unlike gap snaps, point snaps first
|
||||||
|
* go through visual-cluster filtering so a nearby reference can beat a slightly
|
||||||
|
* better offset from a far-away reference.
|
||||||
|
*/
|
||||||
const getPointSnaps = (
|
const getPointSnaps = (
|
||||||
selectedElements: ExcalidrawElement[],
|
selectedElements: ExcalidrawElement[],
|
||||||
selectionSnapPoints: GlobalPoint[],
|
selectionSnapPoints: SnapPoint[],
|
||||||
app: AppClassProperties,
|
app: AppClassProperties,
|
||||||
event: KeyboardModifiersObject,
|
event: KeyboardModifiersObject,
|
||||||
nearestSnapsX: Snaps,
|
nearestSnapsX: Snaps,
|
||||||
@@ -654,39 +885,62 @@ const getPointSnaps = (
|
|||||||
if (referenceSnapPoints) {
|
if (referenceSnapPoints) {
|
||||||
for (const thisSnapPoint of selectionSnapPoints) {
|
for (const thisSnapPoint of selectionSnapPoints) {
|
||||||
for (const otherSnapPoint of referenceSnapPoints) {
|
for (const otherSnapPoint of referenceSnapPoints) {
|
||||||
const offsetX = otherSnapPoint[0] - thisSnapPoint[0];
|
const offsetX = otherSnapPoint.point[0] - thisSnapPoint.point[0];
|
||||||
const offsetY = otherSnapPoint[1] - thisSnapPoint[1];
|
const offsetY = otherSnapPoint.point[1] - thisSnapPoint.point[1];
|
||||||
|
|
||||||
if (Math.abs(offsetX) <= minOffset.x) {
|
|
||||||
if (Math.abs(offsetX) < minOffset.x) {
|
|
||||||
nearestSnapsX.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (isWithinSnapOffset(offsetX, minOffset.x)) {
|
||||||
nearestSnapsX.push({
|
nearestSnapsX.push({
|
||||||
type: "point",
|
type: "point",
|
||||||
points: [thisSnapPoint, otherSnapPoint],
|
points: [thisSnapPoint.point, otherSnapPoint.point],
|
||||||
|
pointTypes: [thisSnapPoint.type, otherSnapPoint.type],
|
||||||
|
snapSourceIds: [
|
||||||
|
thisSnapPoint.snapSourceId,
|
||||||
|
otherSnapPoint.snapSourceId,
|
||||||
|
],
|
||||||
|
visualDistance: Math.abs(
|
||||||
|
otherSnapPoint.point[1] - thisSnapPoint.point[1],
|
||||||
|
),
|
||||||
offset: offsetX,
|
offset: offsetX,
|
||||||
});
|
});
|
||||||
|
|
||||||
minOffset.x = Math.abs(offsetX);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Math.abs(offsetY) <= minOffset.y) {
|
if (isWithinSnapOffset(offsetY, minOffset.y)) {
|
||||||
if (Math.abs(offsetY) < minOffset.y) {
|
|
||||||
nearestSnapsY.length = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
nearestSnapsY.push({
|
nearestSnapsY.push({
|
||||||
type: "point",
|
type: "point",
|
||||||
points: [thisSnapPoint, otherSnapPoint],
|
points: [thisSnapPoint.point, otherSnapPoint.point],
|
||||||
|
pointTypes: [thisSnapPoint.type, otherSnapPoint.type],
|
||||||
|
snapSourceIds: [
|
||||||
|
thisSnapPoint.snapSourceId,
|
||||||
|
otherSnapPoint.snapSourceId,
|
||||||
|
],
|
||||||
|
visualDistance: Math.abs(
|
||||||
|
otherSnapPoint.point[0] - thisSnapPoint.point[0],
|
||||||
|
),
|
||||||
offset: offsetY,
|
offset: offsetY,
|
||||||
});
|
});
|
||||||
|
|
||||||
minOffset.y = Math.abs(offsetY);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clusterBreakDistance = getSnapReferenceClusterBreakDistance(
|
||||||
|
app.state.zoom.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
filterPointSnapsToNearestCluster(nearestSnapsX, clusterBreakDistance);
|
||||||
|
filterPointSnapsToNearestCluster(nearestSnapsY, clusterBreakDistance);
|
||||||
|
|
||||||
|
if (nearestSnapsX.length > 0) {
|
||||||
|
minOffset.x = Math.min(
|
||||||
|
...nearestSnapsX.map((snap) => Math.abs(snap.offset)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nearestSnapsY.length > 0) {
|
||||||
|
minOffset.y = Math.min(
|
||||||
|
...nearestSnapsY.map((snap) => Math.abs(snap.offset)),
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const snapDraggedElements = (
|
export const snapDraggedElements = (
|
||||||
@@ -720,7 +974,7 @@ export const snapDraggedElements = (
|
|||||||
y: snapDistance,
|
y: snapDistance,
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectionPoints = getElementsCorners(selectedElements, elementsMap, {
|
const selectionPoints = getElementsSnapPoints(selectedElements, elementsMap, {
|
||||||
dragOffset,
|
dragOffset,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -770,7 +1024,7 @@ export const snapDraggedElements = (
|
|||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
getElementsCorners(selectedElements, elementsMap, {
|
getElementsSnapPoints(selectedElements, elementsMap, {
|
||||||
dragOffset: newDragOffset,
|
dragOffset: newDragOffset,
|
||||||
}),
|
}),
|
||||||
app,
|
app,
|
||||||
@@ -825,12 +1079,185 @@ const dedupePoints = (points: GlobalPoint[]): GlobalPoint[] => {
|
|||||||
return Array.from(map.values());
|
return Array.from(map.values());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Point snaplines are collected from every winning point snap first. Multiple
|
||||||
|
// snap source pairs can collapse to the same rendered line, so candidates keep
|
||||||
|
// source-pair metadata until redundant center and inner outer lines are removed.
|
||||||
|
type PointSnapLineSourcePair = {
|
||||||
|
snapSourcePairKey: string;
|
||||||
|
pointType: SnapPointType;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PointSnapLineCandidate = PointSnapLine & {
|
||||||
|
sourcePairs: PointSnapLineSourcePair[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type PointSnapLineBucket = {
|
||||||
|
points: GlobalPoint[];
|
||||||
|
sourcePairs: PointSnapLineSourcePair[];
|
||||||
|
};
|
||||||
|
|
||||||
|
type OuterSnapLineCoordinateRange = {
|
||||||
|
count: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Describes which snap source pair produced a rendered point snapline, so later
|
||||||
|
* filtering can reason about redundancy without mixing unrelated references.
|
||||||
|
* Only center-to-center snaps count as center snaplines; a center-to-outer snap
|
||||||
|
* still explains an outer edge or midpoint alignment.
|
||||||
|
*/
|
||||||
|
const getPointSnapLineSourcePair = (
|
||||||
|
snap: PointSnap,
|
||||||
|
): PointSnapLineSourcePair => {
|
||||||
|
return {
|
||||||
|
snapSourcePairKey: snap.snapSourceIds.join("->"),
|
||||||
|
pointType: snap.pointTypes.every((pointType) => pointType === "center")
|
||||||
|
? "center"
|
||||||
|
: "outer",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds each snap source pair's outer coordinate range. Center snaplines use
|
||||||
|
* this range to decide whether the surrounding outer lines already explain
|
||||||
|
* them, and outer snaplines use it to keep only the two extremes.
|
||||||
|
*/
|
||||||
|
const getOuterCoordinateRangesBySnapSourcePairKey = (
|
||||||
|
snapLines: PointSnapLineCandidate[],
|
||||||
|
getCoordinate: (snapLine: PointSnapLine) => number,
|
||||||
|
) => {
|
||||||
|
const ranges = new Map<string, OuterSnapLineCoordinateRange>();
|
||||||
|
|
||||||
|
for (const snapLine of snapLines) {
|
||||||
|
const coordinate = getCoordinate(snapLine);
|
||||||
|
|
||||||
|
for (const sourcePair of snapLine.sourcePairs) {
|
||||||
|
if (sourcePair.pointType === "center") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = ranges.get(sourcePair.snapSourcePairKey);
|
||||||
|
|
||||||
|
ranges.set(sourcePair.snapSourcePairKey, {
|
||||||
|
count: (range?.count ?? 0) + 1,
|
||||||
|
min: range ? Math.min(range.min, coordinate) : coordinate,
|
||||||
|
max: range ? Math.max(range.max, coordinate) : coordinate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ranges;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A center snapline is redundant when outer snaplines from the same snap source
|
||||||
|
* pair exist on both sides, because the outer lines already imply center
|
||||||
|
* alignment.
|
||||||
|
*/
|
||||||
|
const isRedundantCenterSnapLine = (
|
||||||
|
coordinate: number,
|
||||||
|
outerCoordinateRange?: OuterSnapLineCoordinateRange,
|
||||||
|
) => {
|
||||||
|
if (!outerCoordinateRange) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
outerCoordinateRange.min < coordinate &&
|
||||||
|
outerCoordinateRange.max > coordinate
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When more than two outer snaplines come from the same snap source pair, the
|
||||||
|
* inner ones are redundant; the two extremes communicate the same shape
|
||||||
|
* alignment.
|
||||||
|
*/
|
||||||
|
const isRedundantOuterSnapLine = (
|
||||||
|
coordinate: number,
|
||||||
|
outerCoordinateRange?: OuterSnapLineCoordinateRange,
|
||||||
|
) => {
|
||||||
|
if (!outerCoordinateRange || outerCoordinateRange.count <= 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
coordinate !== outerCoordinateRange.min &&
|
||||||
|
coordinate !== outerCoordinateRange.max
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Keeps a visual snapline if at least one snap source pair represented by the
|
||||||
|
* line still needs it. This matters when multiple snap source pairs collapse to
|
||||||
|
* the same rendered coordinate.
|
||||||
|
*/
|
||||||
|
const isPointSnapLineNeededBySourcePair = (
|
||||||
|
sourcePair: PointSnapLineSourcePair,
|
||||||
|
coordinate: number,
|
||||||
|
outerCoordinateRangesBySnapSourcePairKey: Map<
|
||||||
|
string,
|
||||||
|
OuterSnapLineCoordinateRange
|
||||||
|
>,
|
||||||
|
) => {
|
||||||
|
const outerCoordinateRange = outerCoordinateRangesBySnapSourcePairKey.get(
|
||||||
|
sourcePair.snapSourcePairKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sourcePair.pointType === "center") {
|
||||||
|
return !isRedundantCenterSnapLine(coordinate, outerCoordinateRange);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isRedundantOuterSnapLine(coordinate, outerCoordinateRange);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes point snaplines that are visually redundant, while preserving any line
|
||||||
|
* that is still needed by at least one snap source pair represented on that
|
||||||
|
* line.
|
||||||
|
*/
|
||||||
|
const filterRedundantPointSnapLines = (
|
||||||
|
snapLines: PointSnapLineCandidate[],
|
||||||
|
getCoordinate: (snapLine: PointSnapLine) => number,
|
||||||
|
): PointSnapLine[] => {
|
||||||
|
if (snapLines.length < 3) {
|
||||||
|
return snapLines.map(({ sourcePairs, ...snapLine }) => snapLine);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track outer-line ranges per snap source pair. The same visual snapline can
|
||||||
|
// be produced by multiple snap source pairs, so a line is removed only when
|
||||||
|
// every pair represented by that line considers it redundant.
|
||||||
|
const outerCoordinateRangesBySnapSourcePairKey =
|
||||||
|
getOuterCoordinateRangesBySnapSourcePairKey(snapLines, getCoordinate);
|
||||||
|
|
||||||
|
return snapLines
|
||||||
|
.filter((snapLine) => {
|
||||||
|
const coordinate = getCoordinate(snapLine);
|
||||||
|
|
||||||
|
return snapLine.sourcePairs.some((sourcePair) =>
|
||||||
|
isPointSnapLineNeededBySourcePair(
|
||||||
|
sourcePair,
|
||||||
|
coordinate,
|
||||||
|
outerCoordinateRangesBySnapSourcePairKey,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.map(({ sourcePairs, ...snapLine }) => snapLine);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merges point snaps that render on the same x/y coordinate, attaches their
|
||||||
|
* snap source pair metadata, then removes redundant center and inner outer
|
||||||
|
* lines.
|
||||||
|
*/
|
||||||
const createPointSnapLines = (
|
const createPointSnapLines = (
|
||||||
nearestSnapsX: Snaps,
|
nearestSnapsX: Snaps,
|
||||||
nearestSnapsY: Snaps,
|
nearestSnapsY: Snaps,
|
||||||
): PointSnapLine[] => {
|
): PointSnapLine[] => {
|
||||||
const snapsX = {} as { [key: string]: GlobalPoint[] };
|
const snapsX = {} as { [key: string]: PointSnapLineBucket };
|
||||||
const snapsY = {} as { [key: string]: GlobalPoint[] };
|
const snapsY = {} as { [key: string]: PointSnapLineBucket };
|
||||||
|
|
||||||
if (nearestSnapsX.length > 0) {
|
if (nearestSnapsX.length > 0) {
|
||||||
for (const snap of nearestSnapsX) {
|
for (const snap of nearestSnapsX) {
|
||||||
@@ -838,13 +1265,14 @@ const createPointSnapLines = (
|
|||||||
// key = thisPoint.x
|
// key = thisPoint.x
|
||||||
const key = round(snap.points[0][0]);
|
const key = round(snap.points[0][0]);
|
||||||
if (!snapsX[key]) {
|
if (!snapsX[key]) {
|
||||||
snapsX[key] = [];
|
snapsX[key] = { points: [], sourcePairs: [] };
|
||||||
}
|
}
|
||||||
snapsX[key].push(
|
snapsX[key].points.push(
|
||||||
...snap.points.map((p) =>
|
...snap.points.map((p) =>
|
||||||
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
snapsX[key].sourcePairs.push(getPointSnapLineSourcePair(snap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -855,44 +1283,55 @@ const createPointSnapLines = (
|
|||||||
// key = thisPoint.y
|
// key = thisPoint.y
|
||||||
const key = round(snap.points[0][1]);
|
const key = round(snap.points[0][1]);
|
||||||
if (!snapsY[key]) {
|
if (!snapsY[key]) {
|
||||||
snapsY[key] = [];
|
snapsY[key] = { points: [], sourcePairs: [] };
|
||||||
}
|
}
|
||||||
snapsY[key].push(
|
snapsY[key].points.push(
|
||||||
...snap.points.map((p) =>
|
...snap.points.map((p) =>
|
||||||
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
pointFrom<GlobalPoint>(round(p[0]), round(p[1])),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
snapsY[key].sourcePairs.push(getPointSnapLineSourcePair(snap));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.entries(snapsX)
|
const snapLinesX = Object.entries(snapsX).map(([key, snap]) => {
|
||||||
.map(([key, points]) => {
|
return {
|
||||||
return {
|
type: "points",
|
||||||
type: "points",
|
points: dedupePoints(
|
||||||
points: dedupePoints(
|
snap.points
|
||||||
points
|
.map((p) => {
|
||||||
.map((p) => {
|
return pointFrom<GlobalPoint>(Number(key), p[1]);
|
||||||
return pointFrom<GlobalPoint>(Number(key), p[1]);
|
})
|
||||||
})
|
.sort((a, b) => a[1] - b[1]),
|
||||||
.sort((a, b) => a[1] - b[1]),
|
),
|
||||||
),
|
sourcePairs: snap.sourcePairs,
|
||||||
} as PointSnapLine;
|
} as PointSnapLineCandidate;
|
||||||
})
|
});
|
||||||
.concat(
|
|
||||||
Object.entries(snapsY).map(([key, points]) => {
|
const snapLinesY = Object.entries(snapsY).map(([key, snap]) => {
|
||||||
return {
|
return {
|
||||||
type: "points",
|
type: "points",
|
||||||
points: dedupePoints(
|
points: dedupePoints(
|
||||||
points
|
snap.points
|
||||||
.map((p) => {
|
.map((p) => {
|
||||||
return pointFrom<GlobalPoint>(p[0], Number(key));
|
return pointFrom<GlobalPoint>(p[0], Number(key));
|
||||||
})
|
})
|
||||||
.sort((a, b) => a[0] - b[0]),
|
.sort((a, b) => a[0] - b[0]),
|
||||||
),
|
),
|
||||||
} as PointSnapLine;
|
sourcePairs: snap.sourcePairs,
|
||||||
}),
|
} as PointSnapLineCandidate;
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return filterRedundantPointSnapLines(
|
||||||
|
snapLinesX,
|
||||||
|
(snapLine) => snapLine.points[0][0],
|
||||||
|
).concat(
|
||||||
|
filterRedundantPointSnapLines(
|
||||||
|
snapLinesY,
|
||||||
|
(snapLine) => snapLine.points[0][1],
|
||||||
|
),
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const dedupeGapSnapLines = (gapSnapLines: GapSnapLine[]) => {
|
const dedupeGapSnapLines = (gapSnapLines: GapSnapLine[]) => {
|
||||||
@@ -917,10 +1356,13 @@ const createGapSnapLines = (
|
|||||||
dragOffset: Vector2D,
|
dragOffset: Vector2D,
|
||||||
gapSnaps: GapSnap[],
|
gapSnaps: GapSnap[],
|
||||||
): GapSnapLine[] => {
|
): GapSnapLine[] => {
|
||||||
|
// Use the same rounded bounds as getGapSnaps(). Otherwise a gap snap can be
|
||||||
|
// accepted during snapping but skipped here because the raw bounds miss the
|
||||||
|
// reference gap overlap by a tiny floating-point delta.
|
||||||
const [minX, minY, maxX, maxY] = getDraggedElementsBounds(
|
const [minX, minY, maxX, maxY] = getDraggedElementsBounds(
|
||||||
selectedElements,
|
selectedElements,
|
||||||
dragOffset,
|
dragOffset,
|
||||||
);
|
).map((bound) => round(bound));
|
||||||
|
|
||||||
const gapSnapLines: GapSnapLine[] = [];
|
const gapSnapLines: GapSnapLine[] = [];
|
||||||
|
|
||||||
@@ -1143,40 +1585,52 @@ export const snapResizingElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionSnapPoints: GlobalPoint[] = [];
|
const selectionSnapPoints: SnapPoint[] = [];
|
||||||
|
|
||||||
if (transformHandle) {
|
if (transformHandle) {
|
||||||
switch (transformHandle) {
|
switch (transformHandle) {
|
||||||
case "e": {
|
case "e": {
|
||||||
selectionSnapPoints.push(pointFrom(maxX, minY), pointFrom(maxX, maxY));
|
selectionSnapPoints.push(
|
||||||
|
outerSnapPoint(pointFrom(maxX, minY)),
|
||||||
|
outerSnapPoint(pointFrom(maxX, maxY)),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "w": {
|
case "w": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(minX, maxY));
|
selectionSnapPoints.push(
|
||||||
|
outerSnapPoint(pointFrom(minX, minY)),
|
||||||
|
outerSnapPoint(pointFrom(minX, maxY)),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "n": {
|
case "n": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, minY), pointFrom(maxX, minY));
|
selectionSnapPoints.push(
|
||||||
|
outerSnapPoint(pointFrom(minX, minY)),
|
||||||
|
outerSnapPoint(pointFrom(maxX, minY)),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "s": {
|
case "s": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, maxY), pointFrom(maxX, maxY));
|
selectionSnapPoints.push(
|
||||||
|
outerSnapPoint(pointFrom(minX, maxY)),
|
||||||
|
outerSnapPoint(pointFrom(maxX, maxY)),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "ne": {
|
case "ne": {
|
||||||
selectionSnapPoints.push(pointFrom(maxX, minY));
|
selectionSnapPoints.push(outerSnapPoint(pointFrom(maxX, minY)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "nw": {
|
case "nw": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, minY));
|
selectionSnapPoints.push(outerSnapPoint(pointFrom(minX, minY)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "se": {
|
case "se": {
|
||||||
selectionSnapPoints.push(pointFrom(maxX, maxY));
|
selectionSnapPoints.push(outerSnapPoint(pointFrom(maxX, maxY)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "sw": {
|
case "sw": {
|
||||||
selectionSnapPoints.push(pointFrom(minX, maxY));
|
selectionSnapPoints.push(outerSnapPoint(pointFrom(minX, maxY)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1218,11 +1672,11 @@ export const snapResizingElements = (
|
|||||||
round(bound),
|
round(bound),
|
||||||
);
|
);
|
||||||
|
|
||||||
const corners: GlobalPoint[] = [
|
const corners: SnapPoint[] = [
|
||||||
pointFrom(x1, y1),
|
outerSnapPoint(pointFrom(x1, y1)),
|
||||||
pointFrom(x1, y2),
|
outerSnapPoint(pointFrom(x1, y2)),
|
||||||
pointFrom(x2, y1),
|
outerSnapPoint(pointFrom(x2, y1)),
|
||||||
pointFrom(x2, y2),
|
outerSnapPoint(pointFrom(x2, y2)),
|
||||||
];
|
];
|
||||||
|
|
||||||
getPointSnaps(
|
getPointSnaps(
|
||||||
@@ -1258,8 +1712,8 @@ export const snapNewElement = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectionSnapPoints: GlobalPoint[] = [
|
const selectionSnapPoints: SnapPoint[] = [
|
||||||
pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y),
|
outerSnapPoint(pointFrom(origin.x + dragOffset.x, origin.y + dragOffset.y)),
|
||||||
];
|
];
|
||||||
|
|
||||||
const snapDistance = getSnapDistance(app.state.zoom.value);
|
const snapDistance = getSnapDistance(app.state.zoom.value);
|
||||||
@@ -1292,7 +1746,7 @@ export const snapNewElement = (
|
|||||||
nearestSnapsX.length = 0;
|
nearestSnapsX.length = 0;
|
||||||
nearestSnapsY.length = 0;
|
nearestSnapsY.length = 0;
|
||||||
|
|
||||||
const corners = getElementsCorners([newElement], elementsMap, {
|
const corners = getElementsSnapPoints([newElement], elementsMap, {
|
||||||
boundingBoxCorners: true,
|
boundingBoxCorners: true,
|
||||||
omitCenter: true,
|
omitCenter: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -961,7 +961,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -980,7 +979,6 @@ exports[`contextMenu element > right-clicking on a group should select whole gro
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -1159,7 +1157,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": true,
|
"scrolledOutside": true,
|
||||||
@@ -1175,7 +1172,6 @@ exports[`contextMenu element > selecting 'Add to library' in context menu adds e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -1375,7 +1371,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -1391,7 +1386,6 @@ exports[`contextMenu element > selecting 'Bring forward' in context menu brings
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -1708,7 +1702,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -1724,7 +1717,6 @@ exports[`contextMenu element > selecting 'Bring to front' in context menu brings
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2041,7 +2033,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": true,
|
"scrolledOutside": true,
|
||||||
@@ -2057,7 +2048,6 @@ exports[`contextMenu element > selecting 'Copy styles' in context menu copies st
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2257,7 +2247,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2271,7 +2260,6 @@ exports[`contextMenu element > selecting 'Delete' in context menu deletes elemen
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2500,7 +2488,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2516,7 +2503,6 @@ exports[`contextMenu element > selecting 'Duplicate' in context menu duplicates
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2802,7 +2788,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2821,7 +2806,6 @@ exports[`contextMenu element > selecting 'Group selection' in context menu group
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3174,7 +3158,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -3190,7 +3173,6 @@ exports[`contextMenu element > selecting 'Paste styles' in context menu pastes s
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3669,7 +3651,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -3685,7 +3666,6 @@ exports[`contextMenu element > selecting 'Send backward' in context menu sends e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3994,7 +3974,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -4010,7 +3989,6 @@ exports[`contextMenu element > selecting 'Send to back' in context menu sends el
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4321,7 +4299,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -4338,7 +4315,6 @@ exports[`contextMenu element > selecting 'Ungroup selection' in context menu ung
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5608,7 +5584,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -5625,7 +5600,6 @@ exports[`contextMenu element > shows 'Group selection' in context menu for multi
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -6827,7 +6801,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -6846,7 +6819,6 @@ exports[`contextMenu element > shows 'Ungroup selection' in context menu for gro
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7787,7 +7759,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -7801,7 +7772,6 @@ exports[`contextMenu element > shows context menu for canvas > [end of test] app
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8786,7 +8756,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": true,
|
"scrolledOutside": true,
|
||||||
@@ -8802,7 +8771,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9782,7 +9750,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -9798,7 +9765,6 @@ exports[`contextMenu element > shows context menu for element > [end of test] ap
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"id4": true,
|
"id4": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -720,7 +719,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"id4": true,
|
"id4": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -1284,7 +1282,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -1296,7 +1293,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -1646,7 +1642,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -1658,7 +1653,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2010,7 +2004,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -2022,7 +2015,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2272,7 +2264,6 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -2286,7 +2277,40 @@ exports[`history > multiplayer undo/redo > conflicts in arrows and their bindabl
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
"startBoundElement": {
|
||||||
|
"angle": 0,
|
||||||
|
"backgroundColor": "transparent",
|
||||||
|
"boundElements": [
|
||||||
|
{
|
||||||
|
"id": "id4",
|
||||||
|
"type": "arrow",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"customData": undefined,
|
||||||
|
"fillStyle": "solid",
|
||||||
|
"frameId": null,
|
||||||
|
"groupIds": [],
|
||||||
|
"height": 100,
|
||||||
|
"id": "id0",
|
||||||
|
"index": "a0",
|
||||||
|
"isDeleted": false,
|
||||||
|
"link": null,
|
||||||
|
"locked": false,
|
||||||
|
"opacity": 100,
|
||||||
|
"roughness": 1,
|
||||||
|
"roundness": null,
|
||||||
|
"seed": 1,
|
||||||
|
"strokeColor": "#1e1e1e",
|
||||||
|
"strokeStyle": "solid",
|
||||||
|
"strokeWidth": 2,
|
||||||
|
"type": "rectangle",
|
||||||
|
"updated": 1,
|
||||||
|
"version": 3,
|
||||||
|
"versionNonce": 493213705,
|
||||||
|
"width": 100,
|
||||||
|
"x": -100,
|
||||||
|
"y": -50,
|
||||||
|
},
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2730,7 +2754,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -2742,7 +2765,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3035,7 +3057,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -3047,7 +3068,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3356,7 +3376,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -3368,7 +3387,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3652,7 +3670,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -3664,7 +3681,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3940,7 +3956,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -3952,7 +3967,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4177,7 +4191,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -4189,7 +4202,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4436,7 +4448,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -4448,7 +4459,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4709,7 +4719,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -4721,7 +4730,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4940,7 +4948,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -4952,7 +4959,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5171,7 +5177,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -5183,7 +5188,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5420,7 +5424,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -5432,7 +5435,6 @@ exports[`history > multiplayer undo/redo > conflicts in bound text elements and
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5678,7 +5680,6 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -5690,7 +5691,6 @@ exports[`history > multiplayer undo/redo > conflicts in frames and their childre
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5937,7 +5937,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -5949,7 +5948,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -6268,7 +6266,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"id8": true,
|
"id8": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -6280,7 +6277,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -6697,7 +6693,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -6712,7 +6707,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7074,7 +7068,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -7094,7 +7087,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7385,7 +7377,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -7679,7 +7670,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -7691,7 +7681,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7911,7 +7900,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -7923,7 +7911,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8265,7 +8252,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -8277,7 +8263,6 @@ exports[`history > multiplayer undo/redo > should iterate through the history wh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8622,7 +8607,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -8637,7 +8621,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9027,7 +9010,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -9039,7 +9021,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9308,7 +9289,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -9322,7 +9302,6 @@ exports[`history > multiplayer undo/redo > should not let remote changes to inte
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9574,7 +9553,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -9588,7 +9566,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9841,7 +9818,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -9855,7 +9831,6 @@ exports[`history > multiplayer undo/redo > should not override remote changes on
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -10078,7 +10053,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -10090,7 +10064,6 @@ exports[`history > multiplayer undo/redo > should override remotely added groups
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -10374,7 +10347,6 @@ exports[`history > multiplayer undo/redo > should override remotely added points
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -10693,7 +10665,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -10705,7 +10676,6 @@ exports[`history > multiplayer undo/redo > should redistribute deltas when eleme
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -10934,7 +10904,6 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -10946,7 +10915,6 @@ exports[`history > multiplayer undo/redo > should redraw arrows on undo > [end o
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -11858,7 +11826,6 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -11872,7 +11839,6 @@ exports[`history > multiplayer undo/redo > should update history entries after r
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -12120,7 +12086,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -12132,7 +12097,6 @@ exports[`history > singleplayer undo/redo > remounting undo/redo buttons should
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -12357,7 +12321,6 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -12371,7 +12334,6 @@ exports[`history > singleplayer undo/redo > should clear the redo stack on eleme
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -12596,7 +12558,6 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -12608,7 +12569,6 @@ exports[`history > singleplayer undo/redo > should create entry when selecting f
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -12992,7 +12952,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -13006,7 +12965,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -13201,7 +13159,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -13215,7 +13172,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on e
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -13413,7 +13369,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -13428,7 +13383,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -13713,7 +13667,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -13728,7 +13681,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on i
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14016,7 +13968,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": -50,
|
"scrollX": -50,
|
||||||
"scrollY": -50,
|
"scrollY": -50,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -14028,7 +13979,6 @@ exports[`history > singleplayer undo/redo > should create new history entry on s
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14260,7 +14210,6 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -14274,7 +14223,6 @@ exports[`history > singleplayer undo/redo > should disable undo/redo buttons whe
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14499,7 +14447,6 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -14513,7 +14460,6 @@ exports[`history > singleplayer undo/redo > should end up with no history entry
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14740,7 +14686,6 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -14752,7 +14697,6 @@ exports[`history > singleplayer undo/redo > should iterate through the history w
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14987,7 +14931,6 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -15001,7 +14944,6 @@ exports[`history > singleplayer undo/redo > should not clear the redo stack on s
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -15323,7 +15265,6 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -15335,7 +15276,6 @@ exports[`history > singleplayer undo/redo > should not collapse when applying co
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -15492,7 +15432,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -15509,7 +15448,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -15781,7 +15719,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -15793,7 +15730,6 @@ exports[`history > singleplayer undo/redo > should not end up with history entry
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -16046,7 +15982,6 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -16058,7 +15993,6 @@ exports[`history > singleplayer undo/redo > should not modify anything on unrela
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -16200,7 +16134,6 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -16214,7 +16147,6 @@ exports[`history > singleplayer undo/redo > should not override appstate changes
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -16485,7 +16417,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -16497,7 +16428,6 @@ exports[`history > singleplayer undo/redo > should support appstate name or view
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -16648,7 +16578,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -17398,7 +17327,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -18046,7 +17974,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -18692,7 +18619,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -19445,7 +19371,6 @@ exports[`history > singleplayer undo/redo > should support bidirectional binding
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -20215,7 +20140,6 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -20230,7 +20154,6 @@ exports[`history > singleplayer undo/redo > should support changes in elements'
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -20697,7 +20620,6 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||||||
"id1": true,
|
"id1": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -20714,7 +20636,6 @@ exports[`history > singleplayer undo/redo > should support duplication of groups
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -21210,7 +21131,6 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
@@ -21222,7 +21142,6 @@ exports[`history > singleplayer undo/redo > should support element creation, del
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -21671,7 +21590,6 @@ exports[`history > singleplayer undo/redo > should support linear element creati
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"searchMatches": null,
|
"searchMatches": null,
|
||||||
|
|||||||
@@ -88,7 +88,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -107,7 +106,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -516,7 +514,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -537,7 +534,6 @@ exports[`given element A and group of elements B and given both are selected whe
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -930,7 +926,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -946,7 +941,6 @@ exports[`regression tests > Cmd/Ctrl-click exclusively select element under poin
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -1498,7 +1492,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -1514,7 +1507,6 @@ exports[`regression tests > Drags selected element when hitting only bounding bo
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -1709,7 +1701,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -1728,7 +1719,6 @@ exports[`regression tests > adjusts z order when grouping > [end of test] appSta
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2095,7 +2085,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2111,7 +2100,6 @@ exports[`regression tests > alt-drag duplicates an element > [end of test] appSt
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2340,7 +2328,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2356,7 +2343,6 @@ exports[`regression tests > arrow keys > [end of test] appState 1`] = `
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2524,7 +2510,6 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2540,7 +2525,6 @@ exports[`regression tests > can drag element that covers another element, while
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -2849,7 +2833,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -2865,7 +2848,6 @@ exports[`regression tests > change the properties of a shape > [end of test] app
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3108,7 +3090,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -3124,7 +3105,6 @@ exports[`regression tests > click on an element and drag it > [dragged] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3351,7 +3331,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -3367,7 +3346,6 @@ exports[`regression tests > click on an element and drag it > [end of test] appS
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3589,7 +3567,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -3605,7 +3582,6 @@ exports[`regression tests > click to select a shape > [end of test] appState 1`]
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -3849,7 +3825,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -3866,7 +3841,6 @@ exports[`regression tests > click-drag to select a group > [end of test] appStat
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4163,7 +4137,6 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -4181,7 +4154,6 @@ exports[`regression tests > deleting last but one element in editing group shoul
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4604,7 +4576,6 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -4646,7 +4617,6 @@ exports[`regression tests > deselects group of selected elements on pointer down
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -4889,7 +4859,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -4903,7 +4872,6 @@ exports[`regression tests > deselects group of selected elements on pointer up w
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5166,7 +5134,6 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -5208,7 +5175,6 @@ exports[`regression tests > deselects selected element on pointer down when poin
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5376,7 +5342,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -5390,7 +5355,6 @@ exports[`regression tests > deselects selected element, on pointer up, when clic
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5576,7 +5540,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -5592,7 +5555,6 @@ exports[`regression tests > double click to edit a group > [end of test] appStat
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -5974,7 +5936,6 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -5991,7 +5952,6 @@ exports[`regression tests > drags selected elements from point inside common bou
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -6270,7 +6230,6 @@ exports[`regression tests > draw every type of shape > [end of test] appState 1`
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -7063,7 +7022,6 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -7079,7 +7037,6 @@ exports[`regression tests > given a group of selected elements with an element t
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7398,7 +7355,6 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -7415,7 +7371,6 @@ exports[`regression tests > given a selected element A and a not selected elemen
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7679,7 +7634,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -7695,7 +7649,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -7916,7 +7869,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -7932,7 +7884,6 @@ exports[`regression tests > given selected element A with lower z-index than uns
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8156,7 +8107,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -8172,7 +8122,6 @@ exports[`regression tests > key 2 selects rectangle tool > [end of test] appStat
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8338,7 +8287,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -8354,7 +8302,6 @@ exports[`regression tests > key 3 selects diamond tool > [end of test] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8520,7 +8467,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -8536,7 +8482,6 @@ exports[`regression tests > key 4 selects ellipse tool > [end of test] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -8702,7 +8647,6 @@ exports[`regression tests > key 5 selects arrow tool > [end of test] appState 1`
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -8936,7 +8880,6 @@ exports[`regression tests > key 6 selects line tool > [end of test] appState 1`]
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -9168,7 +9111,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -9182,7 +9124,6 @@ exports[`regression tests > key 7 selects freedraw tool > [end of test] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9362,7 +9303,6 @@ exports[`regression tests > key a selects arrow tool > [end of test] appState 1`
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -9596,7 +9536,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -9612,7 +9551,6 @@ exports[`regression tests > key d selects diamond tool > [end of test] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -9778,7 +9716,6 @@ exports[`regression tests > key l selects line tool > [end of test] appState 1`]
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -10010,7 +9947,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -10026,7 +9962,6 @@ exports[`regression tests > key o selects ellipse tool > [end of test] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -10192,7 +10127,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -10206,7 +10140,6 @@ exports[`regression tests > key p selects freedraw tool > [end of test] appState
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -10386,7 +10319,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -10402,7 +10334,6 @@ exports[`regression tests > key r selects rectangle tool > [end of test] appStat
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -10572,7 +10503,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -10592,7 +10522,6 @@ exports[`regression tests > make a group and duplicate it > [end of test] appSta
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -11103,7 +11032,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -11119,7 +11047,6 @@ exports[`regression tests > noop interaction after undo shouldn't create history
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -11383,7 +11310,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": "-6.25000",
|
"scrollX": "-6.25000",
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -11397,7 +11323,6 @@ exports[`regression tests > pinch-to-zoom works > [end of test] appState 1`] = `
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -11510,7 +11435,6 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -11524,7 +11448,6 @@ exports[`regression tests > shift click on selected element should deselect it o
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -11713,7 +11636,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -11730,7 +11652,6 @@ exports[`regression tests > shift-click to multiselect, then drag > [end of test
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -12035,7 +11956,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -12053,7 +11973,6 @@ exports[`regression tests > should group elements and ungroup them > [end of tes
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -12467,7 +12386,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -12488,7 +12406,6 @@ exports[`regression tests > single-clicking on a subgroup of a selected group sh
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -13107,7 +13024,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 60,
|
"scrollX": 60,
|
||||||
"scrollY": 60,
|
"scrollY": 60,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -13121,7 +13037,6 @@ exports[`regression tests > spacebar + drag scrolls the canvas > [end of test] a
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -13234,7 +13149,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"id0": true,
|
"id0": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -13250,7 +13164,6 @@ exports[`regression tests > supports nested groups > [end of test] appState 1`]
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -13868,7 +13781,6 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"id6": true,
|
"id6": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -13912,7 +13824,6 @@ exports[`regression tests > switches from group of selected elements to another
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14208,7 +14119,6 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"id3": true,
|
"id3": true,
|
||||||
},
|
},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -14252,7 +14162,6 @@ exports[`regression tests > switches selected element on pointer down > [end of
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14472,7 +14381,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 20,
|
"scrollX": 20,
|
||||||
"scrollY": "-18.53553",
|
"scrollY": "-18.53553",
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -14486,7 +14394,6 @@ exports[`regression tests > two-finger scroll works > [end of test] appState 1`]
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -14597,7 +14504,6 @@ exports[`regression tests > undo/redo drawing an element > [end of test] appStat
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -14963,7 +14869,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -14977,7 +14882,6 @@ exports[`regression tests > updates fontSize & fontFamily appState > [end of tes
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
@@ -15091,7 +14995,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -15105,7 +15008,6 @@ exports[`regression tests > zoom hotkeys > [end of test] appState 1`] = `
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": true,
|
"showWelcomeScreen": true,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
|
|||||||
@@ -1,135 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { vi } from "vitest";
|
|
||||||
|
|
||||||
import { resolvablePromise } from "@excalidraw/common";
|
|
||||||
|
|
||||||
import { Excalidraw } from "../index";
|
|
||||||
|
|
||||||
import { API } from "./helpers/api";
|
|
||||||
import { act, render, waitFor } from "./test-utils";
|
|
||||||
|
|
||||||
import type { ExcalidrawImperativeAPI, ScrollConstraints } from "../types";
|
|
||||||
|
|
||||||
const FIRST_SCROLL_CONSTRAINTS: ScrollConstraints = {
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
width: 400,
|
|
||||||
height: 300,
|
|
||||||
lockZoom: true,
|
|
||||||
overscrollAllowance: 0,
|
|
||||||
viewportZoomFactor: 1,
|
|
||||||
animateOnNextUpdate: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
const SECOND_SCROLL_CONSTRAINTS: ScrollConstraints = {
|
|
||||||
x: 100,
|
|
||||||
y: 200,
|
|
||||||
width: 500,
|
|
||||||
height: 350,
|
|
||||||
lockZoom: false,
|
|
||||||
overscrollAllowance: 0.2,
|
|
||||||
viewportZoomFactor: 0.8,
|
|
||||||
animateOnNextUpdate: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("scrollConstraints", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("syncs prop updates after mount", async () => {
|
|
||||||
const { rerender } = await render(<Excalidraw />);
|
|
||||||
|
|
||||||
expect(window.h.state.scrollConstraints).toBe(null);
|
|
||||||
|
|
||||||
rerender(<Excalidraw scrollConstraints={FIRST_SCROLL_CONSTRAINTS} />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(window.h.state.scrollConstraints).toEqual(
|
|
||||||
FIRST_SCROLL_CONSTRAINTS,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
rerender(<Excalidraw scrollConstraints={SECOND_SCROLL_CONSTRAINTS} />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(window.h.state.scrollConstraints).toEqual(
|
|
||||||
SECOND_SCROLL_CONSTRAINTS,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
rerender(<Excalidraw />);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(window.h.state.scrollConstraints).toBe(null);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores setScrollConstraints() when the prop is controlled", async () => {
|
|
||||||
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
|
||||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
|
|
||||||
await render(
|
|
||||||
<Excalidraw
|
|
||||||
scrollConstraints={FIRST_SCROLL_CONSTRAINTS}
|
|
||||||
onExcalidrawAPI={(api) => {
|
|
||||||
if (api) {
|
|
||||||
excalidrawAPIPromise.resolve(api);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const excalidrawAPI = await excalidrawAPIPromise;
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
excalidrawAPI.setScrollConstraints(SECOND_SCROLL_CONSTRAINTS);
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(warn).toHaveBeenCalledWith(
|
|
||||||
"Excalidraw: `setScrollConstraints()` is ignored when the `scrollConstraints` prop is controlled. Update the prop value instead.",
|
|
||||||
);
|
|
||||||
expect(window.h.state.scrollConstraints).toEqual(FIRST_SCROLL_CONSTRAINTS);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("ignores scrollToContent() scrollLock when the prop is controlled", async () => {
|
|
||||||
const excalidrawAPIPromise = resolvablePromise<ExcalidrawImperativeAPI>();
|
|
||||||
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
||||||
|
|
||||||
await render(
|
|
||||||
<Excalidraw
|
|
||||||
scrollConstraints={FIRST_SCROLL_CONSTRAINTS}
|
|
||||||
onExcalidrawAPI={(api) => {
|
|
||||||
if (api) {
|
|
||||||
excalidrawAPIPromise.resolve(api);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
const excalidrawAPI = await excalidrawAPIPromise;
|
|
||||||
const rectangle = API.createElement({
|
|
||||||
x: 1000,
|
|
||||||
y: 1000,
|
|
||||||
width: 100,
|
|
||||||
height: 100,
|
|
||||||
});
|
|
||||||
API.setElements([rectangle]);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
excalidrawAPI.scrollToContent(rectangle, {
|
|
||||||
animate: false,
|
|
||||||
fitToViewport: true,
|
|
||||||
scrollLock: {
|
|
||||||
lockZoom: true,
|
|
||||||
overscrollAllowance: 0,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(warn).toHaveBeenCalledWith(
|
|
||||||
"Excalidraw: `scrollToContent()` with `scrollLock` is ignored when the `scrollConstraints` prop is controlled. Update the prop value instead.",
|
|
||||||
);
|
|
||||||
expect(window.h.state.scrollConstraints).toEqual(FIRST_SCROLL_CONSTRAINTS);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -615,6 +615,32 @@ describe("box-selection overlap mode", () => {
|
|||||||
|
|
||||||
assertSelectedElements([]);
|
assertSelectedElements([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should not select a framed element when selection only overlaps its clipped-out outline", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
x: 100,
|
||||||
|
y: 100,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const rect1 = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 200,
|
||||||
|
height: 200,
|
||||||
|
frameId: frame.id,
|
||||||
|
backgroundColor: "red",
|
||||||
|
fillStyle: "solid",
|
||||||
|
});
|
||||||
|
|
||||||
|
API.setElements([frame, rect1]);
|
||||||
|
|
||||||
|
boxSelect(40, 170, 70, 220);
|
||||||
|
|
||||||
|
assertSelectedElements([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("inner box-selection", () => {
|
describe("inner box-selection", () => {
|
||||||
|
|||||||
@@ -0,0 +1,833 @@
|
|||||||
|
import { arrayToMap } from "@excalidraw/common";
|
||||||
|
import {
|
||||||
|
pointFrom,
|
||||||
|
rangeInclusive,
|
||||||
|
type GlobalPoint,
|
||||||
|
type Radians,
|
||||||
|
} from "@excalidraw/math";
|
||||||
|
|
||||||
|
import type { ExcalidrawElement } from "@excalidraw/element/types";
|
||||||
|
|
||||||
|
import { getDefaultAppState } from "../appState";
|
||||||
|
import {
|
||||||
|
getElementsCorners,
|
||||||
|
getVisibleGaps,
|
||||||
|
getReferenceSnapPoints,
|
||||||
|
SnapCache,
|
||||||
|
snapDraggedElements,
|
||||||
|
} from "../snapping";
|
||||||
|
|
||||||
|
import { API } from "./helpers/api";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
NormalizedZoomValue,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
type ReferenceSnapPoints = NonNullable<
|
||||||
|
ReturnType<typeof SnapCache.getReferenceSnapPoints>
|
||||||
|
>;
|
||||||
|
|
||||||
|
const NO_MODIFIER_KEYS = {
|
||||||
|
altKey: false,
|
||||||
|
ctrlKey: false,
|
||||||
|
metaKey: false,
|
||||||
|
shiftKey: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSnappingApp = (appState: Partial<AppState> = {}) =>
|
||||||
|
({
|
||||||
|
props: {},
|
||||||
|
state: {
|
||||||
|
...getDefaultAppState(),
|
||||||
|
objectsSnapModeEnabled: true,
|
||||||
|
width: 1000,
|
||||||
|
height: 1000,
|
||||||
|
offsetLeft: 0,
|
||||||
|
offsetTop: 0,
|
||||||
|
...appState,
|
||||||
|
},
|
||||||
|
} as AppClassProperties);
|
||||||
|
|
||||||
|
const getHorizontalPointSnapLineCoordinates = (
|
||||||
|
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||||
|
) => {
|
||||||
|
return snapLines
|
||||||
|
.filter((snapLine) => snapLine.type === "points")
|
||||||
|
.filter((snapLine) => {
|
||||||
|
const [firstPoint, lastPoint] = snapLine.points;
|
||||||
|
|
||||||
|
return firstPoint[1] === lastPoint[1];
|
||||||
|
})
|
||||||
|
.map((snapLine) => {
|
||||||
|
return snapLine.points[0][1];
|
||||||
|
})
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVerticalPointSnapLineCoordinates = (
|
||||||
|
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||||
|
) => {
|
||||||
|
return snapLines
|
||||||
|
.filter((snapLine) => snapLine.type === "points")
|
||||||
|
.filter((snapLine) => {
|
||||||
|
const [firstPoint, lastPoint] = snapLine.points;
|
||||||
|
|
||||||
|
return firstPoint[0] === lastPoint[0];
|
||||||
|
})
|
||||||
|
.map((snapLine) => {
|
||||||
|
return snapLine.points[0][0];
|
||||||
|
})
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHorizontalPointSnapLineMaxX = (
|
||||||
|
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||||
|
) => {
|
||||||
|
const horizontalSnapLine = snapLines
|
||||||
|
.filter((snapLine) => snapLine.type === "points")
|
||||||
|
.find((snapLine) => {
|
||||||
|
const [firstPoint, lastPoint] = snapLine.points;
|
||||||
|
|
||||||
|
return firstPoint[1] === lastPoint[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!horizontalSnapLine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return horizontalSnapLine.points[horizontalSnapLine.points.length - 1][0];
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHorizontalPointSnapLineXRange = (
|
||||||
|
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||||
|
) => {
|
||||||
|
const horizontalSnapLine = snapLines
|
||||||
|
.filter((snapLine) => snapLine.type === "points")
|
||||||
|
.find((snapLine) => {
|
||||||
|
const [firstPoint, lastPoint] = snapLine.points;
|
||||||
|
|
||||||
|
return firstPoint[1] === lastPoint[1];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!horizontalSnapLine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
horizontalSnapLine.points[0][0],
|
||||||
|
horizontalSnapLine.points[horizontalSnapLine.points.length - 1][0],
|
||||||
|
] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHorizontalGapSnapLines = (
|
||||||
|
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||||
|
) => {
|
||||||
|
return snapLines.filter(
|
||||||
|
(snapLine) =>
|
||||||
|
snapLine.type === "gap" && snapLine.direction === "horizontal",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getVerticalGapSnapLines = (
|
||||||
|
snapLines: ReturnType<typeof snapDraggedElements>["snapLines"],
|
||||||
|
) => {
|
||||||
|
return snapLines.filter(
|
||||||
|
(snapLine) => snapLine.type === "gap" && snapLine.direction === "vertical",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPointKeys = (points: ReturnType<typeof getElementsCorners>) => {
|
||||||
|
return points.map((point) => point.join(","));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getReferenceSnapPointKeys = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
app: AppClassProperties,
|
||||||
|
) => {
|
||||||
|
return new Set(
|
||||||
|
getReferenceSnapPoints(
|
||||||
|
elements,
|
||||||
|
selectedElements,
|
||||||
|
app.state,
|
||||||
|
arrayToMap(elements),
|
||||||
|
).map((snapPoint) => snapPoint.point.join(",")),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const primeReferenceSnapPoints = (
|
||||||
|
elements: ExcalidrawElement[],
|
||||||
|
selectedElements: ExcalidrawElement[],
|
||||||
|
) => {
|
||||||
|
const selectedElementIds = new Set(
|
||||||
|
selectedElements.map((element) => element.id),
|
||||||
|
);
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints(
|
||||||
|
elements
|
||||||
|
.filter((element) => !selectedElementIds.has(element.id))
|
||||||
|
.flatMap((element) => {
|
||||||
|
const corners = getElementsCorners([element], elementsMap);
|
||||||
|
|
||||||
|
return corners.map((point, index) => ({
|
||||||
|
point,
|
||||||
|
type: index === corners.length - 1 ? "center" : "outer",
|
||||||
|
snapSourceId: element.id,
|
||||||
|
}));
|
||||||
|
}) as Parameters<typeof SnapCache.setReferenceSnapPoints>[0],
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("snapping", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
SnapCache.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not use frame children as references when snapping outside elements", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
id: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
const frameChild = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "frameChild",
|
||||||
|
x: 37,
|
||||||
|
y: 53,
|
||||||
|
width: 71,
|
||||||
|
height: 83,
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 400,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [frame, frameChild, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const referenceSnapPointKeys = getReferenceSnapPointKeys(
|
||||||
|
elements,
|
||||||
|
[selected],
|
||||||
|
app,
|
||||||
|
);
|
||||||
|
const frameChildPointKeys = getPointKeys(
|
||||||
|
getElementsCorners([frameChild], arrayToMap(elements)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
frameChildPointKeys.some((pointKey) =>
|
||||||
|
referenceSnapPointKeys.has(pointKey),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses frame siblings as references when snapping elements in the same frame", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
id: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
const sibling = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "sibling",
|
||||||
|
x: 37,
|
||||||
|
y: 53,
|
||||||
|
width: 71,
|
||||||
|
height: 83,
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 150,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const elements = [frame, sibling, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const referenceSnapPointKeys = getReferenceSnapPointKeys(
|
||||||
|
elements,
|
||||||
|
[selected],
|
||||||
|
app,
|
||||||
|
);
|
||||||
|
const siblingPointKeys = getPointKeys(
|
||||||
|
getElementsCorners([sibling], arrayToMap(elements)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
siblingPointKeys.some((pointKey) => referenceSnapPointKeys.has(pointKey)),
|
||||||
|
).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not use frame children as references when snapping the frame itself", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
id: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 300,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
const frameChild = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "frameChild",
|
||||||
|
x: 37,
|
||||||
|
y: 53,
|
||||||
|
width: 71,
|
||||||
|
height: 83,
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const elements = [frame, frameChild];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [frame.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const referenceSnapPointKeys = getReferenceSnapPointKeys(
|
||||||
|
elements,
|
||||||
|
[frame],
|
||||||
|
app,
|
||||||
|
);
|
||||||
|
const frameChildPointKeys = getPointKeys(
|
||||||
|
getElementsCorners([frameChild], arrayToMap(elements)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
frameChildPointKeys.some((pointKey) =>
|
||||||
|
referenceSnapPointKeys.has(pointKey),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not use frame children as visible gap references when snapping outside elements", () => {
|
||||||
|
const frame = API.createElement({
|
||||||
|
type: "frame",
|
||||||
|
id: "frame",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 500,
|
||||||
|
height: 300,
|
||||||
|
});
|
||||||
|
const frameChildA = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "frameChildA",
|
||||||
|
x: 50,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const frameChildB = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "frameChildB",
|
||||||
|
x: 250,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
frameId: frame.id,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 700,
|
||||||
|
y: 50,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [frame, frameChildA, frameChildB, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleGaps = getVisibleGaps(
|
||||||
|
elements,
|
||||||
|
[selected],
|
||||||
|
app.state,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(visibleGaps.horizontalGaps).toHaveLength(0);
|
||||||
|
expect(visibleGaps.verticalGaps).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters center and inner outer point snaplines for the same reference", () => {
|
||||||
|
const angle = 0.68 as Radians;
|
||||||
|
const reference = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "reference",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
angle,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 200,
|
||||||
|
y: 0,
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
angle,
|
||||||
|
});
|
||||||
|
const elements = [reference, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
primeReferenceSnapPoints(elements, [selected]);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getHorizontalPointSnapLineCoordinates(snapLines)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a snapline that is redundant for one reference but needed for another", () => {
|
||||||
|
const angle = 0.68 as Radians;
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 200,
|
||||||
|
y: 0,
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
angle,
|
||||||
|
});
|
||||||
|
const elements = [selected];
|
||||||
|
const elementsMap = arrayToMap(elements);
|
||||||
|
const selectedSnapPoints = getElementsCorners([selected], elementsMap);
|
||||||
|
const outerSnapPoints = selectedSnapPoints.slice(0, -1);
|
||||||
|
const centerSnapPoint = selectedSnapPoints[selectedSnapPoints.length - 1];
|
||||||
|
const innerOuterSnapPoint = [...outerSnapPoints].sort(
|
||||||
|
(a, b) => a[1] - b[1],
|
||||||
|
)[1];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const referenceSnapPoints: ReferenceSnapPoints = [
|
||||||
|
...outerSnapPoints.map((point) => ({
|
||||||
|
point: pointFrom<GlobalPoint>(point[0] - 200, point[1]),
|
||||||
|
type: "outer" as const,
|
||||||
|
snapSourceId: "referenceA",
|
||||||
|
})),
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(
|
||||||
|
centerSnapPoint[0] - 200,
|
||||||
|
centerSnapPoint[1],
|
||||||
|
),
|
||||||
|
type: "center" as const,
|
||||||
|
snapSourceId: "referenceA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(
|
||||||
|
innerOuterSnapPoint[0] - 300,
|
||||||
|
innerOuterSnapPoint[1],
|
||||||
|
),
|
||||||
|
type: "outer" as const,
|
||||||
|
snapSourceId: "referenceB",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints(referenceSnapPoints);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
elementsMap,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getHorizontalPointSnapLineCoordinates(snapLines)).toHaveLength(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a center snapline when no outer snaplines imply it", () => {
|
||||||
|
const reference = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "reference",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 200,
|
||||||
|
y: 25,
|
||||||
|
width: 50,
|
||||||
|
height: 50,
|
||||||
|
});
|
||||||
|
const elements = [reference, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
primeReferenceSnapPoints(elements, [selected]);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getHorizontalPointSnapLineCoordinates(snapLines)).toEqual([50]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("filters center snaplines when matching outer offsets differ by rounding precision", () => {
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 2532.227563984471,
|
||||||
|
y: -1553.9657067952232,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const reference = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "reference",
|
||||||
|
x: 2532.2275640966914,
|
||||||
|
y: -1299.4323092037737,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const elements = [reference, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
primeReferenceSnapPoints(elements, [selected]);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getVerticalPointSnapLineCoordinates(snapLines)).toEqual([
|
||||||
|
2532.227564, 2672.329126,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps outer snaplines stable while dragging a snapped element through rounding-equivalent offsets", () => {
|
||||||
|
const referenceMiddle = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "referenceMiddle",
|
||||||
|
x: 2532.22756398447,
|
||||||
|
y: -1553.9657067952237,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const referenceAbove = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "referenceAbove",
|
||||||
|
x: 2532.2275637826165,
|
||||||
|
y: -1779.7363232531268,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 2532.227563096691,
|
||||||
|
y: -1328.1950902037736,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const elements = [referenceAbove, referenceMiddle, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
primeReferenceSnapPoints(elements, [selected]);
|
||||||
|
|
||||||
|
for (const dragOffsetX of [-4, -1, -0.1, 0, 0.1, 1, 4]) {
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: dragOffsetX, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
const coordinates = getVerticalPointSnapLineCoordinates(snapLines);
|
||||||
|
|
||||||
|
expect(coordinates).toHaveLength(2);
|
||||||
|
expect(coordinates[1] - coordinates[0]).toBeCloseTo(selected.width, 5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps same-offset point snaps even across distant references", () => {
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints([
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(220, 50),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "near",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(900, 50),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "far",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(900);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers a nearby point snap over a slightly better far offset", () => {
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints([
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(220, 54),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "near",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(900, 50),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "far",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { snapOffset, snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(snapOffset.y).toBe(4);
|
||||||
|
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(220);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps same-offset point snaps when references form a continuous cluster", () => {
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints([
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(200, 50),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "referenceA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(350, 50),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "referenceB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(500, 50),
|
||||||
|
type: "center",
|
||||||
|
snapSourceId: "referenceC",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getHorizontalPointSnapLineMaxX(snapLines)).toBe(500);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps same-source same-offset point snaps across zoom-scaled cluster breaks", () => {
|
||||||
|
const reference = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "reference",
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 338.608871112217,
|
||||||
|
y: 0,
|
||||||
|
width: 140.1015625,
|
||||||
|
height: 140.1015625,
|
||||||
|
});
|
||||||
|
const elements = [reference, selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
zoom: { value: 1.5 as NormalizedZoomValue },
|
||||||
|
});
|
||||||
|
|
||||||
|
primeReferenceSnapPoints(elements, [selected]);
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
const range = getHorizontalPointSnapLineXRange(snapLines);
|
||||||
|
|
||||||
|
expect(range).not.toBe(null);
|
||||||
|
expect(range![0]).toBeCloseTo(reference.x, 6);
|
||||||
|
expect(range![1]).toBeCloseTo(selected.x + selected.width, 6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders gap snaplines when rounded bounds touch the reference gap overlap", () => {
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 0,
|
||||||
|
y: 0.0000004,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
SnapCache.setVisibleGaps({
|
||||||
|
horizontalGaps: [
|
||||||
|
{
|
||||||
|
startBounds: [200, -100, 300, 0],
|
||||||
|
endBounds: [400, -100, 500, 0],
|
||||||
|
startSide: [pointFrom(300, -100), pointFrom(300, 0)],
|
||||||
|
endSide: [pointFrom(400, -100), pointFrom(400, 0)],
|
||||||
|
overlap: rangeInclusive(-100, 0),
|
||||||
|
length: 100,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
verticalGaps: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getHorizontalGapSnapLines(snapLines)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders gap snaplines when the winning gap offset only differs by rounding precision", () => {
|
||||||
|
const selected = API.createElement({
|
||||||
|
type: "rectangle",
|
||||||
|
id: "selected",
|
||||||
|
x: 0,
|
||||||
|
y: 399.999999,
|
||||||
|
width: 100,
|
||||||
|
height: 100,
|
||||||
|
});
|
||||||
|
const elements = [selected];
|
||||||
|
const app = createSnappingApp({
|
||||||
|
selectedElementIds: { [selected.id]: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
SnapCache.setReferenceSnapPoints([
|
||||||
|
{
|
||||||
|
point: pointFrom<GlobalPoint>(0, 399.999999),
|
||||||
|
type: "outer",
|
||||||
|
snapSourceId: "reference",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
SnapCache.setVisibleGaps({
|
||||||
|
horizontalGaps: [],
|
||||||
|
verticalGaps: [
|
||||||
|
{
|
||||||
|
startBounds: [0, 100, 100, 200],
|
||||||
|
endBounds: [0, 250, 100, 350],
|
||||||
|
startSide: [pointFrom(0, 200), pointFrom(100, 200)],
|
||||||
|
endSide: [pointFrom(0, 250), pointFrom(100, 250)],
|
||||||
|
overlap: rangeInclusive(0, 100),
|
||||||
|
length: 50,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { snapLines } = snapDraggedElements(
|
||||||
|
elements,
|
||||||
|
{ x: 0, y: 0 },
|
||||||
|
app,
|
||||||
|
NO_MODIFIER_KEYS,
|
||||||
|
arrayToMap(elements),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getVerticalGapSnapLines(snapLines)).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -315,7 +315,10 @@ export interface AppState {
|
|||||||
bindingPreference: "enabled" | "disabled";
|
bindingPreference: "enabled" | "disabled";
|
||||||
/** user preference whether arrow snap to midpoints while binding */
|
/** user preference whether arrow snap to midpoints while binding */
|
||||||
isMidpointSnappingEnabled: boolean;
|
isMidpointSnappingEnabled: boolean;
|
||||||
startBoundElement: NonDeleted<ExcalidrawBindableElement> | null;
|
/**
|
||||||
|
* The bindable element the UI highlights for the user when an arrow is
|
||||||
|
* dragged or otherwise its endpoint being close to said element.
|
||||||
|
*/
|
||||||
suggestedBinding: {
|
suggestedBinding: {
|
||||||
element: NonDeleted<ExcalidrawBindableElement>;
|
element: NonDeleted<ExcalidrawBindableElement>;
|
||||||
midPoint?: GlobalPoint;
|
midPoint?: GlobalPoint;
|
||||||
@@ -347,8 +350,11 @@ export interface AppState {
|
|||||||
type: "selection" | "lasso";
|
type: "selection" | "lasso";
|
||||||
initialized: boolean;
|
initialized: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Pen handling
|
||||||
penMode: boolean;
|
penMode: boolean;
|
||||||
penDetected: boolean;
|
penDetected: boolean;
|
||||||
|
|
||||||
exportBackground: boolean;
|
exportBackground: boolean;
|
||||||
exportEmbedScene: boolean;
|
exportEmbedScene: boolean;
|
||||||
exportWithDarkMode: boolean;
|
exportWithDarkMode: boolean;
|
||||||
@@ -454,7 +460,6 @@ export interface AppState {
|
|||||||
userToFollow: UserToFollow | null;
|
userToFollow: UserToFollow | null;
|
||||||
/** the socket ids of the users following the current user */
|
/** the socket ids of the users following the current user */
|
||||||
followedBy: Set<SocketId>;
|
followedBy: Set<SocketId>;
|
||||||
scrollConstraints: ScrollConstraints | null;
|
|
||||||
|
|
||||||
/** image cropping */
|
/** image cropping */
|
||||||
isCropping: boolean;
|
isCropping: boolean;
|
||||||
@@ -473,6 +478,9 @@ export interface AppState {
|
|||||||
// as elements are unlocked, we remove the groupId from the elements
|
// as elements are unlocked, we remove the groupId from the elements
|
||||||
// and also remove groupId from this map
|
// and also remove groupId from this map
|
||||||
lockedMultiSelections: { [groupId: string]: true };
|
lockedMultiSelections: { [groupId: string]: true };
|
||||||
|
// Stores the current bind mode which is detemined at various points during
|
||||||
|
// a drag operation (like pointer position vs bindable element) but needed
|
||||||
|
// globally for calculating the binding strategy
|
||||||
bindMode: BindMode;
|
bindMode: BindMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,10 +496,7 @@ export type SearchMatch = {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UIAppState = Omit<
|
export type UIAppState = Omit<AppState, "cursorButton" | "scrollX" | "scrollY">;
|
||||||
AppState,
|
|
||||||
"startBoundElement" | "cursorButton" | "scrollX" | "scrollY"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
export type NormalizedZoomValue = number & { _brand: "normalizedZoom" };
|
||||||
|
|
||||||
@@ -547,8 +552,6 @@ export type ExcalidrawInitialDataState = Merge<
|
|||||||
ImportedDataState,
|
ImportedDataState,
|
||||||
{
|
{
|
||||||
libraryItems?: MaybePromise<Required<ImportedDataState>["libraryItems"]>;
|
libraryItems?: MaybePromise<Required<ImportedDataState>["libraryItems"]>;
|
||||||
scrollX?: number;
|
|
||||||
scrollY?: number;
|
|
||||||
}
|
}
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@@ -665,7 +668,6 @@ export interface ExcalidrawProps {
|
|||||||
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
onScrollChange?: (scrollX: number, scrollY: number, zoom: Zoom) => void;
|
||||||
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
onUserFollow?: (payload: OnUserFollowedPayload) => void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
scrollConstraints?: AppState["scrollConstraints"];
|
|
||||||
validateEmbeddable?:
|
validateEmbeddable?:
|
||||||
| boolean
|
| boolean
|
||||||
| string[]
|
| string[]
|
||||||
@@ -874,8 +876,13 @@ export type PointerDownState = Readonly<{
|
|||||||
// Whether selected element(s) were duplicated, might change during the
|
// Whether selected element(s) were duplicated, might change during the
|
||||||
// pointer interaction
|
// pointer interaction
|
||||||
hasBeenDuplicated: boolean;
|
hasBeenDuplicated: boolean;
|
||||||
|
// Whether the pointer is hitting the common bounding box of selected
|
||||||
|
// elements, which is useful for discriminating between selecitng
|
||||||
|
// the entire selection vs a specific element
|
||||||
hasHitCommonBoundingBoxOfSelectedElements: boolean;
|
hasHitCommonBoundingBoxOfSelectedElements: boolean;
|
||||||
};
|
};
|
||||||
|
// This is determined on the initial pointer down event to
|
||||||
|
// set various interaction modalities
|
||||||
withCmdOrCtrl: boolean;
|
withCmdOrCtrl: boolean;
|
||||||
drag: {
|
drag: {
|
||||||
// Might change during the pointer interaction
|
// Might change during the pointer interaction
|
||||||
@@ -901,6 +908,7 @@ export type PointerDownState = Readonly<{
|
|||||||
onKeyUp: null | ((event: KeyboardEvent) => void);
|
onKeyUp: null | ((event: KeyboardEvent) => void);
|
||||||
};
|
};
|
||||||
boxSelection: {
|
boxSelection: {
|
||||||
|
// If the box selection tool is activated on pointer down
|
||||||
hasOccurred: boolean;
|
hasOccurred: boolean;
|
||||||
};
|
};
|
||||||
}>;
|
}>;
|
||||||
@@ -992,8 +1000,6 @@ export interface ExcalidrawImperativeAPI {
|
|||||||
) => UnsubscribeCallback;
|
) => UnsubscribeCallback;
|
||||||
onStateChange: InstanceType<typeof App>["onStateChange"];
|
onStateChange: InstanceType<typeof App>["onStateChange"];
|
||||||
onEvent: InstanceType<typeof App>["onEvent"];
|
onEvent: InstanceType<typeof App>["onEvent"];
|
||||||
app: InstanceType<typeof App>;
|
|
||||||
setScrollConstraints: InstanceType<typeof App>["setScrollConstraints"];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FrameNameBounds = {
|
export type FrameNameBounds = {
|
||||||
@@ -1060,69 +1066,3 @@ export type Offsets = Partial<{
|
|||||||
bottom: number;
|
bottom: number;
|
||||||
left: number;
|
left: number;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
export type ScrollConstraints = {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
animateOnNextUpdate?: boolean;
|
|
||||||
/**
|
|
||||||
* A factor that determines the minimum zoom level that should fit the
|
|
||||||
* constrained area into the viewport.
|
|
||||||
*/
|
|
||||||
viewportZoomFactor?: number;
|
|
||||||
/**
|
|
||||||
* If true, the user will not be able to zoom out beyond the scroll
|
|
||||||
* constraints (taking into account the viewportZoomFactor).
|
|
||||||
*/
|
|
||||||
lockZoom?: boolean;
|
|
||||||
/**
|
|
||||||
* <0-1> - how much can you scroll beyond the constrained area within the
|
|
||||||
* timeout window. Note you will still be snapped back to the constrained area
|
|
||||||
* after the timeout.
|
|
||||||
*/
|
|
||||||
overscrollAllowance?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Optional scroll constraint settings derived from the final viewport computed
|
|
||||||
* by `scrollToContent()`.
|
|
||||||
*/
|
|
||||||
export type ScrollToContentLockOptions = {
|
|
||||||
lockZoom?: boolean;
|
|
||||||
overscrollAllowance?: number;
|
|
||||||
viewportZoomFactor?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScrollToContentOptions =
|
|
||||||
| ({
|
|
||||||
fitToContent?: boolean;
|
|
||||||
fitToViewport?: never;
|
|
||||||
viewportZoomFactor?: number;
|
|
||||||
animate?: boolean;
|
|
||||||
duration?: number;
|
|
||||||
scrollLock?: boolean | ScrollToContentLockOptions;
|
|
||||||
} & {
|
|
||||||
minZoom?: number;
|
|
||||||
maxZoom?: number;
|
|
||||||
canvasOffsets?: Offsets;
|
|
||||||
})
|
|
||||||
| ({
|
|
||||||
fitToContent?: never;
|
|
||||||
fitToViewport?: boolean;
|
|
||||||
viewportZoomFactor?: number;
|
|
||||||
animate?: boolean;
|
|
||||||
duration?: number;
|
|
||||||
scrollLock?: boolean | ScrollToContentLockOptions;
|
|
||||||
} & {
|
|
||||||
minZoom?: number;
|
|
||||||
maxZoom?: number;
|
|
||||||
canvasOffsets?: Offsets;
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AnimateTranslateCanvasValues = {
|
|
||||||
scrollX: AppState["scrollX"];
|
|
||||||
scrollY: AppState["scrollY"];
|
|
||||||
zoom: AppState["zoom"]["value"];
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
export const PRECISION = 10e-5;
|
|
||||||
|
|
||||||
// Legendre-Gauss abscissae (x values) and weights for n=24
|
// Legendre-Gauss abscissae (x values) and weights for n=24
|
||||||
// Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html
|
// Refeerence: https://pomax.github.io/bezierinfo/legendre-gauss.html
|
||||||
export const LegendreGaussN24TValues = [
|
export const LegendreGaussN24TValues = [
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
},
|
},
|
||||||
"previousSelectedElementIds": {},
|
"previousSelectedElementIds": {},
|
||||||
"resizingElement": null,
|
"resizingElement": null,
|
||||||
"scrollConstraints": null,
|
|
||||||
"scrollX": 0,
|
"scrollX": 0,
|
||||||
"scrollY": 0,
|
"scrollY": 0,
|
||||||
"scrolledOutside": false,
|
"scrolledOutside": false,
|
||||||
@@ -99,7 +98,6 @@ exports[`exportToSvg > with default arguments 1`] = `
|
|||||||
"showHyperlinkPopup": false,
|
"showHyperlinkPopup": false,
|
||||||
"showWelcomeScreen": false,
|
"showWelcomeScreen": false,
|
||||||
"snapLines": [],
|
"snapLines": [],
|
||||||
"startBoundElement": null,
|
|
||||||
"stats": {
|
"stats": {
|
||||||
"open": false,
|
"open": false,
|
||||||
"panels": 3,
|
"panels": 3,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"public": true,
|
||||||
"headers": [
|
"headers": [
|
||||||
{
|
{
|
||||||
"source": "/(.*)",
|
"source": "/(.*)",
|
||||||
|
|||||||
Reference in New Issue
Block a user