feat: partition main canvas vertically (#6759)
Co-authored-by: Marcel Mraz <marcel.mraz@adacta-fintech.com> Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
parent
3ea07076ad
commit
a376bd9495
@ -90,7 +90,7 @@
|
|||||||
"vite-plugin-ejs": "1.6.4",
|
"vite-plugin-ejs": "1.6.4",
|
||||||
"vite-plugin-pwa": "0.16.4",
|
"vite-plugin-pwa": "0.16.4",
|
||||||
"vite-plugin-svgr": "2.4.0",
|
"vite-plugin-svgr": "2.4.0",
|
||||||
"vitest": "0.32.2",
|
"vitest": "0.34.1",
|
||||||
"vitest-canvas-mock": "0.3.2"
|
"vitest-canvas-mock": "0.3.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
@ -423,7 +423,7 @@ export const actionToggleHandTool = register({
|
|||||||
type: "hand",
|
type: "hand",
|
||||||
lastActiveToolBeforeEraser: appState.activeTool,
|
lastActiveToolBeforeEraser: appState.activeTool,
|
||||||
});
|
});
|
||||||
setCursor(app.canvas, CURSOR_TYPE.GRAB);
|
setCursor(app.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -259,23 +259,25 @@ const duplicateElements = (
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
elements: finalElements,
|
elements: finalElements,
|
||||||
appState: selectGroupsForSelectedElements(
|
appState: {
|
||||||
{
|
...appState,
|
||||||
...appState,
|
...selectGroupsForSelectedElements(
|
||||||
selectedGroupIds: {},
|
{
|
||||||
selectedElementIds: nextElementsToSelect.reduce(
|
editingGroupId: appState.editingGroupId,
|
||||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
selectedElementIds: nextElementsToSelect.reduce(
|
||||||
if (!isBoundToContainer(element)) {
|
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||||
acc[element.id] = true;
|
if (!isBoundToContainer(element)) {
|
||||||
}
|
acc[element.id] = true;
|
||||||
return acc;
|
}
|
||||||
},
|
return acc;
|
||||||
{},
|
},
|
||||||
),
|
{},
|
||||||
},
|
),
|
||||||
getNonDeletedElements(finalElements),
|
},
|
||||||
appState,
|
getNonDeletedElements(finalElements),
|
||||||
null,
|
appState,
|
||||||
),
|
null,
|
||||||
|
),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,12 @@ import { AppState } from "../types";
|
|||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, _, { canvas, focusContainer, scene }) => {
|
perform: (
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
_,
|
||||||
|
{ interactiveCanvas, focusContainer, scene },
|
||||||
|
) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
@ -132,7 +137,7 @@ export const actionFinalize = register({
|
|||||||
appState.activeTool.type !== "freedraw") ||
|
appState.activeTool.type !== "freedraw") ||
|
||||||
!multiPointElement
|
!multiPointElement
|
||||||
) {
|
) {
|
||||||
resetCursor(canvas);
|
resetCursor(interactiveCanvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
@ -108,7 +108,7 @@ export const actionSetFrameAsActiveTool = register({
|
|||||||
type: "frame",
|
type: "frame",
|
||||||
});
|
});
|
||||||
|
|
||||||
setCursorForShape(app.canvas, {
|
setCursorForShape(app.interactiveCanvas, {
|
||||||
...appState,
|
...appState,
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
});
|
});
|
||||||
|
@ -149,11 +149,14 @@ export const actionGroup = register({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: selectGroup(
|
appState: {
|
||||||
newGroupId,
|
...appState,
|
||||||
{ ...appState, selectedGroupIds: {} },
|
...selectGroup(
|
||||||
getNonDeletedElements(nextElements),
|
newGroupId,
|
||||||
),
|
{ ...appState, selectedGroupIds: {} },
|
||||||
|
getNonDeletedElements(nextElements),
|
||||||
|
),
|
||||||
|
},
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
@ -212,7 +215,7 @@ export const actionUngroup = register({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const updateAppState = selectGroupsForSelectedElements(
|
const updateAppState = selectGroupsForSelectedElements(
|
||||||
{ ...appState, selectedGroupIds: {} },
|
appState,
|
||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
appState,
|
appState,
|
||||||
null,
|
null,
|
||||||
@ -243,7 +246,7 @@ export const actionUngroup = register({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: updateAppState,
|
appState: { ...appState, ...updateAppState },
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
|
@ -28,22 +28,24 @@ export const actionSelectAll = register({
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appState: selectGroupsForSelectedElements(
|
appState: {
|
||||||
{
|
...appState,
|
||||||
...appState,
|
...selectGroupsForSelectedElements(
|
||||||
selectedLinearElement:
|
{
|
||||||
// single linear element selected
|
editingGroupId: null,
|
||||||
Object.keys(selectedElementIds).length === 1 &&
|
selectedElementIds,
|
||||||
isLinearElement(elements[0])
|
},
|
||||||
? new LinearElementEditor(elements[0], app.scene)
|
getNonDeletedElements(elements),
|
||||||
: null,
|
appState,
|
||||||
editingGroupId: null,
|
app,
|
||||||
selectedElementIds,
|
),
|
||||||
},
|
selectedLinearElement:
|
||||||
getNonDeletedElements(elements),
|
// single linear element selected
|
||||||
appState,
|
Object.keys(selectedElementIds).length === 1 &&
|
||||||
app,
|
isLinearElement(elements[0])
|
||||||
),
|
? new LinearElementEditor(elements[0], app.scene)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -213,13 +213,13 @@ export const SelectedShapeActions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
canvas,
|
interactiveCanvas,
|
||||||
activeTool,
|
activeTool,
|
||||||
setAppState,
|
setAppState,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
appState,
|
appState,
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
activeTool: UIAppState["activeTool"];
|
activeTool: UIAppState["activeTool"];
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
@ -270,7 +270,7 @@ export const ShapesSwitcher = ({
|
|||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
});
|
});
|
||||||
setCursorForShape(canvas, {
|
setCursorForShape(interactiveCanvas, {
|
||||||
...appState,
|
...appState,
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
});
|
});
|
||||||
|
@ -6,14 +6,14 @@ import { render, queryByTestId } from "../tests/test-utils";
|
|||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
describe("Test <App/>", () => {
|
describe("Test <App/>", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -8,9 +8,9 @@ import { mutateElement } from "../element/mutateElement";
|
|||||||
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
|
import { useCreatePortalContainer } from "../hooks/useCreatePortalContainer";
|
||||||
import { useOutsideClick } from "../hooks/useOutsideClick";
|
import { useOutsideClick } from "../hooks/useOutsideClick";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
||||||
import { getSelectedElements } from "../scene";
|
import { getSelectedElements } from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
|
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
|
||||||
|
|
||||||
import "./EyeDropper.scss";
|
import "./EyeDropper.scss";
|
||||||
@ -98,7 +98,7 @@ export const EyeDropper: React.FC<{
|
|||||||
},
|
},
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
invalidateShapeForElement(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
Scene.getScene(
|
Scene.getScene(
|
||||||
metaStuffRef.current.selectedElements[0],
|
metaStuffRef.current.selectedElements[0],
|
||||||
|
@ -34,7 +34,7 @@ const JSONExportModal = ({
|
|||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
onCloseRequest: () => void;
|
onCloseRequest: () => void;
|
||||||
exportOpts: ExportOpts;
|
exportOpts: ExportOpts;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement;
|
||||||
}) => {
|
}) => {
|
||||||
const { onExportToBackend } = exportOpts;
|
const { onExportToBackend } = exportOpts;
|
||||||
return (
|
return (
|
||||||
@ -100,7 +100,7 @@ export const JSONExportDialog = ({
|
|||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
exportOpts: ExportOpts;
|
exportOpts: ExportOpts;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement;
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
}) => {
|
}) => {
|
||||||
const handleClose = React.useCallback(() => {
|
const handleClose = React.useCallback(() => {
|
||||||
|
@ -57,7 +57,8 @@ interface LayerUIProps {
|
|||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement;
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
@ -117,6 +118,7 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
|
interactiveCanvas,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
@ -272,7 +274,7 @@ const LayerUI = ({
|
|||||||
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onImageAction={({ pointerType }) => {
|
onImageAction={({ pointerType }) => {
|
||||||
@ -413,7 +415,7 @@ const LayerUI = ({
|
|||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
onHandToolToggle={onHandToolToggle}
|
onHandToolToggle={onHandToolToggle}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
canvas={canvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
onImageAction={onImageAction}
|
onImageAction={onImageAction}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
@ -464,7 +466,7 @@ const LayerUI = ({
|
|||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState((appState) => ({
|
setAppState((appState) => ({
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
...calculateScrollCenter(elements, appState),
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -507,8 +509,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
|
const {
|
||||||
const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
|
canvas: _pC,
|
||||||
|
interactiveCanvas: _pIC,
|
||||||
|
appState: prevAppState,
|
||||||
|
...prev
|
||||||
|
} = prevProps;
|
||||||
|
const {
|
||||||
|
canvas: _nC,
|
||||||
|
interactiveCanvas: _nIC,
|
||||||
|
appState: nextAppState,
|
||||||
|
...next
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isShallowEqual(
|
isShallowEqual(
|
||||||
|
@ -36,7 +36,7 @@ type MobileMenuProps = {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
canvas: HTMLCanvasElement | null;
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
|
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
@ -58,7 +58,7 @@ export const MobileMenu = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
canvas,
|
interactiveCanvas,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
@ -85,7 +85,7 @@ export const MobileMenu = ({
|
|||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onImageAction={({ pointerType }) => {
|
onImageAction={({ pointerType }) => {
|
||||||
@ -202,7 +202,7 @@ export const MobileMenu = ({
|
|||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState((appState) => ({
|
setAppState((appState) => ({
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
...calculateScrollCenter(elements, appState),
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
222
src/components/canvases/InteractiveCanvas.tsx
Normal file
222
src/components/canvases/InteractiveCanvas.tsx
Normal file
@ -0,0 +1,222 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { renderInteractiveScene } from "../../renderer/renderScene";
|
||||||
|
import {
|
||||||
|
isRenderThrottlingEnabled,
|
||||||
|
isShallowEqual,
|
||||||
|
sceneCoordsToViewportCoords,
|
||||||
|
} from "../../utils";
|
||||||
|
import { CURSOR_TYPE } from "../../constants";
|
||||||
|
import { t } from "../../i18n";
|
||||||
|
import type { DOMAttributes } from "react";
|
||||||
|
import type { AppState, InteractiveCanvasAppState } from "../../types";
|
||||||
|
import type {
|
||||||
|
InteractiveCanvasRenderConfig,
|
||||||
|
RenderInteractiveSceneCallback,
|
||||||
|
} from "../../scene/types";
|
||||||
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
|
||||||
|
type InteractiveCanvasProps = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
versionNonce: number | undefined;
|
||||||
|
selectionNonce: number | undefined;
|
||||||
|
scale: number;
|
||||||
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderInteractiveSceneCallback: (
|
||||||
|
data: RenderInteractiveSceneCallback,
|
||||||
|
) => void;
|
||||||
|
handleCanvasRef: (canvas: HTMLCanvasElement | null) => void;
|
||||||
|
onContextMenu: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement | HTMLDivElement>["onContextMenu"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
onPointerMove: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement>["onPointerMove"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
onPointerUp: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement>["onPointerUp"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
onPointerCancel: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement>["onPointerCancel"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
onTouchMove: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement>["onTouchMove"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
onPointerDown: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement>["onPointerDown"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
onDoubleClick: Exclude<
|
||||||
|
DOMAttributes<HTMLCanvasElement>["onDoubleClick"],
|
||||||
|
undefined
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
||||||
|
const isComponentMounted = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isComponentMounted.current) {
|
||||||
|
isComponentMounted.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cursorButton: {
|
||||||
|
[id: string]: string | undefined;
|
||||||
|
} = {};
|
||||||
|
const pointerViewportCoords: InteractiveCanvasRenderConfig["remotePointerViewportCoords"] =
|
||||||
|
{};
|
||||||
|
const remoteSelectedElementIds: InteractiveCanvasRenderConfig["remoteSelectedElementIds"] =
|
||||||
|
{};
|
||||||
|
const pointerUsernames: { [id: string]: string } = {};
|
||||||
|
const pointerUserStates: { [id: string]: string } = {};
|
||||||
|
|
||||||
|
props.appState.collaborators.forEach((user, socketId) => {
|
||||||
|
if (user.selectedElementIds) {
|
||||||
|
for (const id of Object.keys(user.selectedElementIds)) {
|
||||||
|
if (!(id in remoteSelectedElementIds)) {
|
||||||
|
remoteSelectedElementIds[id] = [];
|
||||||
|
}
|
||||||
|
remoteSelectedElementIds[id].push(socketId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!user.pointer) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (user.username) {
|
||||||
|
pointerUsernames[socketId] = user.username;
|
||||||
|
}
|
||||||
|
if (user.userState) {
|
||||||
|
pointerUserStates[socketId] = user.userState;
|
||||||
|
}
|
||||||
|
pointerViewportCoords[socketId] = sceneCoordsToViewportCoords(
|
||||||
|
{
|
||||||
|
sceneX: user.pointer.x,
|
||||||
|
sceneY: user.pointer.y,
|
||||||
|
},
|
||||||
|
props.appState,
|
||||||
|
);
|
||||||
|
cursorButton[socketId] = user.button;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectionColor = getComputedStyle(
|
||||||
|
document.querySelector(".excalidraw")!,
|
||||||
|
).getPropertyValue("--color-selection");
|
||||||
|
|
||||||
|
renderInteractiveScene(
|
||||||
|
{
|
||||||
|
canvas: props.canvas,
|
||||||
|
elements: props.elements,
|
||||||
|
visibleElements: props.visibleElements,
|
||||||
|
selectedElements: props.selectedElements,
|
||||||
|
scale: window.devicePixelRatio,
|
||||||
|
appState: props.appState,
|
||||||
|
renderConfig: {
|
||||||
|
remotePointerViewportCoords: pointerViewportCoords,
|
||||||
|
remotePointerButton: cursorButton,
|
||||||
|
remoteSelectedElementIds,
|
||||||
|
remotePointerUsernames: pointerUsernames,
|
||||||
|
remotePointerUserStates: pointerUserStates,
|
||||||
|
selectionColor,
|
||||||
|
renderScrollbars: false,
|
||||||
|
},
|
||||||
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
},
|
||||||
|
isRenderThrottlingEnabled(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className="excalidraw__canvas interactive"
|
||||||
|
style={{
|
||||||
|
width: props.appState.width,
|
||||||
|
height: props.appState.height,
|
||||||
|
cursor: props.appState.viewModeEnabled
|
||||||
|
? CURSOR_TYPE.GRAB
|
||||||
|
: CURSOR_TYPE.AUTO,
|
||||||
|
}}
|
||||||
|
width={props.appState.width * props.scale}
|
||||||
|
height={props.appState.height * props.scale}
|
||||||
|
ref={props.handleCanvasRef}
|
||||||
|
onContextMenu={props.onContextMenu}
|
||||||
|
onPointerMove={props.onPointerMove}
|
||||||
|
onPointerUp={props.onPointerUp}
|
||||||
|
onPointerCancel={props.onPointerCancel}
|
||||||
|
onTouchMove={props.onTouchMove}
|
||||||
|
onPointerDown={props.onPointerDown}
|
||||||
|
onDoubleClick={
|
||||||
|
props.appState.viewModeEnabled ? undefined : props.onDoubleClick
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{t("labels.drawingCanvas")}
|
||||||
|
</canvas>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelevantAppStateProps = (
|
||||||
|
appState: AppState,
|
||||||
|
): Omit<InteractiveCanvasAppState, "editingElement"> => ({
|
||||||
|
zoom: appState.zoom,
|
||||||
|
scrollX: appState.scrollX,
|
||||||
|
scrollY: appState.scrollY,
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
viewModeEnabled: appState.viewModeEnabled,
|
||||||
|
editingGroupId: appState.editingGroupId,
|
||||||
|
editingLinearElement: appState.editingLinearElement,
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
frameToHighlight: appState.frameToHighlight,
|
||||||
|
offsetLeft: appState.offsetLeft,
|
||||||
|
offsetTop: appState.offsetTop,
|
||||||
|
theme: appState.theme,
|
||||||
|
pendingImageElementId: appState.pendingImageElementId,
|
||||||
|
selectionElement: appState.selectionElement,
|
||||||
|
selectedGroupIds: appState.selectedGroupIds,
|
||||||
|
selectedLinearElement: appState.selectedLinearElement,
|
||||||
|
multiElement: appState.multiElement,
|
||||||
|
isBindingEnabled: appState.isBindingEnabled,
|
||||||
|
suggestedBindings: appState.suggestedBindings,
|
||||||
|
isRotating: appState.isRotating,
|
||||||
|
elementsToHighlight: appState.elementsToHighlight,
|
||||||
|
openSidebar: appState.openSidebar,
|
||||||
|
showHyperlinkPopup: appState.showHyperlinkPopup,
|
||||||
|
collaborators: appState.collaborators, // Necessary for collab. sessions
|
||||||
|
activeEmbeddable: appState.activeEmbeddable,
|
||||||
|
});
|
||||||
|
|
||||||
|
const areEqual = (
|
||||||
|
prevProps: InteractiveCanvasProps,
|
||||||
|
nextProps: InteractiveCanvasProps,
|
||||||
|
) => {
|
||||||
|
// This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation
|
||||||
|
if (
|
||||||
|
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||||
|
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||||
|
prevProps.scale !== nextProps.scale ||
|
||||||
|
// we need to memoize on element arrays because they may have renewed
|
||||||
|
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||||
|
// on appState)
|
||||||
|
prevProps.elements !== nextProps.elements ||
|
||||||
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
||||||
|
prevProps.selectedElements !== nextProps.selectedElements
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comparing the interactive appState for changes in case of some edge cases
|
||||||
|
return isShallowEqual(
|
||||||
|
// asserting AppState because we're being passed the whole AppState
|
||||||
|
// but resolve to only the InteractiveCanvas-relevant props
|
||||||
|
getRelevantAppStateProps(prevProps.appState as AppState),
|
||||||
|
getRelevantAppStateProps(nextProps.appState as AppState),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(InteractiveCanvas, areEqual);
|
113
src/components/canvases/StaticCanvas.tsx
Normal file
113
src/components/canvases/StaticCanvas.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
|
import { renderStaticScene } from "../../renderer/renderScene";
|
||||||
|
import { isRenderThrottlingEnabled, isShallowEqual } from "../../utils";
|
||||||
|
import type { AppState, StaticCanvasAppState } from "../../types";
|
||||||
|
import type { StaticCanvasRenderConfig } from "../../scene/types";
|
||||||
|
import type { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
|
||||||
|
type StaticCanvasProps = {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
rc: RoughCanvas;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
versionNonce: number | undefined;
|
||||||
|
selectionNonce: number | undefined;
|
||||||
|
scale: number;
|
||||||
|
appState: StaticCanvasAppState;
|
||||||
|
renderConfig: StaticCanvasRenderConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaticCanvas = (props: StaticCanvasProps) => {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const isComponentMounted = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const wrapper = wrapperRef.current;
|
||||||
|
if (!wrapper) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = props.canvas;
|
||||||
|
|
||||||
|
if (!isComponentMounted.current) {
|
||||||
|
isComponentMounted.current = true;
|
||||||
|
|
||||||
|
wrapper.replaceChildren(canvas);
|
||||||
|
canvas.classList.add("excalidraw__canvas", "static");
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.style.width = `${props.appState.width}px`;
|
||||||
|
canvas.style.height = `${props.appState.height}px`;
|
||||||
|
canvas.width = props.appState.width * props.scale;
|
||||||
|
canvas.height = props.appState.height * props.scale;
|
||||||
|
|
||||||
|
renderStaticScene(
|
||||||
|
{
|
||||||
|
canvas,
|
||||||
|
rc: props.rc,
|
||||||
|
scale: props.scale,
|
||||||
|
elements: props.elements,
|
||||||
|
visibleElements: props.visibleElements,
|
||||||
|
appState: props.appState,
|
||||||
|
renderConfig: props.renderConfig,
|
||||||
|
},
|
||||||
|
isRenderThrottlingEnabled(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="excalidraw__canvas-wrapper" ref={wrapperRef} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getRelevantAppStateProps = (
|
||||||
|
appState: AppState,
|
||||||
|
): Omit<
|
||||||
|
StaticCanvasAppState,
|
||||||
|
| "editingElement"
|
||||||
|
| "selectedElementIds"
|
||||||
|
| "editingGroupId"
|
||||||
|
| "frameToHighlight"
|
||||||
|
> => ({
|
||||||
|
zoom: appState.zoom,
|
||||||
|
scrollX: appState.scrollX,
|
||||||
|
scrollY: appState.scrollY,
|
||||||
|
width: appState.width,
|
||||||
|
height: appState.height,
|
||||||
|
viewModeEnabled: appState.viewModeEnabled,
|
||||||
|
offsetLeft: appState.offsetLeft,
|
||||||
|
offsetTop: appState.offsetTop,
|
||||||
|
theme: appState.theme,
|
||||||
|
pendingImageElementId: appState.pendingImageElementId,
|
||||||
|
shouldCacheIgnoreZoom: appState.shouldCacheIgnoreZoom,
|
||||||
|
viewBackgroundColor: appState.viewBackgroundColor,
|
||||||
|
exportScale: appState.exportScale,
|
||||||
|
selectedElementsAreBeingDragged: appState.selectedElementsAreBeingDragged,
|
||||||
|
gridSize: appState.gridSize,
|
||||||
|
frameRendering: appState.frameRendering,
|
||||||
|
});
|
||||||
|
|
||||||
|
const areEqual = (
|
||||||
|
prevProps: StaticCanvasProps,
|
||||||
|
nextProps: StaticCanvasProps,
|
||||||
|
) => {
|
||||||
|
if (
|
||||||
|
prevProps.versionNonce !== nextProps.versionNonce ||
|
||||||
|
prevProps.scale !== nextProps.scale ||
|
||||||
|
// we need to memoize on element arrays because they may have renewed
|
||||||
|
// even if versionNonce didn't change (e.g. we filter elements out based
|
||||||
|
// on appState)
|
||||||
|
prevProps.elements !== nextProps.elements ||
|
||||||
|
prevProps.visibleElements !== nextProps.visibleElements
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isShallowEqual(
|
||||||
|
// asserting AppState because we're being passed the whole AppState
|
||||||
|
// but resolve to only the StaticCanvas-relevant props
|
||||||
|
getRelevantAppStateProps(prevProps.appState as AppState),
|
||||||
|
getRelevantAppStateProps(nextProps.appState as AppState),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(StaticCanvas, areEqual);
|
4
src/components/canvases/index.tsx
Normal file
4
src/components/canvases/index.tsx
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import InteractiveCanvas from "./InteractiveCanvas";
|
||||||
|
import StaticCanvas from "./StaticCanvas";
|
||||||
|
|
||||||
|
export { InteractiveCanvas, StaticCanvas };
|
@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--zIndex-canvas: 1;
|
--zIndex-canvas: 1;
|
||||||
--zIndex-wysiwyg: 2;
|
--zIndex-interactiveCanvas: 2;
|
||||||
--zIndex-layerUI: 3;
|
--zIndex-wysiwyg: 3;
|
||||||
|
--zIndex-layerUI: 4;
|
||||||
|
|
||||||
--zIndex-modal: 1000;
|
--zIndex-modal: 1000;
|
||||||
--zIndex-popup: 1001;
|
--zIndex-popup: 1001;
|
||||||
@ -69,10 +70,19 @@
|
|||||||
|
|
||||||
z-index: var(--zIndex-canvas);
|
z-index: var(--zIndex-canvas);
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
z-index: var(--zIndex-interactiveCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the main canvas from document flow to avoid resizeObserver
|
// Remove the main canvas from document flow to avoid resizeObserver
|
||||||
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__canvas-wrapper,
|
||||||
|
&__canvas.static {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
&__canvas {
|
&__canvas {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
@ -144,11 +144,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
fileHandle: fileHandle || blob.handle || null,
|
fileHandle: fileHandle || blob.handle || null,
|
||||||
...cleanAppStateForExport(data.appState || {}),
|
...cleanAppStateForExport(data.appState || {}),
|
||||||
...(localAppState
|
...(localAppState
|
||||||
? calculateScrollCenter(
|
? calculateScrollCenter(data.elements || [], localAppState)
|
||||||
data.elements || [],
|
|
||||||
localAppState,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
files: data.files,
|
files: data.files,
|
||||||
|
@ -25,10 +25,7 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { KEYS } from "../keys";
|
import { KEYS } from "../keys";
|
||||||
import {
|
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement";
|
||||||
DEFAULT_LINK_SIZE,
|
|
||||||
invalidateShapeForElement,
|
|
||||||
} from "../renderer/renderElement";
|
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
||||||
import { Bounds } from "./bounds";
|
import { Bounds } from "./bounds";
|
||||||
@ -42,6 +39,7 @@ import "./Hyperlink.scss";
|
|||||||
import { trackEvent } from "../analytics";
|
import { trackEvent } from "../analytics";
|
||||||
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
||||||
import { isEmbeddableElement } from "./typeChecks";
|
import { isEmbeddableElement } from "./typeChecks";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
|
||||||
const CONTAINER_WIDTH = 320;
|
const CONTAINER_WIDTH = 320;
|
||||||
const SPACE_BOTTOM = 85;
|
const SPACE_BOTTOM = 85;
|
||||||
@ -115,7 +113,7 @@ export const Hyperlink = ({
|
|||||||
validated: false,
|
validated: false,
|
||||||
link,
|
link,
|
||||||
});
|
});
|
||||||
invalidateShapeForElement(element);
|
ShapeCache.delete(element);
|
||||||
} else {
|
} else {
|
||||||
const { width, height } = element;
|
const { width, height } = element;
|
||||||
const embedLink = getEmbedLink(link);
|
const embedLink = getEmbedLink(link);
|
||||||
@ -147,7 +145,7 @@ export const Hyperlink = ({
|
|||||||
validated: true,
|
validated: true,
|
||||||
link,
|
link,
|
||||||
});
|
});
|
||||||
invalidateShapeForElement(element);
|
ShapeCache.delete(element);
|
||||||
if (embeddableLinkCache.has(element.id)) {
|
if (embeddableLinkCache.has(element.id)) {
|
||||||
embeddableLinkCache.delete(element.id);
|
embeddableLinkCache.delete(element.id);
|
||||||
}
|
}
|
||||||
@ -393,7 +391,7 @@ export const getContextMenuLabel = (
|
|||||||
export const getLinkHandleFromCoords = (
|
export const getLinkHandleFromCoords = (
|
||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: number,
|
angle: number,
|
||||||
appState: UIAppState,
|
appState: Pick<UIAppState, "zoom">,
|
||||||
): [x: number, y: number, width: number, height: number] => {
|
): [x: number, y: number, width: number, height: number] => {
|
||||||
const size = DEFAULT_LINK_SIZE;
|
const size = DEFAULT_LINK_SIZE;
|
||||||
const linkWidth = size / appState.zoom.value;
|
const linkWidth = size / appState.zoom.value;
|
||||||
|
@ -474,6 +474,7 @@ const maybeCalculateNewGapWhenScaling = (
|
|||||||
return { elementId, gap: newGap, focus };
|
return { elementId, gap: newGap, focus };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: this is a bottleneck, optimise
|
||||||
export const getEligibleElementsForBinding = (
|
export const getEligibleElementsForBinding = (
|
||||||
elements: NonDeleted<ExcalidrawElement>[],
|
elements: NonDeleted<ExcalidrawElement>[],
|
||||||
): SuggestedBinding[] => {
|
): SuggestedBinding[] => {
|
||||||
|
@ -10,10 +10,7 @@ import { distance2d, rotate, rotatePoint } from "../math";
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { Drawable, Op } from "roughjs/bin/core";
|
import { Drawable, Op } from "roughjs/bin/core";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import {
|
import { generateRoughOptions } from "../renderer/renderElement";
|
||||||
getShapeForElement,
|
|
||||||
generateRoughOptions,
|
|
||||||
} from "../renderer/renderElement";
|
|
||||||
import {
|
import {
|
||||||
isArrowElement,
|
isArrowElement,
|
||||||
isFreeDrawElement,
|
isFreeDrawElement,
|
||||||
@ -24,6 +21,7 @@ import { rescalePoints } from "../points";
|
|||||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||||
import { LinearElementEditor } from "./linearElementEditor";
|
import { LinearElementEditor } from "./linearElementEditor";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
|
||||||
export type RectangleBox = {
|
export type RectangleBox = {
|
||||||
x: number;
|
x: number;
|
||||||
@ -621,7 +619,7 @@ const getLinearElementRotatedBounds = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// first element is always the curve
|
// first element is always the curve
|
||||||
const cachedShape = getShapeForElement(element)?.[0];
|
const cachedShape = ShapeCache.get(element)?.[0];
|
||||||
const shape = cachedShape ?? generateLinearElementShape(element);
|
const shape = cachedShape ?? generateLinearElementShape(element);
|
||||||
const ops = getCurvePathOps(shape);
|
const ops = getCurvePathOps(shape);
|
||||||
const transformXY = (x: number, y: number) =>
|
const transformXY = (x: number, y: number) =>
|
||||||
|
@ -39,7 +39,6 @@ import {
|
|||||||
import { FrameNameBoundsCache, Point } from "../types";
|
import { FrameNameBoundsCache, Point } from "../types";
|
||||||
import { Drawable } from "roughjs/bin/core";
|
import { Drawable } from "roughjs/bin/core";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
|
||||||
import {
|
import {
|
||||||
hasBoundTextElement,
|
hasBoundTextElement,
|
||||||
isEmbeddableElement,
|
isEmbeddableElement,
|
||||||
@ -50,6 +49,7 @@ import { isTransparent } from "../utils";
|
|||||||
import { shouldShowBoundingBox } from "./transformHandles";
|
import { shouldShowBoundingBox } from "./transformHandles";
|
||||||
import { getBoundTextElement } from "./textElement";
|
import { getBoundTextElement } from "./textElement";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
|
||||||
const isElementDraggableFromInside = (
|
const isElementDraggableFromInside = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
@ -489,7 +489,7 @@ const hitTestFreeDrawElement = (
|
|||||||
B = element.points[i + 1];
|
B = element.points[i + 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
const shape = getShapeForElement(element);
|
const shape = ShapeCache.get(element);
|
||||||
|
|
||||||
// for filled freedraw shapes, support
|
// for filled freedraw shapes, support
|
||||||
// selecting from inside
|
// selecting from inside
|
||||||
@ -502,7 +502,7 @@ const hitTestFreeDrawElement = (
|
|||||||
|
|
||||||
const hitTestLinear = (args: HitTestArgs): boolean => {
|
const hitTestLinear = (args: HitTestArgs): boolean => {
|
||||||
const { element, threshold } = args;
|
const { element, threshold } = args;
|
||||||
if (!getShapeForElement(element)) {
|
if (!ShapeCache.get(element)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -520,7 +520,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
|
|||||||
}
|
}
|
||||||
const [relX, relY] = GAPoint.toTuple(point);
|
const [relX, relY] = GAPoint.toTuple(point);
|
||||||
|
|
||||||
const shape = getShapeForElement(element as ExcalidrawLinearElement);
|
const shape = ShapeCache.get(element as ExcalidrawLinearElement);
|
||||||
|
|
||||||
if (!shape) {
|
if (!shape) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -25,7 +25,12 @@ import {
|
|||||||
getElementPointsCoords,
|
getElementPointsCoords,
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { Point, AppState, PointerCoords } from "../types";
|
import {
|
||||||
|
Point,
|
||||||
|
AppState,
|
||||||
|
PointerCoords,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import History from "../history";
|
import History from "../history";
|
||||||
|
|
||||||
@ -39,9 +44,9 @@ import { tupleToCoors } from "../utils";
|
|||||||
import { isBindingElement } from "./typeChecks";
|
import { isBindingElement } from "./typeChecks";
|
||||||
import { shouldRotateWithDiscreteAngle } from "../keys";
|
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||||
import { getShapeForElement } from "../renderer/renderElement";
|
|
||||||
import { DRAGGING_THRESHOLD } from "../constants";
|
import { DRAGGING_THRESHOLD } from "../constants";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
|
||||||
const editorMidPointsCache: {
|
const editorMidPointsCache: {
|
||||||
version: number | null;
|
version: number | null;
|
||||||
@ -398,7 +403,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static getEditorMidPoints = (
|
static getEditorMidPoints = (
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): typeof editorMidPointsCache["points"] => {
|
): typeof editorMidPointsCache["points"] => {
|
||||||
const boundText = getBoundTextElement(element);
|
const boundText = getBoundTextElement(element);
|
||||||
|
|
||||||
@ -422,7 +427,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static updateEditorMidPointsCache = (
|
static updateEditorMidPointsCache = (
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||||
|
|
||||||
@ -1418,7 +1423,7 @@ export class LinearElementEditor {
|
|||||||
let y1;
|
let y1;
|
||||||
let x2;
|
let x2;
|
||||||
let y2;
|
let y2;
|
||||||
if (element.points.length < 2 || !getShapeForElement(element)) {
|
if (element.points.length < 2 || !ShapeCache.get(element)) {
|
||||||
// XXX this is just a poor estimate and not very useful
|
// XXX this is just a poor estimate and not very useful
|
||||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||||
(limits, [x, y]) => {
|
(limits, [x, y]) => {
|
||||||
@ -1437,7 +1442,7 @@ export class LinearElementEditor {
|
|||||||
x2 = maxX + element.x;
|
x2 = maxX + element.x;
|
||||||
y2 = maxY + element.y;
|
y2 = maxY + element.y;
|
||||||
} else {
|
} else {
|
||||||
const shape = getShapeForElement(element)!;
|
const shape = ShapeCache.generateElementShape(element);
|
||||||
|
|
||||||
// first element is always the curve
|
// first element is always the curve
|
||||||
const ops = getCurvePathOps(shape[0]);
|
const ops = getCurvePathOps(shape[0]);
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { ExcalidrawElement } from "./types";
|
import { ExcalidrawElement } from "./types";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { getSizeFromPoints } from "../points";
|
import { getSizeFromPoints } from "../points";
|
||||||
import { randomInteger } from "../random";
|
import { randomInteger } from "../random";
|
||||||
import { Point } from "../types";
|
import { Point } from "../types";
|
||||||
import { getUpdatedTimestamp } from "../utils";
|
import { getUpdatedTimestamp } from "../utils";
|
||||||
import { Mutable } from "../utility-types";
|
import { Mutable } from "../utility-types";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
|
||||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||||
Partial<TElement>,
|
Partial<TElement>,
|
||||||
@ -89,7 +89,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
|||||||
typeof fileId != "undefined" ||
|
typeof fileId != "undefined" ||
|
||||||
typeof points !== "undefined"
|
typeof points !== "undefined"
|
||||||
) {
|
) {
|
||||||
invalidateShapeForElement(element);
|
ShapeCache.delete(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
element.version++;
|
element.version++;
|
||||||
|
@ -2,7 +2,9 @@ import { ExcalidrawElement } from "./types";
|
|||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||||
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
import { SHIFT_LOCKING_ANGLE } from "../constants";
|
||||||
import { AppState } from "../types";
|
import { AppState, Zoom } from "../types";
|
||||||
|
import { getElementBounds } from "./bounds";
|
||||||
|
import { viewportCoordsToSceneCoords } from "../utils";
|
||||||
|
|
||||||
export const isInvisiblySmallElement = (
|
export const isInvisiblySmallElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
@ -13,6 +15,42 @@ export const isInvisiblySmallElement = (
|
|||||||
return element.width === 0 && element.height === 0;
|
return element.width === 0 && element.height === 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isElementInViewport = (
|
||||||
|
element: ExcalidrawElement,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
viewTransformations: {
|
||||||
|
zoom: Zoom;
|
||||||
|
offsetLeft: number;
|
||||||
|
offsetTop: number;
|
||||||
|
scrollX: number;
|
||||||
|
scrollY: number;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const [x1, y1, x2, y2] = getElementBounds(element); // scene coordinates
|
||||||
|
const topLeftSceneCoords = viewportCoordsToSceneCoords(
|
||||||
|
{
|
||||||
|
clientX: viewTransformations.offsetLeft,
|
||||||
|
clientY: viewTransformations.offsetTop,
|
||||||
|
},
|
||||||
|
viewTransformations,
|
||||||
|
);
|
||||||
|
const bottomRightSceneCoords = viewportCoordsToSceneCoords(
|
||||||
|
{
|
||||||
|
clientX: viewTransformations.offsetLeft + width,
|
||||||
|
clientY: viewTransformations.offsetTop + height,
|
||||||
|
},
|
||||||
|
viewTransformations,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
topLeftSceneCoords.x <= x2 &&
|
||||||
|
topLeftSceneCoords.y <= y2 &&
|
||||||
|
bottomRightSceneCoords.x >= x1 &&
|
||||||
|
bottomRightSceneCoords.y >= y1
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a perfect shape or diagonal/horizontal/vertical line
|
* Makes a perfect shape or diagonal/horizontal/vertical line
|
||||||
*/
|
*/
|
||||||
|
@ -759,7 +759,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
|
|
||||||
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -903,7 +903,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.clickAt(10, 20);
|
mouse.clickAt(10, 20);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
mouse.up();
|
mouse.up();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1154,7 +1154,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
h.elements = [container, text];
|
h.elements = [container, text];
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1168,7 +1168,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
|
||||||
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
|
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
|
||||||
);
|
);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1406,7 +1406,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
API.setSelectedElements([textElement]);
|
API.setSelectedElements([textElement]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
|
@ -116,7 +116,7 @@ export const textWysiwyg = ({
|
|||||||
}) => void;
|
}) => void;
|
||||||
getViewportCoords: (x: number, y: number) => [number, number];
|
getViewportCoords: (x: number, y: number) => [number, number];
|
||||||
element: ExcalidrawTextElement;
|
element: ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement;
|
||||||
excalidrawContainer: HTMLDivElement | null;
|
excalidrawContainer: HTMLDivElement | null;
|
||||||
app: App;
|
app: App;
|
||||||
}) => {
|
}) => {
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { AppState, Zoom } from "../types";
|
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isFrameElement, isLinearElement } from "./typeChecks";
|
import { isFrameElement, isLinearElement } from "./typeChecks";
|
||||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||||
@ -276,8 +276,8 @@ export const getTransformHandles = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const shouldShowBoundingBox = (
|
export const shouldShowBoundingBox = (
|
||||||
elements: NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -16,14 +16,24 @@ export const isBrowserStorageStateNewer = (type: BrowserStateTypes) => {
|
|||||||
|
|
||||||
export const updateBrowserStateVersion = (type: BrowserStateTypes) => {
|
export const updateBrowserStateVersion = (type: BrowserStateTypes) => {
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
localStorage.setItem(type, JSON.stringify(timestamp));
|
try {
|
||||||
LOCAL_STATE_VERSIONS[type] = timestamp;
|
localStorage.setItem(type, JSON.stringify(timestamp));
|
||||||
|
LOCAL_STATE_VERSIONS[type] = timestamp;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error while updating browser state verison", error);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetBrowserStateVersions = () => {
|
export const resetBrowserStateVersions = () => {
|
||||||
for (const key of Object.keys(LOCAL_STATE_VERSIONS) as BrowserStateTypes[]) {
|
try {
|
||||||
const timestamp = -1;
|
for (const key of Object.keys(
|
||||||
localStorage.setItem(key, JSON.stringify(timestamp));
|
LOCAL_STATE_VERSIONS,
|
||||||
LOCAL_STATE_VERSIONS[key] = timestamp;
|
) as BrowserStateTypes[]) {
|
||||||
|
const timestamp = -1;
|
||||||
|
localStorage.setItem(key, JSON.stringify(timestamp));
|
||||||
|
LOCAL_STATE_VERSIONS[key] = timestamp;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("error while resetting browser state verison", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -598,7 +598,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Partial<AppState>,
|
appState: Partial<AppState>,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement,
|
||||||
) => {
|
) => {
|
||||||
if (exportedElements.length === 0) {
|
if (exportedElements.length === 0) {
|
||||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||||
|
20
src/frame.ts
20
src/frame.ts
@ -16,7 +16,7 @@ import {
|
|||||||
} from "./element/textElement";
|
} from "./element/textElement";
|
||||||
import { arrayToMap, findIndex } from "./utils";
|
import { arrayToMap, findIndex } from "./utils";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { AppClassProperties, AppState } from "./types";
|
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { isFrameElement } from "./element";
|
import { isFrameElement } from "./element";
|
||||||
import { moveOneRight } from "./zindex";
|
import { moveOneRight } from "./zindex";
|
||||||
@ -469,9 +469,16 @@ export const addElementsToFrame = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nextElements = allElements.slice();
|
let nextElements = allElements.slice();
|
||||||
|
// Optimisation since findIndex on "newElements" is slow
|
||||||
|
const nextElementsIndex = nextElements.reduce(
|
||||||
|
(acc: Record<string, number | undefined>, element, index) => {
|
||||||
|
acc[element.id] = index;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
|
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
|
||||||
|
|
||||||
for (const element of omitGroupsContainingFrames(
|
for (const element of omitGroupsContainingFrames(
|
||||||
allElements,
|
allElements,
|
||||||
_elementsToAdd,
|
_elementsToAdd,
|
||||||
@ -485,8 +492,8 @@ export const addElementsToFrame = (
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
|
const frameIndex = nextElementsIndex[frame.id] ?? -1;
|
||||||
const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
|
const elementIndex = nextElementsIndex[element.id] ?? -1;
|
||||||
|
|
||||||
if (elementIndex < frameBoundary) {
|
if (elementIndex < frameBoundary) {
|
||||||
nextElements = [
|
nextElements = [
|
||||||
@ -648,7 +655,7 @@ export const omitGroupsContainingFrames = (
|
|||||||
*/
|
*/
|
||||||
export const getTargetFrame = (
|
export const getTargetFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
appState: AppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element) || element
|
? getContainerElement(element) || element
|
||||||
@ -660,11 +667,12 @@ export const getTargetFrame = (
|
|||||||
: getContainingFrame(_element);
|
: getContainingFrame(_element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: this a huge bottleneck for large scenes, optimise
|
||||||
// given an element, return if the element is in some frame
|
// given an element, return if the element is in some frame
|
||||||
export const isElementInFrame = (
|
export const isElementInFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
appState: AppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const frame = getTargetFrame(element, appState);
|
const frame = getTargetFrame(element, appState);
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
|
246
src/groups.ts
246
src/groups.ts
@ -4,27 +4,40 @@ import {
|
|||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { AppClassProperties, AppState } from "./types";
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
import { makeNextSelectedElementIds } from "./scene/selection";
|
import { makeNextSelectedElementIds } from "./scene/selection";
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
): AppState => {
|
): Pick<
|
||||||
const elementsInGroup = elements.filter((element) =>
|
InteractiveCanvasAppState,
|
||||||
element.groupIds.includes(groupId),
|
"selectedGroupIds" | "selectedElementIds" | "editingGroupId"
|
||||||
|
> => {
|
||||||
|
const elementsInGroup = elements.reduce(
|
||||||
|
(acc: Record<string, true>, element) => {
|
||||||
|
if (element.groupIds.includes(groupId)) {
|
||||||
|
acc[element.id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (elementsInGroup.length < 2) {
|
if (Object.keys(elementsInGroup).length < 2) {
|
||||||
if (
|
if (
|
||||||
appState.selectedGroupIds[groupId] ||
|
appState.selectedGroupIds[groupId] ||
|
||||||
appState.editingGroupId === groupId
|
appState.editingGroupId === groupId
|
||||||
) {
|
) {
|
||||||
return {
|
return {
|
||||||
...appState,
|
selectedElementIds: appState.selectedElementIds,
|
||||||
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: false },
|
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: false },
|
||||||
editingGroupId: null,
|
editingGroupId: null,
|
||||||
};
|
};
|
||||||
@ -33,104 +46,184 @@ export const selectGroup = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...appState,
|
editingGroupId: appState.editingGroupId,
|
||||||
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
|
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
...Object.fromEntries(
|
...elementsInGroup,
|
||||||
elementsInGroup.map((element) => [element.id, true]),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectGroupsForSelectedElements = (function () {
|
||||||
|
type SelectGroupsReturnType = Pick<
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
"selectedGroupIds" | "editingGroupId" | "selectedElementIds"
|
||||||
|
>;
|
||||||
|
|
||||||
|
let lastSelectedElements: readonly NonDeleted<ExcalidrawElement>[] | null =
|
||||||
|
null;
|
||||||
|
let lastElements: readonly NonDeleted<ExcalidrawElement>[] | null = null;
|
||||||
|
let lastReturnValue: SelectGroupsReturnType | null = null;
|
||||||
|
|
||||||
|
const _selectGroups = (
|
||||||
|
selectedElements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
|
appState: Pick<AppState, "selectedElementIds" | "editingGroupId">,
|
||||||
|
): SelectGroupsReturnType => {
|
||||||
|
if (
|
||||||
|
lastReturnValue !== undefined &&
|
||||||
|
elements === lastElements &&
|
||||||
|
selectedElements === lastSelectedElements &&
|
||||||
|
appState.editingGroupId === lastReturnValue?.editingGroupId
|
||||||
|
) {
|
||||||
|
return lastReturnValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const selectedGroupIds: Record<GroupId, boolean> = {};
|
||||||
|
// Gather all the groups withing selected elements
|
||||||
|
for (const selectedElement of selectedElements) {
|
||||||
|
let groupIds = selectedElement.groupIds;
|
||||||
|
if (appState.editingGroupId) {
|
||||||
|
// handle the case where a group is nested within a group
|
||||||
|
const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
|
||||||
|
if (indexOfEditingGroup > -1) {
|
||||||
|
groupIds = groupIds.slice(0, indexOfEditingGroup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (groupIds.length > 0) {
|
||||||
|
const lastSelectedGroup = groupIds[groupIds.length - 1];
|
||||||
|
selectedGroupIds[lastSelectedGroup] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather all the elements within selected groups
|
||||||
|
const groupElementsIndex: Record<GroupId, string[]> = {};
|
||||||
|
const selectedElementIdsInGroups = elements.reduce(
|
||||||
|
(acc: Record<string, true>, element) => {
|
||||||
|
const groupId = element.groupIds.find((id) => selectedGroupIds[id]);
|
||||||
|
|
||||||
|
if (groupId) {
|
||||||
|
acc[element.id] = true;
|
||||||
|
|
||||||
|
// Populate the index
|
||||||
|
if (!Array.isArray(groupElementsIndex[groupId])) {
|
||||||
|
groupElementsIndex[groupId] = [element.id];
|
||||||
|
} else {
|
||||||
|
groupElementsIndex[groupId].push(element.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const groupId of Object.keys(groupElementsIndex)) {
|
||||||
|
// If there is one element in the group, and the group is selected or it's being edited, it's not a group
|
||||||
|
if (groupElementsIndex[groupId].length < 2) {
|
||||||
|
if (selectedGroupIds[groupId]) {
|
||||||
|
selectedGroupIds[groupId] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastElements = elements;
|
||||||
|
lastSelectedElements = selectedElements;
|
||||||
|
|
||||||
|
lastReturnValue = {
|
||||||
|
editingGroupId: appState.editingGroupId,
|
||||||
|
selectedGroupIds,
|
||||||
|
selectedElementIds: {
|
||||||
|
...appState.selectedElementIds,
|
||||||
|
...selectedElementIdsInGroups,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return lastReturnValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When you select an element, you often want to actually select the whole group it's in, unless
|
||||||
|
* you're currently editing that group.
|
||||||
|
*/
|
||||||
|
const selectGroupsForSelectedElements = (
|
||||||
|
appState: Pick<AppState, "selectedElementIds" | "editingGroupId">,
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
prevAppState: InteractiveCanvasAppState,
|
||||||
|
/**
|
||||||
|
* supply null in cases where you don't have access to App instance and
|
||||||
|
* you don't care about optimizing selectElements retrieval
|
||||||
|
*/
|
||||||
|
app: AppClassProperties | null,
|
||||||
|
): Pick<
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
"selectedGroupIds" | "editingGroupId" | "selectedElementIds"
|
||||||
|
> => {
|
||||||
|
const selectedElements = app
|
||||||
|
? app.scene.getSelectedElements({
|
||||||
|
selectedElementIds: appState.selectedElementIds,
|
||||||
|
// supplying elements explicitly in case we're passed non-state elements
|
||||||
|
elements,
|
||||||
|
})
|
||||||
|
: getSelectedElements(elements, appState);
|
||||||
|
|
||||||
|
if (!selectedElements.length) {
|
||||||
|
return {
|
||||||
|
selectedGroupIds: {},
|
||||||
|
editingGroupId: null,
|
||||||
|
selectedElementIds: makeNextSelectedElementIds(
|
||||||
|
appState.selectedElementIds,
|
||||||
|
prevAppState,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return _selectGroups(selectedElements, elements, appState);
|
||||||
|
};
|
||||||
|
|
||||||
|
selectGroupsForSelectedElements.clearCache = () => {
|
||||||
|
lastElements = null;
|
||||||
|
lastSelectedElements = null;
|
||||||
|
lastReturnValue = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return selectGroupsForSelectedElements;
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the element's group is selected, don't render an individual
|
* If the element's group is selected, don't render an individual
|
||||||
* selection border around it.
|
* selection border around it.
|
||||||
*/
|
*/
|
||||||
export const isSelectedViaGroup = (
|
export const isSelectedViaGroup = (
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) => getSelectedGroupForElement(appState, element) != null;
|
) => getSelectedGroupForElement(appState, element) != null;
|
||||||
|
|
||||||
export const getSelectedGroupForElement = (
|
export const getSelectedGroupForElement = (
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) =>
|
) =>
|
||||||
element.groupIds
|
element.groupIds
|
||||||
.filter((groupId) => groupId !== appState.editingGroupId)
|
.filter((groupId) => groupId !== appState.editingGroupId)
|
||||||
.find((groupId) => appState.selectedGroupIds[groupId]);
|
.find((groupId) => appState.selectedGroupIds[groupId]);
|
||||||
|
|
||||||
export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
export const getSelectedGroupIds = (
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
): GroupId[] =>
|
||||||
Object.entries(appState.selectedGroupIds)
|
Object.entries(appState.selectedGroupIds)
|
||||||
.filter(([groupId, isSelected]) => isSelected)
|
.filter(([groupId, isSelected]) => isSelected)
|
||||||
.map(([groupId, isSelected]) => groupId);
|
.map(([groupId, isSelected]) => groupId);
|
||||||
|
|
||||||
/**
|
|
||||||
* When you select an element, you often want to actually select the whole group it's in, unless
|
|
||||||
* you're currently editing that group.
|
|
||||||
*/
|
|
||||||
export const selectGroupsForSelectedElements = (
|
|
||||||
appState: AppState,
|
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
|
||||||
prevAppState: AppState,
|
|
||||||
/**
|
|
||||||
* supply null in cases where you don't have access to App instance and
|
|
||||||
* you don't care about optimizing selectElements retrieval
|
|
||||||
*/
|
|
||||||
app: AppClassProperties | null,
|
|
||||||
): AppState => {
|
|
||||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
|
||||||
|
|
||||||
const selectedElements = app
|
|
||||||
? app.scene.getSelectedElements({
|
|
||||||
selectedElementIds: appState.selectedElementIds,
|
|
||||||
// supplying elements explicitly in case we're passed non-state elements
|
|
||||||
elements,
|
|
||||||
})
|
|
||||||
: getSelectedElements(elements, appState);
|
|
||||||
|
|
||||||
if (!selectedElements.length) {
|
|
||||||
return {
|
|
||||||
...nextAppState,
|
|
||||||
editingGroupId: null,
|
|
||||||
selectedElementIds: makeNextSelectedElementIds(
|
|
||||||
nextAppState.selectedElementIds,
|
|
||||||
prevAppState,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const selectedElement of selectedElements) {
|
|
||||||
let groupIds = selectedElement.groupIds;
|
|
||||||
if (appState.editingGroupId) {
|
|
||||||
// handle the case where a group is nested within a group
|
|
||||||
const indexOfEditingGroup = groupIds.indexOf(appState.editingGroupId);
|
|
||||||
if (indexOfEditingGroup > -1) {
|
|
||||||
groupIds = groupIds.slice(0, indexOfEditingGroup);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (groupIds.length > 0) {
|
|
||||||
const groupId = groupIds[groupIds.length - 1];
|
|
||||||
nextAppState = selectGroup(groupId, nextAppState, elements);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
nextAppState.selectedElementIds = makeNextSelectedElementIds(
|
|
||||||
nextAppState.selectedElementIds,
|
|
||||||
prevAppState,
|
|
||||||
);
|
|
||||||
|
|
||||||
return nextAppState;
|
|
||||||
};
|
|
||||||
|
|
||||||
// given a list of elements, return the the actual group ids that should be selected
|
// given a list of elements, return the the actual group ids that should be selected
|
||||||
// or used to update the elements
|
// or used to update the elements
|
||||||
export const selectGroupsFromGivenElements = (
|
export const selectGroupsFromGivenElements = (
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
let nextAppState: InteractiveCanvasAppState = {
|
||||||
|
...appState,
|
||||||
|
selectedGroupIds: {},
|
||||||
|
};
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
let groupIds = element.groupIds;
|
let groupIds = element.groupIds;
|
||||||
@ -142,7 +235,10 @@ export const selectGroupsFromGivenElements = (
|
|||||||
}
|
}
|
||||||
if (groupIds.length > 0) {
|
if (groupIds.length > 0) {
|
||||||
const groupId = groupIds[groupIds.length - 1];
|
const groupId = groupIds[groupIds.length - 1];
|
||||||
nextAppState = selectGroup(groupId, nextAppState, elements);
|
nextAppState = {
|
||||||
|
...nextAppState,
|
||||||
|
...selectGroup(groupId, nextAppState, elements),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,9 +10,9 @@ import {
|
|||||||
ExcalidrawLinearElement,
|
ExcalidrawLinearElement,
|
||||||
NonDeleted,
|
NonDeleted,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { getShapeForElement } from "./renderer/renderElement";
|
|
||||||
import { getCurvePathOps } from "./element/bounds";
|
import { getCurvePathOps } from "./element/bounds";
|
||||||
import { Mutable } from "./utility-types";
|
import { Mutable } from "./utility-types";
|
||||||
|
import { ShapeCache } from "./scene/ShapeCache";
|
||||||
|
|
||||||
export const rotate = (
|
export const rotate = (
|
||||||
x1: number,
|
x1: number,
|
||||||
@ -303,7 +303,7 @@ export const getControlPointsForBezierCurve = (
|
|||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
endPoint: Point,
|
endPoint: Point,
|
||||||
) => {
|
) => {
|
||||||
const shape = getShapeForElement(element as ExcalidrawLinearElement);
|
const shape = ShapeCache.generateElementShape(element);
|
||||||
if (!shape) {
|
if (!shape) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ import { Drawable, Options } from "roughjs/bin/core";
|
|||||||
import { RoughSVG } from "roughjs/bin/svg";
|
import { RoughSVG } from "roughjs/bin/svg";
|
||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
import { RenderConfig } from "../scene/types";
|
import { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
import {
|
import {
|
||||||
distance,
|
distance,
|
||||||
getFontString,
|
getFontString,
|
||||||
@ -36,7 +36,13 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
import {
|
||||||
|
AppState,
|
||||||
|
StaticCanvasAppState,
|
||||||
|
BinaryFiles,
|
||||||
|
Zoom,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
@ -61,6 +67,7 @@ import {
|
|||||||
} from "../element/embeddable";
|
} from "../element/embeddable";
|
||||||
import { getContainingFrame } from "../frame";
|
import { getContainingFrame } from "../frame";
|
||||||
import { normalizeLink, toValidURL } from "../data/url";
|
import { normalizeLink, toValidURL } from "../data/url";
|
||||||
|
import { ShapeCache } from "../scene/ShapeCache";
|
||||||
|
|
||||||
// using a stronger invert (100% vs our regular 93%) and saturate
|
// using a stronger invert (100% vs our regular 93%) and saturate
|
||||||
// as a temp hack to make images in dark theme look closer to original
|
// as a temp hack to make images in dark theme look closer to original
|
||||||
@ -72,17 +79,18 @@ const defaultAppState = getDefaultAppState();
|
|||||||
|
|
||||||
const isPendingImageElement = (
|
const isPendingImageElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
) =>
|
) =>
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
!renderConfig.imageCache.has(element.fileId);
|
!renderConfig.imageCache.has(element.fileId);
|
||||||
|
|
||||||
const shouldResetImageFilter = (
|
const shouldResetImageFilter = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
renderConfig.theme === "dark" &&
|
appState.theme === "dark" &&
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
!isPendingImageElement(element, renderConfig) &&
|
!isPendingImageElement(element, renderConfig) &&
|
||||||
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
||||||
@ -99,9 +107,9 @@ const getCanvasPadding = (element: ExcalidrawElement) =>
|
|||||||
export interface ExcalidrawElementWithCanvas {
|
export interface ExcalidrawElementWithCanvas {
|
||||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
theme: RenderConfig["theme"];
|
theme: AppState["theme"];
|
||||||
scale: number;
|
scale: number;
|
||||||
zoomValue: RenderConfig["zoom"]["value"];
|
zoomValue: AppState["zoom"]["value"];
|
||||||
canvasOffsetX: number;
|
canvasOffsetX: number;
|
||||||
canvasOffsetY: number;
|
canvasOffsetY: number;
|
||||||
boundTextElementVersion: number | null;
|
boundTextElementVersion: number | null;
|
||||||
@ -165,7 +173,8 @@ const cappedElementCanvasSize = (
|
|||||||
const generateElementCanvas = (
|
const generateElementCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
): ExcalidrawElementWithCanvas => {
|
): ExcalidrawElementWithCanvas => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
@ -205,17 +214,17 @@ const generateElementCanvas = (
|
|||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
|
|
||||||
// in dark theme, revert the image color filter
|
// in dark theme, revert the image color filter
|
||||||
if (shouldResetImageFilter(element, renderConfig)) {
|
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
context.filter = IMAGE_INVERT_FILTER;
|
context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
canvas,
|
canvas,
|
||||||
theme: renderConfig.theme,
|
theme: appState.theme,
|
||||||
scale,
|
scale,
|
||||||
zoomValue: zoom.value,
|
zoomValue: zoom.value,
|
||||||
canvasOffsetX,
|
canvasOffsetX,
|
||||||
@ -262,11 +271,13 @@ const drawImagePlaceholder = (
|
|||||||
size,
|
size,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const drawElementOnCanvas = (
|
const drawElementOnCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
context.globalAlpha =
|
context.globalAlpha =
|
||||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||||
@ -277,7 +288,7 @@ const drawElementOnCanvas = (
|
|||||||
case "ellipse": {
|
case "ellipse": {
|
||||||
context.lineJoin = "round";
|
context.lineJoin = "round";
|
||||||
context.lineCap = "round";
|
context.lineCap = "round";
|
||||||
rc.draw(getShapeForElement(element)!);
|
rc.draw(ShapeCache.get(element)!);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "arrow":
|
case "arrow":
|
||||||
@ -285,7 +296,7 @@ const drawElementOnCanvas = (
|
|||||||
context.lineJoin = "round";
|
context.lineJoin = "round";
|
||||||
context.lineCap = "round";
|
context.lineCap = "round";
|
||||||
|
|
||||||
getShapeForElement(element)!.forEach((shape) => {
|
ShapeCache.get(element)!.forEach((shape) => {
|
||||||
rc.draw(shape);
|
rc.draw(shape);
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@ -296,7 +307,7 @@ const drawElementOnCanvas = (
|
|||||||
context.fillStyle = element.strokeColor;
|
context.fillStyle = element.strokeColor;
|
||||||
|
|
||||||
const path = getFreeDrawPath2D(element) as Path2D;
|
const path = getFreeDrawPath2D(element) as Path2D;
|
||||||
const fillShape = getShapeForElement(element);
|
const fillShape = ShapeCache.get(element);
|
||||||
|
|
||||||
if (fillShape) {
|
if (fillShape) {
|
||||||
rc.draw(fillShape);
|
rc.draw(fillShape);
|
||||||
@ -321,7 +332,7 @@ const drawElementOnCanvas = (
|
|||||||
element.height,
|
element.height,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
drawImagePlaceholder(element, context, renderConfig.zoom.value);
|
drawImagePlaceholder(element, context, appState.zoom.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -378,33 +389,6 @@ const elementWithCanvasCache = new WeakMap<
|
|||||||
ExcalidrawElementWithCanvas
|
ExcalidrawElementWithCanvas
|
||||||
>();
|
>();
|
||||||
|
|
||||||
const shapeCache = new WeakMap<ExcalidrawElement, ElementShape>();
|
|
||||||
|
|
||||||
type ElementShape = Drawable | Drawable[] | null;
|
|
||||||
|
|
||||||
type ElementShapes = {
|
|
||||||
freedraw: Drawable | null;
|
|
||||||
arrow: Drawable[];
|
|
||||||
line: Drawable[];
|
|
||||||
text: null;
|
|
||||||
image: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getShapeForElement = <T extends ExcalidrawElement>(element: T) =>
|
|
||||||
shapeCache.get(element) as T["type"] extends keyof ElementShapes
|
|
||||||
? ElementShapes[T["type"]] | undefined
|
|
||||||
: Drawable | null | undefined;
|
|
||||||
|
|
||||||
export const setShapeForElement = <T extends ExcalidrawElement>(
|
|
||||||
element: T,
|
|
||||||
shape: T["type"] extends keyof ElementShapes
|
|
||||||
? ElementShapes[T["type"]]
|
|
||||||
: Drawable,
|
|
||||||
) => shapeCache.set(element, shape);
|
|
||||||
|
|
||||||
export const invalidateShapeForElement = (element: ExcalidrawElement) =>
|
|
||||||
shapeCache.delete(element);
|
|
||||||
|
|
||||||
export const generateRoughOptions = (
|
export const generateRoughOptions = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
continuousPath = false,
|
continuousPath = false,
|
||||||
@ -494,16 +478,22 @@ const modifyEmbeddableForRoughOptions = (
|
|||||||
* @param element
|
* @param element
|
||||||
* @param generator
|
* @param generator
|
||||||
*/
|
*/
|
||||||
const generateElementShape = (
|
export const generateElementShape = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
generator: RoughGenerator,
|
generator: RoughGenerator,
|
||||||
isExporting: boolean = false,
|
isExporting: boolean = false,
|
||||||
) => {
|
): Drawable | Drawable[] | null => {
|
||||||
let shape = isExporting ? undefined : shapeCache.get(element);
|
const cachedShape = isExporting ? undefined : ShapeCache.get(element);
|
||||||
|
|
||||||
|
if (cachedShape) {
|
||||||
|
return cachedShape;
|
||||||
|
}
|
||||||
|
|
||||||
// `null` indicates no rc shape applicable for this element type
|
// `null` indicates no rc shape applicable for this element type
|
||||||
// (= do not generate anything)
|
// (= do not generate anything)
|
||||||
if (shape === undefined) {
|
if (cachedShape === undefined) {
|
||||||
|
let shape: Drawable | Drawable[] | null = null;
|
||||||
|
|
||||||
elementWithCanvasCache.delete(element);
|
elementWithCanvasCache.delete(element);
|
||||||
|
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
@ -539,7 +529,7 @@ const generateElementShape = (
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setShapeForElement(element, shape);
|
ShapeCache.set(element, shape);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -589,7 +579,7 @@ const generateElementShape = (
|
|||||||
generateRoughOptions(element),
|
generateRoughOptions(element),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
setShapeForElement(element, shape);
|
ShapeCache.set(element, shape);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -601,7 +591,7 @@ const generateElementShape = (
|
|||||||
element.height,
|
element.height,
|
||||||
generateRoughOptions(element),
|
generateRoughOptions(element),
|
||||||
);
|
);
|
||||||
setShapeForElement(element, shape);
|
ShapeCache.set(element, shape);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "line":
|
case "line":
|
||||||
@ -726,7 +716,7 @@ const generateElementShape = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setShapeForElement(element, shape);
|
ShapeCache.set(element, shape);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -742,36 +732,39 @@ const generateElementShape = (
|
|||||||
} else {
|
} else {
|
||||||
shape = null;
|
shape = null;
|
||||||
}
|
}
|
||||||
setShapeForElement(element, shape);
|
ShapeCache.set(element, shape);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "text":
|
case "text":
|
||||||
case "image": {
|
case "image": {
|
||||||
// just to ensure we don't regenerate element.canvas on rerenders
|
// just to ensure we don't regenerate element.canvas on rerenders
|
||||||
setShapeForElement(element, null);
|
ShapeCache.set(element, null);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return shape;
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const generateElementWithCanvas = (
|
const generateElementWithCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const zoom: Zoom = renderConfig ? renderConfig.zoom : defaultAppState.zoom;
|
const zoom: Zoom = renderConfig ? appState.zoom : defaultAppState.zoom;
|
||||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||||
const shouldRegenerateBecauseZoom =
|
const shouldRegenerateBecauseZoom =
|
||||||
prevElementWithCanvas &&
|
prevElementWithCanvas &&
|
||||||
prevElementWithCanvas.zoomValue !== zoom.value &&
|
prevElementWithCanvas.zoomValue !== zoom.value &&
|
||||||
!renderConfig?.shouldCacheIgnoreZoom;
|
!appState?.shouldCacheIgnoreZoom;
|
||||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||||
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!prevElementWithCanvas ||
|
!prevElementWithCanvas ||
|
||||||
shouldRegenerateBecauseZoom ||
|
shouldRegenerateBecauseZoom ||
|
||||||
prevElementWithCanvas.theme !== renderConfig.theme ||
|
prevElementWithCanvas.theme !== appState.theme ||
|
||||||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
|
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
|
||||||
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
|
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
|
||||||
) {
|
) {
|
||||||
@ -779,6 +772,7 @@ const generateElementWithCanvas = (
|
|||||||
element,
|
element,
|
||||||
zoom,
|
zoom,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||||
@ -790,9 +784,9 @@ const generateElementWithCanvas = (
|
|||||||
|
|
||||||
const drawElementFromCanvas = (
|
const drawElementFromCanvas = (
|
||||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||||
rc: RoughCanvas,
|
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const element = elementWithCanvas.element;
|
const element = elementWithCanvas.element;
|
||||||
const padding = getCanvasPadding(element);
|
const padding = getCanvasPadding(element);
|
||||||
@ -807,8 +801,8 @@ const drawElementFromCanvas = (
|
|||||||
y2 = Math.ceil(y2);
|
y2 = Math.ceil(y2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio;
|
const cx = ((x1 + x2) / 2 + appState.scrollX) * window.devicePixelRatio;
|
||||||
const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio;
|
const cy = ((y1 + y2) / 2 + appState.scrollY) * window.devicePixelRatio;
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||||
@ -906,9 +900,9 @@ const drawElementFromCanvas = (
|
|||||||
|
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
elementWithCanvas.canvas!,
|
elementWithCanvas.canvas!,
|
||||||
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
(x1 + appState.scrollX) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||||
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
(y1 + appState.scrollY) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||||
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
|
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
|
||||||
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
||||||
@ -926,8 +920,8 @@ const drawElementFromCanvas = (
|
|||||||
context.strokeStyle = "#c92a2a";
|
context.strokeStyle = "#c92a2a";
|
||||||
context.lineWidth = 3;
|
context.lineWidth = 3;
|
||||||
context.strokeRect(
|
context.strokeRect(
|
||||||
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
(coords.x + appState.scrollX) * window.devicePixelRatio,
|
||||||
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
(coords.y + appState.scrollY) * window.devicePixelRatio,
|
||||||
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||||
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
@ -938,40 +932,38 @@ const drawElementFromCanvas = (
|
|||||||
// Clear the nested element we appended to the DOM
|
// Clear the nested element we appended to the DOM
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const renderSelectionElement = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.translate(element.x + appState.scrollX, element.y + appState.scrollY);
|
||||||
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
|
// render from 0.5px offset to get 1px wide line
|
||||||
|
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
|
||||||
|
// TODO can be be improved by offseting to the negative when user selects
|
||||||
|
// from right to left
|
||||||
|
const offset = 0.5 / appState.zoom.value;
|
||||||
|
|
||||||
|
context.fillRect(offset, offset, element.width, element.height);
|
||||||
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
|
context.strokeStyle = " rgb(105, 101, 219)";
|
||||||
|
context.strokeRect(offset, offset, element.width, element.height);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
export const renderElement = (
|
export const renderElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: AppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const generator = rc.generator;
|
const generator = rc.generator;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "selection": {
|
|
||||||
// do not render selection when exporting
|
|
||||||
if (!renderConfig.isExporting) {
|
|
||||||
context.save();
|
|
||||||
context.translate(
|
|
||||||
element.x + renderConfig.scrollX,
|
|
||||||
element.y + renderConfig.scrollY,
|
|
||||||
);
|
|
||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
|
||||||
|
|
||||||
// render from 0.5px offset to get 1px wide line
|
|
||||||
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
|
|
||||||
// TODO can be be improved by offseting to the negative when user selects
|
|
||||||
// from right to left
|
|
||||||
const offset = 0.5 / renderConfig.zoom.value;
|
|
||||||
|
|
||||||
context.fillRect(offset, offset, element.width, element.height);
|
|
||||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
|
||||||
context.strokeStyle = " rgb(105, 101, 219)";
|
|
||||||
context.strokeRect(offset, offset, element.width, element.height);
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "frame": {
|
case "frame": {
|
||||||
if (
|
if (
|
||||||
!renderConfig.isExporting &&
|
!renderConfig.isExporting &&
|
||||||
@ -980,12 +972,12 @@ export const renderElement = (
|
|||||||
) {
|
) {
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(
|
context.translate(
|
||||||
element.x + renderConfig.scrollX,
|
element.x + appState.scrollX,
|
||||||
element.y + renderConfig.scrollY,
|
element.y + appState.scrollY,
|
||||||
);
|
);
|
||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
context.lineWidth = 2 / renderConfig.zoom.value;
|
context.lineWidth = 2 / appState.zoom.value;
|
||||||
context.strokeStyle = FRAME_STYLE.strokeColor;
|
context.strokeStyle = FRAME_STYLE.strokeColor;
|
||||||
|
|
||||||
if (FRAME_STYLE.radius && context.roundRect) {
|
if (FRAME_STYLE.radius && context.roundRect) {
|
||||||
@ -995,7 +987,7 @@ export const renderElement = (
|
|||||||
0,
|
0,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
FRAME_STYLE.radius / renderConfig.zoom.value,
|
FRAME_STYLE.radius / appState.zoom.value,
|
||||||
);
|
);
|
||||||
context.stroke();
|
context.stroke();
|
||||||
context.closePath();
|
context.closePath();
|
||||||
@ -1012,22 +1004,28 @@ export const renderElement = (
|
|||||||
|
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
const cy = (y1 + y2) / 2 + appState.scrollY;
|
||||||
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||||
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
element,
|
element,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
drawElementFromCanvas(
|
||||||
|
elementWithCanvas,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -1043,8 +1041,8 @@ export const renderElement = (
|
|||||||
generateElementShape(element, generator, renderConfig.isExporting);
|
generateElementShape(element, generator, renderConfig.isExporting);
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
const cy = (y1 + y2) / 2 + appState.scrollY;
|
||||||
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||||
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
@ -1062,7 +1060,7 @@ export const renderElement = (
|
|||||||
context.save();
|
context.save();
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
|
|
||||||
if (shouldResetImageFilter(element, renderConfig)) {
|
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
context.filter = "none";
|
context.filter = "none";
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
@ -1096,7 +1094,13 @@ export const renderElement = (
|
|||||||
|
|
||||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||||
|
|
||||||
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
|
drawElementOnCanvas(
|
||||||
|
element,
|
||||||
|
tempRc,
|
||||||
|
tempCanvasContext,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
tempCanvasContext.translate(shiftX, shiftY);
|
tempCanvasContext.translate(shiftX, shiftY);
|
||||||
|
|
||||||
@ -1133,7 +1137,7 @@ export const renderElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
@ -1143,6 +1147,7 @@ export const renderElement = (
|
|||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
element,
|
element,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
|
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
|
||||||
@ -1150,7 +1155,7 @@ export const renderElement = (
|
|||||||
if (
|
if (
|
||||||
// do not disable smoothing during zoom as blurry shapes look better
|
// do not disable smoothing during zoom as blurry shapes look better
|
||||||
// on low resolution (while still zooming in) than sharp ones
|
// on low resolution (while still zooming in) than sharp ones
|
||||||
!renderConfig?.shouldCacheIgnoreZoom &&
|
!appState?.shouldCacheIgnoreZoom &&
|
||||||
// angle is 0 -> always disable smoothing
|
// angle is 0 -> always disable smoothing
|
||||||
(!element.angle ||
|
(!element.angle ||
|
||||||
// or check if angle is a right angle in which case we can still
|
// or check if angle is a right angle in which case we can still
|
||||||
@ -1167,7 +1172,12 @@ export const renderElement = (
|
|||||||
context.imageSmoothingEnabled = false;
|
context.imageSmoothingEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
drawElementFromCanvas(
|
||||||
|
elementWithCanvas,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
||||||
@ -1273,7 +1283,7 @@ export const renderElementToSvg = (
|
|||||||
generateElementShape(element, generator);
|
generateElementShape(element, generator);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
getShapeForElement(element)!,
|
ShapeCache.get(element)!,
|
||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
);
|
);
|
||||||
if (opacity !== 1) {
|
if (opacity !== 1) {
|
||||||
@ -1303,7 +1313,7 @@ export const renderElementToSvg = (
|
|||||||
generateElementShape(element, generator, true);
|
generateElementShape(element, generator, true);
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
getShapeForElement(element)!,
|
ShapeCache.get(element)!,
|
||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
);
|
);
|
||||||
const opacity = element.opacity / 100;
|
const opacity = element.opacity / 100;
|
||||||
@ -1337,7 +1347,7 @@ export const renderElementToSvg = (
|
|||||||
// render embeddable element + iframe
|
// render embeddable element + iframe
|
||||||
const embeddableNode = roughSVGDrawWithPrecision(
|
const embeddableNode = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
getShapeForElement(element)!,
|
ShapeCache.get(element)!,
|
||||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||||
);
|
);
|
||||||
embeddableNode.setAttribute("stroke-linecap", "round");
|
embeddableNode.setAttribute("stroke-linecap", "round");
|
||||||
@ -1450,7 +1460,7 @@ export const renderElementToSvg = (
|
|||||||
}
|
}
|
||||||
group.setAttribute("stroke-linecap", "round");
|
group.setAttribute("stroke-linecap", "round");
|
||||||
|
|
||||||
getShapeForElement(element)!.forEach((shape) => {
|
ShapeCache.get(element)!.forEach((shape) => {
|
||||||
const node = roughSVGDrawWithPrecision(
|
const node = roughSVGDrawWithPrecision(
|
||||||
rsvg,
|
rsvg,
|
||||||
shape,
|
shape,
|
||||||
@ -1493,7 +1503,7 @@ export const renderElementToSvg = (
|
|||||||
case "freedraw": {
|
case "freedraw": {
|
||||||
generateElementShape(element, generator);
|
generateElementShape(element, generator);
|
||||||
generateFreeDrawShape(element);
|
generateFreeDrawShape(element);
|
||||||
const shape = getShapeForElement(element);
|
const shape = ShapeCache.get(element);
|
||||||
const node = shape
|
const node = shape
|
||||||
? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
|
? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
|
||||||
: svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
: svgRoot.ownerDocument!.createElementNS(SVG_NS, "g");
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,9 @@ import { isTextElement, refreshTextDimensions } from "../element";
|
|||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
|
||||||
import { getFontString } from "../utils";
|
import { getFontString } from "../utils";
|
||||||
import type Scene from "./Scene";
|
import type Scene from "./Scene";
|
||||||
|
import { ShapeCache } from "./ShapeCache";
|
||||||
|
|
||||||
export class Fonts {
|
export class Fonts {
|
||||||
private scene: Scene;
|
private scene: Scene;
|
||||||
@ -54,7 +54,7 @@ export class Fonts {
|
|||||||
|
|
||||||
this.scene.mapElements((element) => {
|
this.scene.mapElements((element) => {
|
||||||
if (isTextElement(element) && !isBoundToContainer(element)) {
|
if (isTextElement(element) && !isBoundToContainer(element)) {
|
||||||
invalidateShapeForElement(element);
|
ShapeCache.delete(element);
|
||||||
didUpdate = true;
|
didUpdate = true;
|
||||||
return newElementWith(element, {
|
return newElementWith(element, {
|
||||||
...refreshTextDimensions(element),
|
...refreshTextDimensions(element),
|
||||||
|
131
src/scene/Renderer.ts
Normal file
131
src/scene/Renderer.ts
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
import { isElementInViewport } from "../element/sizeHelpers";
|
||||||
|
import { isImageElement } from "../element/typeChecks";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
import { cancelRender } from "../renderer/renderScene";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
import { memoize } from "../utils";
|
||||||
|
import Scene from "./Scene";
|
||||||
|
|
||||||
|
export class Renderer {
|
||||||
|
private scene: Scene;
|
||||||
|
|
||||||
|
constructor(scene: Scene) {
|
||||||
|
this.scene = scene;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRenderableElements = (() => {
|
||||||
|
const getVisibleCanvasElements = ({
|
||||||
|
elements,
|
||||||
|
zoom,
|
||||||
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
}: {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
zoom: AppState["zoom"];
|
||||||
|
offsetLeft: AppState["offsetLeft"];
|
||||||
|
offsetTop: AppState["offsetTop"];
|
||||||
|
scrollX: AppState["scrollX"];
|
||||||
|
scrollY: AppState["scrollY"];
|
||||||
|
height: AppState["height"];
|
||||||
|
width: AppState["width"];
|
||||||
|
}): readonly NonDeletedExcalidrawElement[] => {
|
||||||
|
return elements.filter((element) =>
|
||||||
|
isElementInViewport(element, width, height, {
|
||||||
|
zoom,
|
||||||
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCanvasElements = ({
|
||||||
|
editingElement,
|
||||||
|
elements,
|
||||||
|
pendingImageElementId,
|
||||||
|
}: {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
editingElement: AppState["editingElement"];
|
||||||
|
pendingImageElementId: AppState["pendingImageElementId"];
|
||||||
|
}) => {
|
||||||
|
return elements.filter((element) => {
|
||||||
|
if (isImageElement(element)) {
|
||||||
|
if (
|
||||||
|
// => not placed on canvas yet (but in elements array)
|
||||||
|
pendingImageElementId === element.id
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// we don't want to render text element that's being currently edited
|
||||||
|
// (it's rendered on remote only)
|
||||||
|
return (
|
||||||
|
!editingElement ||
|
||||||
|
editingElement.type !== "text" ||
|
||||||
|
element.id !== editingElement.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return memoize(
|
||||||
|
({
|
||||||
|
zoom,
|
||||||
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
editingElement,
|
||||||
|
pendingImageElementId,
|
||||||
|
// unused but serves we cache on it to invalidate elements if they
|
||||||
|
// get mutated
|
||||||
|
versionNonce: _versionNonce,
|
||||||
|
}: {
|
||||||
|
zoom: AppState["zoom"];
|
||||||
|
offsetLeft: AppState["offsetLeft"];
|
||||||
|
offsetTop: AppState["offsetTop"];
|
||||||
|
scrollX: AppState["scrollX"];
|
||||||
|
scrollY: AppState["scrollY"];
|
||||||
|
height: AppState["height"];
|
||||||
|
width: AppState["width"];
|
||||||
|
editingElement: AppState["editingElement"];
|
||||||
|
pendingImageElementId: AppState["pendingImageElementId"];
|
||||||
|
versionNonce: ReturnType<InstanceType<typeof Scene>["getVersionNonce"]>;
|
||||||
|
}) => {
|
||||||
|
const elements = this.scene.getNonDeletedElements();
|
||||||
|
|
||||||
|
const canvasElements = getCanvasElements({
|
||||||
|
elements,
|
||||||
|
editingElement,
|
||||||
|
pendingImageElementId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const visibleElements = getVisibleCanvasElements({
|
||||||
|
elements: canvasElements,
|
||||||
|
zoom,
|
||||||
|
offsetLeft,
|
||||||
|
offsetTop,
|
||||||
|
scrollX,
|
||||||
|
scrollY,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { canvasElements, visibleElements };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
|
||||||
|
// NOTE Doesn't destroy everything (scene, rc, etc.) because it may not be
|
||||||
|
// safe to break TS contract here (for upstream cases)
|
||||||
|
public destroy() {
|
||||||
|
cancelRender();
|
||||||
|
this.getRenderableElements.clear();
|
||||||
|
}
|
||||||
|
}
|
@ -14,6 +14,7 @@ import { isFrameElement } from "../element/typeChecks";
|
|||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Assert, SameType } from "../utility-types";
|
import { Assert, SameType } from "../utility-types";
|
||||||
|
import { randomInteger } from "../random";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
@ -105,6 +106,7 @@ class Scene {
|
|||||||
elements: null,
|
elements: null,
|
||||||
cache: new Map(),
|
cache: new Map(),
|
||||||
};
|
};
|
||||||
|
private versionNonce: number | undefined;
|
||||||
|
|
||||||
getElementsIncludingDeleted() {
|
getElementsIncludingDeleted() {
|
||||||
return this.elements;
|
return this.elements;
|
||||||
@ -172,6 +174,10 @@ class Scene {
|
|||||||
return (this.elementsMap.get(id) as T | undefined) || null;
|
return (this.elementsMap.get(id) as T | undefined) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVersionNonce() {
|
||||||
|
return this.versionNonce;
|
||||||
|
}
|
||||||
|
|
||||||
getNonDeletedElement(
|
getNonDeletedElement(
|
||||||
id: ExcalidrawElement["id"],
|
id: ExcalidrawElement["id"],
|
||||||
): NonDeleted<ExcalidrawElement> | null {
|
): NonDeleted<ExcalidrawElement> | null {
|
||||||
@ -230,6 +236,8 @@ class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
informMutation() {
|
informMutation() {
|
||||||
|
this.versionNonce = randomInteger();
|
||||||
|
|
||||||
for (const callback of Array.from(this.callbacks)) {
|
for (const callback of Array.from(this.callbacks)) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
61
src/scene/ShapeCache.ts
Normal file
61
src/scene/ShapeCache.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import { Drawable } from "roughjs/bin/core";
|
||||||
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
import { ExcalidrawElement } from "../element/types";
|
||||||
|
import { generateElementShape } from "../renderer/renderElement";
|
||||||
|
|
||||||
|
type ElementShape = Drawable | Drawable[] | null;
|
||||||
|
|
||||||
|
type ElementShapes = {
|
||||||
|
freedraw: Drawable | null;
|
||||||
|
arrow: Drawable[];
|
||||||
|
line: Drawable[];
|
||||||
|
text: null;
|
||||||
|
image: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ShapeCache {
|
||||||
|
private static rg = new RoughGenerator();
|
||||||
|
private static cache = new WeakMap<ExcalidrawElement, ElementShape>();
|
||||||
|
|
||||||
|
public static get = <T extends ExcalidrawElement>(element: T) => {
|
||||||
|
return ShapeCache.cache.get(
|
||||||
|
element,
|
||||||
|
) as T["type"] extends keyof ElementShapes
|
||||||
|
? ElementShapes[T["type"]] | undefined
|
||||||
|
: Drawable | null | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
public static set = <T extends ExcalidrawElement>(
|
||||||
|
element: T,
|
||||||
|
shape: T["type"] extends keyof ElementShapes
|
||||||
|
? ElementShapes[T["type"]]
|
||||||
|
: Drawable,
|
||||||
|
) => ShapeCache.cache.set(element, shape);
|
||||||
|
|
||||||
|
public static delete = (element: ExcalidrawElement) =>
|
||||||
|
ShapeCache.cache.delete(element);
|
||||||
|
|
||||||
|
public static destroy = () => {
|
||||||
|
ShapeCache.cache = new WeakMap();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates & caches shape for element if not already cached, otherwise
|
||||||
|
* return cached shape.
|
||||||
|
*/
|
||||||
|
public static generateElementShape = <T extends ExcalidrawElement>(
|
||||||
|
element: T,
|
||||||
|
) => {
|
||||||
|
const shape = generateElementShape(
|
||||||
|
element,
|
||||||
|
ShapeCache.rg,
|
||||||
|
/* so it prefers cache */ false,
|
||||||
|
) as T["type"] extends keyof ElementShapes
|
||||||
|
? ElementShapes[T["type"]]
|
||||||
|
: Drawable | null;
|
||||||
|
|
||||||
|
ShapeCache.cache.set(element, shape);
|
||||||
|
|
||||||
|
return shape;
|
||||||
|
};
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||||
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
||||||
@ -54,26 +54,23 @@ export const exportToCanvas = async (
|
|||||||
|
|
||||||
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
||||||
|
|
||||||
renderScene({
|
renderStaticScene({
|
||||||
elements,
|
|
||||||
appState,
|
|
||||||
scale,
|
|
||||||
rc: rough.canvas(canvas),
|
|
||||||
canvas,
|
canvas,
|
||||||
renderConfig: {
|
rc: rough.canvas(canvas),
|
||||||
|
elements,
|
||||||
|
visibleElements: elements,
|
||||||
|
scale,
|
||||||
|
appState: {
|
||||||
|
...appState,
|
||||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||||
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
zoom: defaultAppState.zoom,
|
zoom: defaultAppState.zoom,
|
||||||
remotePointerViewportCoords: {},
|
|
||||||
remoteSelectedElementIds: {},
|
|
||||||
shouldCacheIgnoreZoom: false,
|
shouldCacheIgnoreZoom: false,
|
||||||
remotePointerUsernames: {},
|
|
||||||
remotePointerUserStates: {},
|
|
||||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||||
|
},
|
||||||
|
renderConfig: {
|
||||||
imageCache,
|
imageCache,
|
||||||
renderScrollbars: false,
|
|
||||||
renderSelection: false,
|
|
||||||
renderGrid: false,
|
renderGrid: false,
|
||||||
isExporting: true,
|
isExporting: true,
|
||||||
},
|
},
|
||||||
|
@ -11,11 +11,7 @@ import {
|
|||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
const isOutsideViewPort = (
|
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
|
||||||
appState: AppState,
|
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
cords: Array<number>,
|
|
||||||
) => {
|
|
||||||
const [x1, y1, x2, y2] = cords;
|
const [x1, y1, x2, y2] = cords;
|
||||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: x1, sceneY: y1 },
|
{ sceneX: x1, sceneY: y1 },
|
||||||
@ -49,7 +45,6 @@ export const centerScrollOn = ({
|
|||||||
export const calculateScrollCenter = (
|
export const calculateScrollCenter = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
): { scrollX: number; scrollY: number } => {
|
): { scrollX: number; scrollY: number } => {
|
||||||
elements = getVisibleElements(elements);
|
elements = getVisibleElements(elements);
|
||||||
|
|
||||||
@ -61,7 +56,7 @@ export const calculateScrollCenter = (
|
|||||||
}
|
}
|
||||||
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
|
|
||||||
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
if (isOutsideViewPort(appState, [x1, y1, x2, y2])) {
|
||||||
[x1, y1, x2, y2] = getClosestElementBounds(
|
[x1, y1, x2, y2] = getClosestElementBounds(
|
||||||
elements,
|
elements,
|
||||||
viewportCoordsToSceneCoords(
|
viewportCoordsToSceneCoords(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
import { Zoom } from "../types";
|
import { InteractiveCanvasAppState } from "../types";
|
||||||
import { ScrollBars } from "./types";
|
import { ScrollBars } from "./types";
|
||||||
import { getGlobalCSSVariable } from "../utils";
|
import { getGlobalCSSVariable } from "../utils";
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
@ -13,15 +13,7 @@ export const getScrollBars = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
viewportWidth: number,
|
viewportWidth: number,
|
||||||
viewportHeight: number,
|
viewportHeight: number,
|
||||||
{
|
appState: InteractiveCanvasAppState,
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
zoom,
|
|
||||||
}: {
|
|
||||||
scrollX: number;
|
|
||||||
scrollY: number;
|
|
||||||
zoom: Zoom;
|
|
||||||
},
|
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
return {
|
return {
|
||||||
@ -34,8 +26,8 @@ export const getScrollBars = (
|
|||||||
getCommonBounds(elements);
|
getCommonBounds(elements);
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
const viewportWidthWithZoom = viewportWidth / zoom.value;
|
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||||
const viewportHeightWithZoom = viewportHeight / zoom.value;
|
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||||
|
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||||
@ -50,8 +42,10 @@ export const getScrollBars = (
|
|||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
// The viewport is the rectangle currently visible for the user
|
// The viewport is the rectangle currently visible for the user
|
||||||
const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left;
|
const viewportMinX =
|
||||||
const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top;
|
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||||
|
const viewportMinY =
|
||||||
|
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppState, InteractiveCanvasAppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
@ -146,7 +146,7 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
|||||||
|
|
||||||
export const getSelectedElements = (
|
export const getSelectedElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Pick<AppState, "selectedElementIds">,
|
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||||
opts?: {
|
opts?: {
|
||||||
includeBoundTextElement?: boolean;
|
includeBoundTextElement?: boolean;
|
||||||
includeElementsInFrames?: boolean;
|
includeElementsInFrames?: boolean;
|
||||||
|
@ -1,33 +1,63 @@
|
|||||||
import { ExcalidrawTextElement } from "../element/types";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import {
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
StaticCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
export type RenderConfig = {
|
export type StaticCanvasRenderConfig = {
|
||||||
// AppState values
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
scrollX: AppState["scrollX"];
|
|
||||||
scrollY: AppState["scrollY"];
|
|
||||||
/** null indicates transparent bg */
|
|
||||||
viewBackgroundColor: AppState["viewBackgroundColor"] | null;
|
|
||||||
zoom: AppState["zoom"];
|
|
||||||
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
|
||||||
theme: AppState["theme"];
|
|
||||||
// collab-related state
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
|
||||||
remotePointerButton?: { [id: string]: string | undefined };
|
|
||||||
remoteSelectedElementIds: { [elementId: string]: string[] };
|
|
||||||
remotePointerUsernames: { [id: string]: string };
|
|
||||||
remotePointerUserStates: { [id: string]: string };
|
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
imageCache: AppClassProperties["imageCache"];
|
imageCache: AppClassProperties["imageCache"];
|
||||||
renderScrollbars?: boolean;
|
renderGrid: boolean;
|
||||||
renderSelection?: boolean;
|
|
||||||
renderGrid?: boolean;
|
|
||||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||||
CSS filters), and we disable render optimizations for best output */
|
CSS filters), and we disable render optimizations for best output */
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveCanvasRenderConfig = {
|
||||||
|
// collab-related state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
remoteSelectedElementIds: { [elementId: string]: string[] };
|
||||||
|
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||||
|
remotePointerUserStates: { [id: string]: string };
|
||||||
|
remotePointerUsernames: { [id: string]: string };
|
||||||
|
remotePointerButton?: { [id: string]: string | undefined };
|
||||||
selectionColor?: string;
|
selectionColor?: string;
|
||||||
|
// extra options passed to the renderer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
renderScrollbars?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderInteractiveSceneCallback = {
|
||||||
|
atLeastOneVisibleElement: boolean;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
scrollBars?: ScrollBars;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StaticSceneRenderConfig = {
|
||||||
|
canvas: HTMLCanvasElement;
|
||||||
|
rc: RoughCanvas;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
scale: number;
|
||||||
|
appState: StaticCanvasAppState;
|
||||||
|
renderConfig: StaticCanvasRenderConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveSceneRenderConfig = {
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
scale: number;
|
||||||
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderConfig: InteractiveCanvasRenderConfig;
|
||||||
|
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SceneScroll = {
|
export type SceneScroll = {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -33,7 +33,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -42,7 +42,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -69,14 +69,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -103,14 +103,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -148,7 +148,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -157,7 +157,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -184,14 +184,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
|
@ -5,7 +5,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
|||||||
class="excalidraw-wysiwyg"
|
class="excalidraw-wysiwyg"
|
||||||
data-type="wysiwyg"
|
data-type="wysiwyg"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: -7.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
style="position: absolute; display: inline-block; min-height: 1em; backface-visibility: hidden; margin: 0px; padding: 0px; border: 0px; outline: 0; resize: none; background: transparent; overflow: hidden; z-index: var(--zIndex-wysiwyg); word-break: break-word; white-space: pre-wrap; overflow-wrap: break-word; box-sizing: content-box; width: 10.5px; height: 25px; left: 35px; top: 7.5px; transform: translate(0px, 0px) scale(1) rotate(0deg); text-align: center; vertical-align: middle; color: rgb(30, 30, 30); opacity: 1; filter: var(--theme-filter); max-height: 992.5px; font: Emoji 20px 20px; line-height: 1.25; font-family: Virgil, Segoe UI Emoji;"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
wrap="off"
|
wrap="off"
|
||||||
/>
|
/>
|
||||||
|
@ -18,14 +18,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 238820263,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -50,14 +50,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1604849351,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
@ -82,14 +82,14 @@ exports[`move element > rectangle 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 1150084233,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 40,
|
"y": 40,
|
||||||
@ -119,14 +119,14 @@ exports[`move element > rectangles with binding arrow 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 81784553,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -156,14 +156,14 @@ exports[`move element > rectangles with binding arrow 2`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 2019559783,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 6,
|
||||||
"versionNonce": 1723083209,
|
"versionNonce": 927333447,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@ -205,7 +205,7 @@ exports[`move element > rectangles with binding arrow 3`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 238820263,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": {
|
"startBinding": {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
@ -218,7 +218,7 @@ exports[`move element > rectangles with binding arrow 3`] = `
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1006504105,
|
"versionNonce": 1051383431,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": 49.981789081137734,
|
"y": 49.981789081137734,
|
||||||
|
@ -38,7 +38,7 @@ exports[`multi point mode in linear elements > arrow 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -47,7 +47,7 @@ exports[`multi point mode in linear elements > arrow 1`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1505387817,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -92,7 +92,7 @@ exports[`multi point mode in linear elements > line 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -101,7 +101,7 @@ exports[`multi point mode in linear elements > line 1`] = `
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1505387817,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,7 @@ exports[`select single element on the scene > arrow 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -40,7 +40,7 @@ exports[`select single element on the scene > arrow 1`] = `
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -78,7 +78,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -87,7 +87,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -112,14 +112,14 @@ exports[`select single element on the scene > diamond 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -144,14 +144,14 @@ exports[`select single element on the scene > ellipse 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -176,14 +176,14 @@ exports[`select single element on the scene > rectangle 1`] = `
|
|||||||
"roundness": {
|
"roundness": {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
|
@ -24,7 +24,7 @@ import { LibraryItem } from "../types";
|
|||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
const checkpoint = (name: string) => {
|
const checkpoint = (name: string) => {
|
||||||
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||||
`[${name}] number of renders`,
|
`[${name}] number of renders`,
|
||||||
);
|
);
|
||||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||||
@ -40,10 +40,10 @@ const mouse = new Pointer("mouse");
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,7 +52,7 @@ const { h } = window;
|
|||||||
describe("contextMenu element", () => {
|
describe("contextMenu element", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
setDateTimeForTests("201933152653");
|
setDateTimeForTests("201933152653");
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ describe("contextMenu element", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shows context menu for canvas", () => {
|
it("shows context menu for canvas", () => {
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -105,7 +105,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -159,7 +159,7 @@ describe("contextMenu element", () => {
|
|||||||
API.setSelectedElements([rect1]);
|
API.setSelectedElements([rect1]);
|
||||||
|
|
||||||
// lower z-index
|
// lower z-index
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
@ -169,7 +169,7 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
// higher z-index
|
// higher z-index
|
||||||
API.setSelectedElements([rect2]);
|
API.setSelectedElements([rect2]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
@ -193,7 +193,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.click(20, 0);
|
mouse.click(20, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -246,7 +246,7 @@ describe("contextMenu element", () => {
|
|||||||
Keyboard.keyPress(KEYS.G);
|
Keyboard.keyPress(KEYS.G);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -285,7 +285,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -333,7 +333,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.reset();
|
mouse.reset();
|
||||||
|
|
||||||
// Copy styles of second rectangle
|
// Copy styles of second rectangle
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 40,
|
||||||
clientY: 40,
|
clientY: 40,
|
||||||
@ -346,7 +346,7 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
// Paste styles to first rectangle
|
// Paste styles to first rectangle
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 10,
|
||||||
clientY: 10,
|
clientY: 10,
|
||||||
@ -370,7 +370,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -386,7 +386,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -407,7 +407,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -430,7 +430,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 40,
|
||||||
clientY: 40,
|
clientY: 40,
|
||||||
@ -452,7 +452,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 10,
|
||||||
clientY: 10,
|
clientY: 10,
|
||||||
@ -474,7 +474,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 40,
|
||||||
clientY: 40,
|
clientY: 40,
|
||||||
@ -495,7 +495,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 10,
|
||||||
clientY: 10,
|
clientY: 10,
|
||||||
@ -520,7 +520,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.click(10, 10);
|
mouse.click(10, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -550,7 +550,7 @@ describe("contextMenu element", () => {
|
|||||||
Keyboard.keyPress(KEYS.G);
|
Keyboard.keyPress(KEYS.G);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
|
@ -15,10 +15,13 @@ import { vi } from "vitest";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -32,7 +35,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -43,7 +46,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -63,7 +67,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -74,7 +78,9 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -94,7 +100,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -105,7 +111,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -125,7 +132,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -136,7 +143,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -160,7 +168,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -171,7 +179,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -203,7 +212,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -211,7 +220,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -222,7 +232,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -230,7 +240,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -241,7 +252,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -249,7 +260,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -260,7 +272,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -273,7 +285,8 @@ describe("Test dragCreate", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -284,7 +297,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -297,7 +310,8 @@ describe("Test dragCreate", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
@ -279,7 +279,7 @@ export class API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static drop = async (blob: Blob) => {
|
static drop = async (blob: Blob) => {
|
||||||
const fileDropEvent = createEvent.drop(GlobalTestState.canvas);
|
const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas);
|
||||||
const text = await new Promise<string>((resolve, reject) => {
|
const text = await new Promise<string>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@ -306,6 +306,6 @@ export class API {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
fireEvent(GlobalTestState.canvas, fileDropEvent);
|
fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ export class Pointer {
|
|||||||
restorePosition(x = 0, y = 0) {
|
restorePosition(x = 0, y = 0) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEvent() {
|
private getEvent() {
|
||||||
@ -129,18 +129,18 @@ export class Pointer {
|
|||||||
if (dx !== 0 || dy !== 0) {
|
if (dx !== 0 || dy !== 0) {
|
||||||
this.clientX += dx;
|
this.clientX += dx;
|
||||||
this.clientY += dy;
|
this.clientY += dy;
|
||||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
down(dx = 0, dy = 0) {
|
down(dx = 0, dy = 0) {
|
||||||
this.move(dx, dy);
|
this.move(dx, dy);
|
||||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
up(dx = 0, dy = 0) {
|
up(dx = 0, dy = 0) {
|
||||||
this.move(dx, dy);
|
this.move(dx, dy);
|
||||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
click(dx = 0, dy = 0) {
|
click(dx = 0, dy = 0) {
|
||||||
@ -150,7 +150,7 @@ export class Pointer {
|
|||||||
|
|
||||||
doubleClick(dx = 0, dy = 0) {
|
doubleClick(dx = 0, dy = 0) {
|
||||||
this.move(dx, dy);
|
this.move(dx, dy);
|
||||||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// absolute coords
|
// absolute coords
|
||||||
@ -159,19 +159,19 @@ export class Pointer {
|
|||||||
moveTo(x: number = this.clientX, y: number = this.clientY) {
|
moveTo(x: number = this.clientX, y: number = this.clientY) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
downAt(x = this.clientX, y = this.clientY) {
|
downAt(x = this.clientX, y = this.clientY) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
upAt(x = this.clientX, y = this.clientY) {
|
upAt(x = this.clientX, y = this.clientY) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
clickAt(x: number, y: number) {
|
clickAt(x: number, y: number) {
|
||||||
@ -180,7 +180,7 @@ export class Pointer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rightClickAt(x: number, y: number) {
|
rightClickAt(x: number, y: number) {
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: x,
|
clientX: x,
|
||||||
clientY: y,
|
clientY: y,
|
||||||
@ -189,7 +189,7 @@ export class Pointer {
|
|||||||
|
|
||||||
doubleClickAt(x: number, y: number) {
|
doubleClickAt(x: number, y: number) {
|
||||||
this.moveTo(x, y);
|
this.moveTo(x, y);
|
||||||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -327,6 +327,13 @@ export class UI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ungroup(elements: ExcalidrawElement[]) {
|
||||||
|
mouse.select(elements);
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.G);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static queryContextMenu = () => {
|
static queryContextMenu = () => {
|
||||||
return GlobalTestState.renderResult.container.querySelector(
|
return GlobalTestState.renderResult.container.querySelector(
|
||||||
".context-menu",
|
".context-menu",
|
||||||
|
@ -26,26 +26,28 @@ import * as textElementUtils from "../element/textElement";
|
|||||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
import { vi } from "vitest";
|
import { vi } from "vitest";
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||||
|
|
||||||
describe("Test Linear Elements", () => {
|
describe("Test Linear Elements", () => {
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
let canvas: HTMLCanvasElement;
|
let interactiveCanvas: HTMLCanvasElement;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
const comp = await render(<ExcalidrawApp />);
|
const comp = await render(<ExcalidrawApp />);
|
||||||
|
h.state.width = 1000;
|
||||||
|
h.state.height = 1000;
|
||||||
container = comp.container;
|
container = comp.container;
|
||||||
canvas = container.querySelector("canvas")!;
|
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
||||||
canvas.width = 1000;
|
|
||||||
canvas.height = 1000;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const p1: Point = [20, 20];
|
const p1: Point = [20, 20];
|
||||||
@ -120,26 +122,26 @@ describe("Test Linear Elements", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const drag = (startPoint: Point, endPoint: Point) => {
|
const drag = (startPoint: Point, endPoint: Point) => {
|
||||||
fireEvent.pointerDown(canvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: startPoint[0],
|
clientX: startPoint[0],
|
||||||
clientY: startPoint[1],
|
clientY: startPoint[1],
|
||||||
});
|
});
|
||||||
fireEvent.pointerMove(canvas, {
|
fireEvent.pointerMove(interactiveCanvas, {
|
||||||
clientX: endPoint[0],
|
clientX: endPoint[0],
|
||||||
clientY: endPoint[1],
|
clientY: endPoint[1],
|
||||||
});
|
});
|
||||||
fireEvent.pointerUp(canvas, {
|
fireEvent.pointerUp(interactiveCanvas, {
|
||||||
clientX: endPoint[0],
|
clientX: endPoint[0],
|
||||||
clientY: endPoint[1],
|
clientY: endPoint[1],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePoint = (point: Point) => {
|
const deletePoint = (point: Point) => {
|
||||||
fireEvent.pointerDown(canvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: point[0],
|
clientX: point[0],
|
||||||
clientY: point[1],
|
clientY: point[1],
|
||||||
});
|
});
|
||||||
fireEvent.pointerUp(canvas, {
|
fireEvent.pointerUp(interactiveCanvas, {
|
||||||
clientX: point[0],
|
clientX: point[0],
|
||||||
clientY: point[1],
|
clientY: point[1],
|
||||||
});
|
});
|
||||||
@ -172,12 +174,14 @@ describe("Test Linear Elements", () => {
|
|||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
@ -199,14 +203,14 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
it("should allow entering and exiting line editor via context menu", () => {
|
it("should allow entering and exiting line editor via context menu", () => {
|
||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
});
|
});
|
||||||
// Enter line editor
|
// Enter line editor
|
||||||
let contextMenu = document.querySelector(".context-menu");
|
let contextMenu = document.querySelector(".context-menu");
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
@ -216,13 +220,13 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
|
||||||
// Exiting line editor
|
// Exiting line editor
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
});
|
});
|
||||||
contextMenu = document.querySelector(".context-menu");
|
contextMenu = document.querySelector(".context-menu");
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
@ -270,7 +274,8 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
@ -307,7 +312,9 @@ describe("Test Linear Elements", () => {
|
|||||||
// update roundness
|
// update roundness
|
||||||
fireEvent.click(screen.getByTitle("Round"));
|
fireEvent.click(screen.getByTitle("Round"));
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(12);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||||
h.elements[0] as ExcalidrawLinearElement,
|
h.elements[0] as ExcalidrawLinearElement,
|
||||||
h.state,
|
h.state,
|
||||||
@ -351,7 +358,9 @@ describe("Test Linear Elements", () => {
|
|||||||
// Move the element
|
// Move the element
|
||||||
drag(startPoint, endPoint);
|
drag(startPoint, endPoint);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
|
|
||||||
expect([line.x, line.y]).toEqual([
|
expect([line.x, line.y]).toEqual([
|
||||||
points[0][0] + deltaX,
|
points[0][0] + deltaX,
|
||||||
points[0][1] + deltaY,
|
points[0][1] + deltaY,
|
||||||
@ -408,7 +417,9 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||||
@ -447,7 +458,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||||
@ -473,7 +485,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||||
@ -507,7 +520,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// delete 3rd point
|
// delete 3rd point
|
||||||
deletePoint(points[2]);
|
deletePoint(points[2]);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(22);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
|
|
||||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
line,
|
line,
|
||||||
@ -553,8 +567,8 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint[0] + delta,
|
||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||||
@ -629,7 +643,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||||
@ -870,10 +885,10 @@ describe("Test Linear Elements", () => {
|
|||||||
]);
|
]);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
easy"
|
easy"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
|
it("should bind text to arrow when clicked on arrow and enter pressed", async () => {
|
||||||
@ -904,10 +919,10 @@ describe("Test Linear Elements", () => {
|
|||||||
]);
|
]);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made
|
collaboration made
|
||||||
easy"
|
easy"
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not bind text to line when double clicked", async () => {
|
it("should not bind text to line when double clicked", async () => {
|
||||||
@ -1046,9 +1061,9 @@ describe("Test Linear Elements", () => {
|
|||||||
`);
|
`);
|
||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
"Online whiteboard
|
"Online whiteboard
|
||||||
collaboration made easy"
|
collaboration made easy"
|
||||||
`);
|
`);
|
||||||
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
||||||
.toMatchInlineSnapshot(`
|
.toMatchInlineSnapshot(`
|
||||||
[
|
[
|
||||||
@ -1206,7 +1221,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
const container = h.elements[0];
|
const container = h.elements[0];
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1231,7 +1246,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.up();
|
mouse.up();
|
||||||
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
|
@ -17,10 +17,13 @@ import { vi } from "vitest";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -29,7 +32,7 @@ const { h } = window;
|
|||||||
describe("move element", () => {
|
describe("move element", () => {
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
@ -39,20 +42,23 @@ describe("move element", () => {
|
|||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||||
|
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(3);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(2);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
||||||
@ -78,7 +84,8 @@ describe("move element", () => {
|
|||||||
// select the second rectangles
|
// select the second rectangles
|
||||||
new Pointer("mouse").clickOn(rectB);
|
new Pointer("mouse").clickOn(rectB);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(23);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(16);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
@ -87,7 +94,8 @@ describe("move element", () => {
|
|||||||
expect([line.x, line.y]).toEqual([110, 50]);
|
expect([line.x, line.y]).toEqual([110, 50]);
|
||||||
expect([line.width, line.height]).toEqual([80, 80]);
|
expect([line.width, line.height]).toEqual([80, 80]);
|
||||||
|
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
|
|
||||||
// Move selected rectangle
|
// Move selected rectangle
|
||||||
Keyboard.keyDown(KEYS.ARROW_RIGHT);
|
Keyboard.keyDown(KEYS.ARROW_RIGHT);
|
||||||
@ -95,7 +103,8 @@ describe("move element", () => {
|
|||||||
Keyboard.keyDown(KEYS.ARROW_DOWN);
|
Keyboard.keyDown(KEYS.ARROW_DOWN);
|
||||||
|
|
||||||
// Check that the arrow size has been changed according to moving the rectangle
|
// Check that the arrow size has been changed according to moving the rectangle
|
||||||
expect(renderScene).toHaveBeenCalledTimes(3);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
@ -111,7 +120,7 @@ describe("move element", () => {
|
|||||||
describe("duplicate element on move when ALT is clicked", () => {
|
describe("duplicate element on move when ALT is clicked", () => {
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
@ -121,13 +130,15 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||||
|
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
||||||
@ -141,7 +152,8 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
|
|
||||||
// TODO: This used to be 4, but binding made it go up to 5. Do we need
|
// TODO: This used to be 4, but binding made it go up to 5. Do we need
|
||||||
// that additional render?
|
// that additional render?
|
||||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
|
|
||||||
|
@ -15,10 +15,13 @@ import { vi } from "vitest";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -39,11 +42,12 @@ describe("remove shape in non linear elements", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -53,11 +57,12 @@ describe("remove shape in non linear elements", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -67,11 +72,12 @@ describe("remove shape in non linear elements", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -83,7 +89,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
// first point is added on pointer down
|
// first point is added on pointer down
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
@ -103,7 +109,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
@ -126,7 +133,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
// first point is added on pointer down
|
// first point is added on pointer down
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
@ -146,7 +153,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils";
|
import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils";
|
||||||
import { Excalidraw, Footer, MainMenu } from "../../packages/excalidraw/index";
|
import { Excalidraw, Footer, MainMenu } from "../../packages/excalidraw/index";
|
||||||
import { queryByText, queryByTestId, screen } from "@testing-library/react";
|
import { queryByText, queryByTestId } from "@testing-library/react";
|
||||||
import { GRID_SIZE, THEME } from "../../constants";
|
import { GRID_SIZE, THEME } from "../../constants";
|
||||||
import { t } from "../../i18n";
|
import { t } from "../../i18n";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -23,7 +23,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
).toBe(0);
|
).toBe(0);
|
||||||
expect(h.state.zenModeEnabled).toBe(false);
|
expect(h.state.zenModeEnabled).toBe(false);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -42,8 +42,8 @@ describe("<Excalidraw/>", () => {
|
|||||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||||
).toBe(0);
|
).toBe(0);
|
||||||
expect(h.state.zenModeEnabled).toBe(true);
|
expect(h.state.zenModeEnabled).toBe(true);
|
||||||
screen.debug();
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -95,7 +95,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
expect(
|
expect(
|
||||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||||
).toBe(0);
|
).toBe(0);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -114,7 +114,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
expect(
|
expect(
|
||||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||||
).toBe(0);
|
).toBe(0);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
|
@ -21,7 +21,7 @@ import { vi } from "vitest";
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
const finger1 = new Pointer("touch", 1);
|
const finger1 = new Pointer("touch", 1);
|
||||||
@ -33,7 +33,7 @@ const finger2 = new Pointer("touch", 2);
|
|||||||
* to debug where a test failure came from.
|
* to debug where a test failure came from.
|
||||||
*/
|
*/
|
||||||
const checkpoint = (name: string) => {
|
const checkpoint = (name: string) => {
|
||||||
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||||
`[${name}] number of renders`,
|
`[${name}] number of renders`,
|
||||||
);
|
);
|
||||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||||
@ -48,7 +48,7 @@ beforeEach(async () => {
|
|||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
setDateTimeForTests("201933152653");
|
setDateTimeForTests("201933152653");
|
||||||
|
|
||||||
@ -1056,6 +1056,28 @@ describe("regression tests", () => {
|
|||||||
expect(API.getSelectedElements()).toEqual(selectedElements_prev);
|
expect(API.getSelectedElements()).toEqual(selectedElements_prev);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("deleting last but one element in editing group should unselect the group", () => {
|
||||||
|
const rect1 = UI.createElement("rectangle", { x: 10 });
|
||||||
|
const rect2 = UI.createElement("rectangle", { x: 50 });
|
||||||
|
|
||||||
|
UI.group([rect1, rect2]);
|
||||||
|
|
||||||
|
mouse.doubleClickOn(rect1);
|
||||||
|
Keyboard.keyDown(KEYS.DELETE);
|
||||||
|
|
||||||
|
// Clicking on the deleted element, hence in the empty space
|
||||||
|
mouse.clickOn(rect1);
|
||||||
|
|
||||||
|
expect(h.state.selectedGroupIds).toEqual({});
|
||||||
|
expect(API.getSelectedElements()).toEqual([]);
|
||||||
|
|
||||||
|
// Clicking back in and expecting no group selection
|
||||||
|
mouse.clickOn(rect2);
|
||||||
|
|
||||||
|
expect(h.state.selectedGroupIds).toEqual({ [rect2.groupIds[0]]: false });
|
||||||
|
expect(API.getSelectedElements()).toEqual([rect2.get()]);
|
||||||
|
});
|
||||||
|
|
||||||
it("Cmd/Ctrl-click exclusively select element under pointer", () => {
|
it("Cmd/Ctrl-click exclusively select element under pointer", () => {
|
||||||
const rect1 = UI.createElement("rectangle", { x: 0 });
|
const rect1 = UI.createElement("rectangle", { x: 0 });
|
||||||
const rect2 = UI.createElement("rectangle", { x: 30 });
|
const rect2 = UI.createElement("rectangle", { x: 30 });
|
||||||
|
@ -14,10 +14,11 @@ import { vi } from "vitest";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -18,10 +18,13 @@ import { vi } from "vitest";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = vi.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -201,7 +204,7 @@ describe("inner box-selection", () => {
|
|||||||
});
|
});
|
||||||
h.elements = [rect1, rect2, rect3];
|
h.elements = [rect1, rect2, rect3];
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.downAt(rect2.x - 20, rect2.x - 20);
|
mouse.downAt(rect2.x - 20, rect2.y - 20);
|
||||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||||
assertSelectedElements([rect2.id, rect3.id]);
|
assertSelectedElements([rect2.id, rect3.id]);
|
||||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||||
@ -220,10 +223,11 @@ describe("selection element", () => {
|
|||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
const selectionElement = h.state.selectionElement!;
|
const selectionElement = h.state.selectionElement!;
|
||||||
expect(selectionElement).not.toBeNull();
|
expect(selectionElement).not.toBeNull();
|
||||||
expect(selectionElement.type).toEqual("selection");
|
expect(selectionElement.type).toEqual("selection");
|
||||||
@ -240,11 +244,12 @@ describe("selection element", () => {
|
|||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(4);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
const selectionElement = h.state.selectionElement!;
|
const selectionElement = h.state.selectionElement!;
|
||||||
expect(selectionElement).not.toBeNull();
|
expect(selectionElement).not.toBeNull();
|
||||||
expect(selectionElement.type).toEqual("selection");
|
expect(selectionElement.type).toEqual("selection");
|
||||||
@ -261,12 +266,13 @@ describe("selection element", () => {
|
|||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -282,7 +288,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
@ -301,7 +307,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -311,7 +318,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("diamond", async () => {
|
it("diamond", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
@ -330,7 +337,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -340,7 +348,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("ellipse", async () => {
|
it("ellipse", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
@ -359,7 +367,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -369,7 +378,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("arrow", async () => {
|
it("arrow", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
@ -401,7 +410,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -410,7 +420,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("arrow escape", async () => {
|
it("arrow escape", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
@ -442,7 +452,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
@ -49,15 +49,30 @@ const renderApp: TestRenderFn = async (ui, options) => {
|
|||||||
// child App component isn't likely mounted yet (and thus canvas not
|
// child App component isn't likely mounted yet (and thus canvas not
|
||||||
// present in DOM)
|
// present in DOM)
|
||||||
get() {
|
get() {
|
||||||
return renderResult.container.querySelector("canvas")!;
|
return renderResult.container.querySelector("canvas.static")!;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(GlobalTestState, "interactiveCanvas", {
|
||||||
|
// must be a getter because at the time of ExcalidrawApp render the
|
||||||
|
// child App component isn't likely mounted yet (and thus canvas not
|
||||||
|
// present in DOM)
|
||||||
|
get() {
|
||||||
|
return renderResult.container.querySelector("canvas.interactive")!;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const canvas = renderResult.container.querySelector("canvas");
|
const canvas = renderResult.container.querySelector("canvas.static");
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
throw new Error("not initialized yet");
|
throw new Error("not initialized yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const interactiveCanvas =
|
||||||
|
renderResult.container.querySelector("canvas.interactive");
|
||||||
|
if (!interactiveCanvas) {
|
||||||
|
throw new Error("not initialized yet");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return renderResult;
|
return renderResult;
|
||||||
@ -81,11 +96,17 @@ export class GlobalTestState {
|
|||||||
*/
|
*/
|
||||||
static renderResult: RenderResult<typeof customQueries> = null!;
|
static renderResult: RenderResult<typeof customQueries> = null!;
|
||||||
/**
|
/**
|
||||||
* retrieves canvas for currently rendered app instance
|
* retrieves static canvas for currently rendered app instance
|
||||||
*/
|
*/
|
||||||
static get canvas(): HTMLCanvasElement {
|
static get canvas(): HTMLCanvasElement {
|
||||||
return null!;
|
return null!;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* retrieves interactive canvas for currently rendered app instance
|
||||||
|
*/
|
||||||
|
static get interactiveCanvas(): HTMLCanvasElement {
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initLocalStorage = (data: ImportedDataState) => {
|
const initLocalStorage = (data: ImportedDataState) => {
|
||||||
|
@ -17,7 +17,9 @@ describe("view mode", () => {
|
|||||||
|
|
||||||
it("after switching to view mode – cursor type should be pointer", async () => {
|
it("after switching to view mode – cursor type should be pointer", async () => {
|
||||||
h.setState({ viewModeEnabled: true });
|
h.setState({ viewModeEnabled: true });
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => {
|
it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => {
|
||||||
@ -29,7 +31,9 @@ describe("view mode", () => {
|
|||||||
pointer.move(100, 100);
|
pointer.move(100, 100);
|
||||||
pointer.click();
|
pointer.click();
|
||||||
Keyboard.keyPress(KEYS.SPACE);
|
Keyboard.keyPress(KEYS.SPACE);
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,13 +49,19 @@ describe("view mode", () => {
|
|||||||
pointer.moveTo(50, 50);
|
pointer.moveTo(50, 50);
|
||||||
// eslint-disable-next-line dot-notation
|
// eslint-disable-next-line dot-notation
|
||||||
if (pointerType["pointerType"] === "mouse") {
|
if (pointerType["pointerType"] === "mouse") {
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.MOVE);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.MOVE,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
h.setState({ viewModeEnabled: true });
|
h.setState({ viewModeEnabled: true });
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -94,7 +94,7 @@ const populateElements = (
|
|||||||
),
|
),
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
});
|
} as AppState);
|
||||||
|
|
||||||
return selectedElementIds;
|
return selectedElementIds;
|
||||||
};
|
};
|
||||||
|
53
src/types.ts
53
src/types.ts
@ -104,6 +104,52 @@ export type LastActiveTool =
|
|||||||
export type SidebarName = string;
|
export type SidebarName = string;
|
||||||
export type SidebarTabName = string;
|
export type SidebarTabName = string;
|
||||||
|
|
||||||
|
export type CommonCanvasAppState = {
|
||||||
|
zoom: AppState["zoom"];
|
||||||
|
scrollX: AppState["scrollX"];
|
||||||
|
scrollY: AppState["scrollY"];
|
||||||
|
width: AppState["width"];
|
||||||
|
height: AppState["height"];
|
||||||
|
viewModeEnabled: AppState["viewModeEnabled"];
|
||||||
|
editingElement: AppState["editingElement"];
|
||||||
|
editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible
|
||||||
|
selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible
|
||||||
|
frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible
|
||||||
|
offsetLeft: AppState["offsetLeft"];
|
||||||
|
offsetTop: AppState["offsetTop"];
|
||||||
|
theme: AppState["theme"];
|
||||||
|
pendingImageElementId: AppState["pendingImageElementId"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StaticCanvasAppState = CommonCanvasAppState & {
|
||||||
|
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||||
|
/** null indicates transparent bg */
|
||||||
|
viewBackgroundColor: AppState["viewBackgroundColor"] | null;
|
||||||
|
exportScale: AppState["exportScale"];
|
||||||
|
selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"];
|
||||||
|
gridSize: AppState["gridSize"];
|
||||||
|
frameRendering: AppState["frameRendering"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveCanvasAppState = CommonCanvasAppState & {
|
||||||
|
// renderInteractiveScene
|
||||||
|
activeEmbeddable: AppState["activeEmbeddable"];
|
||||||
|
editingLinearElement: AppState["editingLinearElement"];
|
||||||
|
selectionElement: AppState["selectionElement"];
|
||||||
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
|
selectedLinearElement: AppState["selectedLinearElement"];
|
||||||
|
multiElement: AppState["multiElement"];
|
||||||
|
isBindingEnabled: AppState["isBindingEnabled"];
|
||||||
|
suggestedBindings: AppState["suggestedBindings"];
|
||||||
|
isRotating: AppState["isRotating"];
|
||||||
|
elementsToHighlight: AppState["elementsToHighlight"];
|
||||||
|
// App
|
||||||
|
openSidebar: AppState["openSidebar"];
|
||||||
|
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
||||||
|
// Collaborators
|
||||||
|
collaborators: AppState["collaborators"];
|
||||||
|
};
|
||||||
|
|
||||||
export type AppState = {
|
export type AppState = {
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
items: ContextMenuItems;
|
items: ContextMenuItems;
|
||||||
@ -407,13 +453,13 @@ export type ExportOpts = {
|
|||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement,
|
||||||
) => void;
|
) => void;
|
||||||
renderCustomUI?: (
|
renderCustomUI?: (
|
||||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: UIAppState,
|
appState: UIAppState,
|
||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
canvas: HTMLCanvasElement | null,
|
canvas: HTMLCanvasElement,
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -458,7 +504,8 @@ export type AppProps = Merge<
|
|||||||
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
|
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
|
||||||
export type AppClassProperties = {
|
export type AppClassProperties = {
|
||||||
props: AppProps;
|
props: AppProps;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement;
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
focusContainer(): void;
|
focusContainer(): void;
|
||||||
library: Library;
|
library: Library;
|
||||||
imageCache: Map<
|
imageCache: Map<
|
||||||
|
103
src/utils.ts
103
src/utils.ts
@ -20,6 +20,7 @@ import { unstable_batchedUpdates } from "react-dom";
|
|||||||
import { SHAPES } from "./shapes";
|
import { SHAPES } from "./shapes";
|
||||||
import { isEraserActive, isHandToolActive } from "./appState";
|
import { isEraserActive, isHandToolActive } from "./appState";
|
||||||
import { ResolutionType } from "./utility-types";
|
import { ResolutionType } from "./utility-types";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
let mockDateTime: string | null = null;
|
let mockDateTime: string | null = null;
|
||||||
|
|
||||||
@ -399,22 +400,25 @@ export const updateActiveTool = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetCursor = (canvas: HTMLCanvasElement | null) => {
|
export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
|
||||||
if (canvas) {
|
if (interactiveCanvas) {
|
||||||
canvas.style.cursor = "";
|
interactiveCanvas.style.cursor = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
|
export const setCursor = (
|
||||||
if (canvas) {
|
interactiveCanvas: HTMLCanvasElement | null,
|
||||||
canvas.style.cursor = cursor;
|
cursor: string,
|
||||||
|
) => {
|
||||||
|
if (interactiveCanvas) {
|
||||||
|
interactiveCanvas.style.cursor = cursor;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let eraserCanvasCache: any;
|
let eraserCanvasCache: any;
|
||||||
let previewDataURL: string;
|
let previewDataURL: string;
|
||||||
export const setEraserCursor = (
|
export const setEraserCursor = (
|
||||||
canvas: HTMLCanvasElement | null,
|
interactiveCanvas: HTMLCanvasElement | null,
|
||||||
theme: AppState["theme"],
|
theme: AppState["theme"],
|
||||||
) => {
|
) => {
|
||||||
const cursorImageSizePx = 20;
|
const cursorImageSizePx = 20;
|
||||||
@ -446,7 +450,7 @@ export const setEraserCursor = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCursor(
|
setCursor(
|
||||||
canvas,
|
interactiveCanvas,
|
||||||
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
||||||
cursorImageSizePx / 2
|
cursorImageSizePx / 2
|
||||||
}, auto`,
|
}, auto`,
|
||||||
@ -454,23 +458,23 @@ export const setEraserCursor = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setCursorForShape = (
|
export const setCursorForShape = (
|
||||||
canvas: HTMLCanvasElement | null,
|
interactiveCanvas: HTMLCanvasElement | null,
|
||||||
appState: Pick<AppState, "activeTool" | "theme">,
|
appState: Pick<AppState, "activeTool" | "theme">,
|
||||||
) => {
|
) => {
|
||||||
if (!canvas) {
|
if (!interactiveCanvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (appState.activeTool.type === "selection") {
|
if (appState.activeTool.type === "selection") {
|
||||||
resetCursor(canvas);
|
resetCursor(interactiveCanvas);
|
||||||
} else if (isHandToolActive(appState)) {
|
} else if (isHandToolActive(appState)) {
|
||||||
canvas.style.cursor = CURSOR_TYPE.GRAB;
|
interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
|
||||||
} else if (isEraserActive(appState)) {
|
} else if (isEraserActive(appState)) {
|
||||||
setEraserCursor(canvas, appState.theme);
|
setEraserCursor(interactiveCanvas, appState.theme);
|
||||||
// do nothing if image tool is selected which suggests there's
|
// do nothing if image tool is selected which suggests there's
|
||||||
// a image-preview set as the cursor
|
// a image-preview set as the cursor
|
||||||
// Ignore custom type as well and let host decide
|
// Ignore custom type as well and let host decide
|
||||||
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
||||||
canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -927,3 +931,74 @@ export const assertNever = (
|
|||||||
|
|
||||||
throw new Error(message);
|
throw new Error(message);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Memoizes on values of `opts` object (strict equality).
|
||||||
|
*/
|
||||||
|
export const memoize = <T extends Record<string, any>, R extends any>(
|
||||||
|
func: (opts: T) => R,
|
||||||
|
) => {
|
||||||
|
let lastArgs: Map<string, any> | undefined;
|
||||||
|
let lastResult: R | undefined;
|
||||||
|
|
||||||
|
const ret = function (opts: T) {
|
||||||
|
const currentArgs = Object.entries(opts);
|
||||||
|
|
||||||
|
if (lastArgs) {
|
||||||
|
let argsAreEqual = true;
|
||||||
|
for (const [key, value] of currentArgs) {
|
||||||
|
if (lastArgs.get(key) !== value) {
|
||||||
|
argsAreEqual = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (argsAreEqual) {
|
||||||
|
return lastResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = func(opts);
|
||||||
|
|
||||||
|
lastArgs = new Map(currentArgs);
|
||||||
|
lastResult = result;
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
ret.clear = () => {
|
||||||
|
lastArgs = undefined;
|
||||||
|
lastResult = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ret as typeof func & { clear: () => void };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isRenderThrottlingEnabled = (() => {
|
||||||
|
// we don't want to throttle in react < 18 because of #5439 and it was
|
||||||
|
// getting more complex to maintain the fix
|
||||||
|
let IS_REACT_18_AND_UP: boolean;
|
||||||
|
try {
|
||||||
|
const version = React.version.split(".");
|
||||||
|
IS_REACT_18_AND_UP = Number(version[0]) > 17;
|
||||||
|
} catch {
|
||||||
|
IS_REACT_18_AND_UP = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let hasWarned = false;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (window.EXCALIDRAW_THROTTLE_RENDER === true) {
|
||||||
|
if (!IS_REACT_18_AND_UP) {
|
||||||
|
if (!hasWarned) {
|
||||||
|
hasWarned = true;
|
||||||
|
console.warn(
|
||||||
|
"Excalidraw: render throttling is disabled on React versions < 18.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
185
yarn.lock
185
yarn.lock
@ -2813,40 +2813,39 @@
|
|||||||
test-exclude "^6.0.0"
|
test-exclude "^6.0.0"
|
||||||
v8-to-istanbul "^9.1.0"
|
v8-to-istanbul "^9.1.0"
|
||||||
|
|
||||||
"@vitest/expect@0.32.2":
|
"@vitest/expect@0.34.1":
|
||||||
version "0.32.2"
|
version "0.34.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720"
|
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.1.tgz#2ba6cb96695f4b4388c6d955423a81afc79b8da0"
|
||||||
integrity sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==
|
integrity sha512-q2CD8+XIsQ+tHwypnoCk8Mnv5e6afLFvinVGCq3/BOT4kQdVQmY6rRfyKkwcg635lbliLPqbunXZr+L1ssUWiQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/spy" "0.32.2"
|
"@vitest/spy" "0.34.1"
|
||||||
"@vitest/utils" "0.32.2"
|
"@vitest/utils" "0.34.1"
|
||||||
chai "^4.3.7"
|
chai "^4.3.7"
|
||||||
|
|
||||||
"@vitest/runner@0.32.2":
|
"@vitest/runner@0.34.1":
|
||||||
version "0.32.2"
|
version "0.34.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.2.tgz#18dd979ce4e8766bcc90948d11b4c8ae6ed90b89"
|
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.1.tgz#23c21ba1db8bff610988c72744db590d0fb6c4ba"
|
||||||
integrity sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==
|
integrity sha512-YfQMpYzDsYB7yqgmlxZ06NI4LurHWfrH7Wy3Pvf/z/vwUSgq1zLAb1lWcItCzQG+NVox+VvzlKQrYEXb47645g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vitest/utils" "0.32.2"
|
"@vitest/utils" "0.34.1"
|
||||||
concordance "^5.0.4"
|
|
||||||
p-limit "^4.0.0"
|
p-limit "^4.0.0"
|
||||||
pathe "^1.1.0"
|
pathe "^1.1.1"
|
||||||
|
|
||||||
"@vitest/snapshot@0.32.2":
|
"@vitest/snapshot@0.34.1":
|
||||||
version "0.32.2"
|
version "0.34.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.2.tgz#500b6453e88e4c50a0aded39839352c16b519b9e"
|
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.1.tgz#814c65f8e714eaf255f47838541004b2a2ba28e6"
|
||||||
integrity sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==
|
integrity sha512-0O9LfLU0114OqdF8lENlrLsnn024Tb1CsS9UwG0YMWY2oGTQfPtkW+B/7ieyv0X9R2Oijhi3caB1xgGgEgclSQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
magic-string "^0.30.0"
|
magic-string "^0.30.1"
|
||||||
pathe "^1.1.0"
|
pathe "^1.1.1"
|
||||||
pretty-format "^27.5.1"
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
"@vitest/spy@0.32.2":
|
"@vitest/spy@0.34.1":
|
||||||
version "0.32.2"
|
version "0.34.1"
|
||||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.2.tgz#f3ef7afe0d34e863b90df7c959fa5af540a6aaf9"
|
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.1.tgz#2f77234a3d554c5dea664943f2caaab92d304f3c"
|
||||||
integrity sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==
|
integrity sha512-UT4WcI3EAPUNO8n6y9QoEqynGGEPmmRxC+cLzneFFXpmacivjHZsNbiKD88KUScv5DCHVDgdBsLD7O7s1enFcQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
tinyspy "^2.1.0"
|
tinyspy "^2.1.1"
|
||||||
|
|
||||||
"@vitest/ui@0.32.2":
|
"@vitest/ui@0.32.2":
|
||||||
version "0.32.2"
|
version "0.32.2"
|
||||||
@ -2870,6 +2869,15 @@
|
|||||||
loupe "^2.3.6"
|
loupe "^2.3.6"
|
||||||
pretty-format "^27.5.1"
|
pretty-format "^27.5.1"
|
||||||
|
|
||||||
|
"@vitest/utils@0.34.1":
|
||||||
|
version "0.34.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/@vitest/utils/-/utils-0.34.1.tgz#e5545c6618775fb9a2dae2a80d94fc2f35222233"
|
||||||
|
integrity sha512-/ql9dsFi4iuEbiNcjNHQWXBum7aL8pyhxvfnD9gNtbjR9fUKAjxhj4AA3yfLXg6gJpMGGecvtF8Au2G9y3q47Q==
|
||||||
|
dependencies:
|
||||||
|
diff-sequences "^29.4.3"
|
||||||
|
loupe "^2.3.6"
|
||||||
|
pretty-format "^29.5.0"
|
||||||
|
|
||||||
abab@^2.0.6:
|
abab@^2.0.6:
|
||||||
version "2.0.6"
|
version "2.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||||
@ -3236,11 +3244,6 @@ blob@0.0.5:
|
|||||||
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
|
resolved "https://registry.yarnpkg.com/blob/-/blob-0.0.5.tgz#d680eeef25f8cd91ad533f5b01eed48e64caf683"
|
||||||
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
|
integrity sha512-gaqbzQPqOoamawKg0LGVd7SzLgXS+JH61oWprSLH+P+abTczqJbhTR8CmJ2u9/bUYNmHTGJx/UEmn6doAvvuig==
|
||||||
|
|
||||||
blueimp-md5@^2.10.0:
|
|
||||||
version "2.19.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/blueimp-md5/-/blueimp-md5-2.19.0.tgz#b53feea5498dcb53dc6ec4b823adb84b729c4af0"
|
|
||||||
integrity sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==
|
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.11"
|
version "1.1.11"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||||
@ -3510,20 +3513,6 @@ concat-map@0.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||||
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
|
||||||
|
|
||||||
concordance@^5.0.4:
|
|
||||||
version "5.0.4"
|
|
||||||
resolved "https://registry.yarnpkg.com/concordance/-/concordance-5.0.4.tgz#9896073261adced72f88d60e4d56f8efc4bbbbd2"
|
|
||||||
integrity sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==
|
|
||||||
dependencies:
|
|
||||||
date-time "^3.1.0"
|
|
||||||
esutils "^2.0.3"
|
|
||||||
fast-diff "^1.2.0"
|
|
||||||
js-string-escape "^1.0.1"
|
|
||||||
lodash "^4.17.15"
|
|
||||||
md5-hex "^3.0.1"
|
|
||||||
semver "^7.3.2"
|
|
||||||
well-known-symbols "^2.0.0"
|
|
||||||
|
|
||||||
confusing-browser-globals@^1.0.11:
|
confusing-browser-globals@^1.0.11:
|
||||||
version "1.0.11"
|
version "1.0.11"
|
||||||
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
|
resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81"
|
||||||
@ -3638,13 +3627,6 @@ data-urls@^4.0.0:
|
|||||||
whatwg-mimetype "^3.0.0"
|
whatwg-mimetype "^3.0.0"
|
||||||
whatwg-url "^12.0.0"
|
whatwg-url "^12.0.0"
|
||||||
|
|
||||||
date-time@^3.1.0:
|
|
||||||
version "3.1.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/date-time/-/date-time-3.1.0.tgz#0d1e934d170579f481ed8df1e2b8ff70ee845e1e"
|
|
||||||
integrity sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==
|
|
||||||
dependencies:
|
|
||||||
time-zone "^1.0.0"
|
|
||||||
|
|
||||||
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
|
debug@4, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.3.3, debug@^4.3.4:
|
||||||
version "4.3.4"
|
version "4.3.4"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||||
@ -4294,7 +4276,7 @@ estree-walker@^2.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac"
|
||||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||||
|
|
||||||
esutils@^2.0.2, esutils@^2.0.3:
|
esutils@^2.0.2:
|
||||||
version "2.0.3"
|
version "2.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||||
@ -4347,11 +4329,6 @@ fast-diff@^1.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
|
||||||
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
|
||||||
|
|
||||||
fast-diff@^1.2.0:
|
|
||||||
version "1.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.3.0.tgz#ece407fa550a64d638536cd727e129c61616e0f0"
|
|
||||||
integrity sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==
|
|
||||||
|
|
||||||
fast-glob@^3.2.12, fast-glob@^3.2.9:
|
fast-glob@^3.2.12, fast-glob@^3.2.9:
|
||||||
version "3.2.12"
|
version "3.2.12"
|
||||||
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80"
|
||||||
@ -5245,11 +5222,6 @@ jotai@1.13.1:
|
|||||||
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.13.1.tgz#20cc46454cbb39096b12fddfa635b873b3668236"
|
resolved "https://registry.yarnpkg.com/jotai/-/jotai-1.13.1.tgz#20cc46454cbb39096b12fddfa635b873b3668236"
|
||||||
integrity sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==
|
integrity sha512-RUmH1S4vLsG3V6fbGlKzGJnLrDcC/HNb5gH2AeA9DzuJknoVxSGvvg8OBB7lke+gDc4oXmdVsaKn/xDUhWZ0vw==
|
||||||
|
|
||||||
js-string-escape@^1.0.1:
|
|
||||||
version "1.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/js-string-escape/-/js-string-escape-1.0.1.tgz#e2625badbc0d67c7533e9edc1068c587ae4137ef"
|
|
||||||
integrity sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==
|
|
||||||
|
|
||||||
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
|
||||||
version "4.0.0"
|
version "4.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||||
@ -5561,13 +5533,6 @@ magic-string@^0.27.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.13"
|
"@jridgewell/sourcemap-codec" "^1.4.13"
|
||||||
|
|
||||||
magic-string@^0.30.0:
|
|
||||||
version "0.30.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.0.tgz#fd58a4748c5c4547338a424e90fa5dd17f4de529"
|
|
||||||
integrity sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==
|
|
||||||
dependencies:
|
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.13"
|
|
||||||
|
|
||||||
magic-string@^0.30.1:
|
magic-string@^0.30.1:
|
||||||
version "0.30.2"
|
version "0.30.2"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.2.tgz#dcf04aad3d0d1314bc743d076c50feb29b3c7aca"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.2.tgz#dcf04aad3d0d1314bc743d076c50feb29b3c7aca"
|
||||||
@ -5582,13 +5547,6 @@ make-dir@^4.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
semver "^7.5.3"
|
semver "^7.5.3"
|
||||||
|
|
||||||
md5-hex@^3.0.1:
|
|
||||||
version "3.0.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/md5-hex/-/md5-hex-3.0.1.tgz#be3741b510591434b2784d79e556eefc2c9a8e5c"
|
|
||||||
integrity sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==
|
|
||||||
dependencies:
|
|
||||||
blueimp-md5 "^2.10.0"
|
|
||||||
|
|
||||||
merge-stream@^2.0.0:
|
merge-stream@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||||
@ -5660,7 +5618,7 @@ mkdirp@^0.5.6:
|
|||||||
dependencies:
|
dependencies:
|
||||||
minimist "^1.2.6"
|
minimist "^1.2.6"
|
||||||
|
|
||||||
mlly@^1.2.0:
|
mlly@^1.2.0, mlly@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b"
|
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b"
|
||||||
integrity sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==
|
integrity sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==
|
||||||
@ -6525,7 +6483,7 @@ semver@^6.1.1, semver@^6.1.2, semver@^6.3.0:
|
|||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
semver@^7.2.1, semver@^7.3.2, semver@^7.3.7:
|
semver@^7.2.1, semver@^7.3.7:
|
||||||
version "7.4.0"
|
version "7.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.4.0.tgz#8481c92feffc531ab1e012a8ffc15bdd3a0f4318"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-7.4.0.tgz#8481c92feffc531ab1e012a8ffc15bdd3a0f4318"
|
||||||
integrity sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==
|
integrity sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==
|
||||||
@ -6703,7 +6661,7 @@ stackback@0.0.2:
|
|||||||
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
resolved "https://registry.yarnpkg.com/stackback/-/stackback-0.0.2.tgz#1ac8a0d9483848d1695e418b6d031a3c3ce68e3b"
|
||||||
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
|
integrity sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==
|
||||||
|
|
||||||
std-env@^3.3.2, std-env@^3.3.3:
|
std-env@^3.3.3:
|
||||||
version "3.3.3"
|
version "3.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe"
|
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe"
|
||||||
integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==
|
integrity sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==
|
||||||
@ -6930,11 +6888,6 @@ through@^2.3.8:
|
|||||||
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
|
||||||
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==
|
||||||
|
|
||||||
time-zone@^1.0.0:
|
|
||||||
version "1.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/time-zone/-/time-zone-1.0.0.tgz#99c5bf55958966af6d06d83bdf3800dc82faec5d"
|
|
||||||
integrity sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==
|
|
||||||
|
|
||||||
tiny-invariant@^1.1.0:
|
tiny-invariant@^1.1.0:
|
||||||
version "1.3.1"
|
version "1.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
|
resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642"
|
||||||
@ -6945,12 +6898,12 @@ tinybench@^2.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5"
|
resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.5.0.tgz#4711c99bbf6f3e986f67eb722fed9cddb3a68ba5"
|
||||||
integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==
|
integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==
|
||||||
|
|
||||||
tinypool@^0.5.0:
|
tinypool@^0.7.0:
|
||||||
version "0.5.0"
|
version "0.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.5.0.tgz#3861c3069bf71e4f1f5aa2d2e6b3aaacc278961e"
|
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021"
|
||||||
integrity sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==
|
integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==
|
||||||
|
|
||||||
tinyspy@^2.1.0:
|
tinyspy@^2.1.1:
|
||||||
version "2.1.1"
|
version "2.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c"
|
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c"
|
||||||
integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==
|
integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==
|
||||||
@ -7231,15 +7184,15 @@ v8-to-istanbul@^9.1.0:
|
|||||||
"@types/istanbul-lib-coverage" "^2.0.1"
|
"@types/istanbul-lib-coverage" "^2.0.1"
|
||||||
convert-source-map "^1.6.0"
|
convert-source-map "^1.6.0"
|
||||||
|
|
||||||
vite-node@0.32.2:
|
vite-node@0.34.1:
|
||||||
version "0.32.2"
|
version "0.34.1"
|
||||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096"
|
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.1.tgz#144900ca4bd54cc419c501d671350bcbc07eb1ee"
|
||||||
integrity sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==
|
integrity sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w==
|
||||||
dependencies:
|
dependencies:
|
||||||
cac "^6.7.14"
|
cac "^6.7.14"
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
mlly "^1.2.0"
|
mlly "^1.4.0"
|
||||||
pathe "^1.1.0"
|
pathe "^1.1.1"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
vite "^3.0.0 || ^4.0.0"
|
vite "^3.0.0 || ^4.0.0"
|
||||||
|
|
||||||
@ -7321,35 +7274,34 @@ vitest-canvas-mock@0.3.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
jest-canvas-mock "~2.4.0"
|
jest-canvas-mock "~2.4.0"
|
||||||
|
|
||||||
vitest@0.32.2:
|
vitest@0.34.1:
|
||||||
version "0.32.2"
|
version "0.34.1"
|
||||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.2.tgz#758ce2220f609e240ac054eca7ad11a5140679ab"
|
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.1.tgz#3ad7f845e7a9fb0d72ab703cae832a54b8469e1e"
|
||||||
integrity sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==
|
integrity sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/chai" "^4.3.5"
|
"@types/chai" "^4.3.5"
|
||||||
"@types/chai-subset" "^1.3.3"
|
"@types/chai-subset" "^1.3.3"
|
||||||
"@types/node" "*"
|
"@types/node" "*"
|
||||||
"@vitest/expect" "0.32.2"
|
"@vitest/expect" "0.34.1"
|
||||||
"@vitest/runner" "0.32.2"
|
"@vitest/runner" "0.34.1"
|
||||||
"@vitest/snapshot" "0.32.2"
|
"@vitest/snapshot" "0.34.1"
|
||||||
"@vitest/spy" "0.32.2"
|
"@vitest/spy" "0.34.1"
|
||||||
"@vitest/utils" "0.32.2"
|
"@vitest/utils" "0.34.1"
|
||||||
acorn "^8.8.2"
|
acorn "^8.9.0"
|
||||||
acorn-walk "^8.2.0"
|
acorn-walk "^8.2.0"
|
||||||
cac "^6.7.14"
|
cac "^6.7.14"
|
||||||
chai "^4.3.7"
|
chai "^4.3.7"
|
||||||
concordance "^5.0.4"
|
|
||||||
debug "^4.3.4"
|
debug "^4.3.4"
|
||||||
local-pkg "^0.4.3"
|
local-pkg "^0.4.3"
|
||||||
magic-string "^0.30.0"
|
magic-string "^0.30.1"
|
||||||
pathe "^1.1.0"
|
pathe "^1.1.1"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
std-env "^3.3.2"
|
std-env "^3.3.3"
|
||||||
strip-literal "^1.0.1"
|
strip-literal "^1.0.1"
|
||||||
tinybench "^2.5.0"
|
tinybench "^2.5.0"
|
||||||
tinypool "^0.5.0"
|
tinypool "^0.7.0"
|
||||||
vite "^3.0.0 || ^4.0.0"
|
vite "^3.0.0 || ^4.0.0"
|
||||||
vite-node "0.32.2"
|
vite-node "0.34.1"
|
||||||
why-is-node-running "^2.2.2"
|
why-is-node-running "^2.2.2"
|
||||||
|
|
||||||
vscode-jsonrpc@6.0.0:
|
vscode-jsonrpc@6.0.0:
|
||||||
@ -7437,11 +7389,6 @@ webworkify@^1.5.0:
|
|||||||
resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
|
resolved "https://registry.yarnpkg.com/webworkify/-/webworkify-1.5.0.tgz#734ad87a774de6ebdd546e1d3e027da5b8f4a42c"
|
||||||
integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==
|
integrity sha512-AMcUeyXAhbACL8S2hqqdqOLqvJ8ylmIbNwUIqQujRSouf4+eUFaXbG6F1Rbu+srlJMmxQWsiU7mOJi0nMBfM1g==
|
||||||
|
|
||||||
well-known-symbols@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/well-known-symbols/-/well-known-symbols-2.0.0.tgz#e9c7c07dbd132b7b84212c8174391ec1f9871ba5"
|
|
||||||
integrity sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==
|
|
||||||
|
|
||||||
whatwg-encoding@^2.0.0:
|
whatwg-encoding@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
|
resolved "https://registry.yarnpkg.com/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz#e7635f597fd87020858626805a2729fa7698ac53"
|
||||||
@ -7534,9 +7481,9 @@ why-is-node-running@^2.2.2:
|
|||||||
stackback "0.0.2"
|
stackback "0.0.2"
|
||||||
|
|
||||||
word-wrap@^1.2.3:
|
word-wrap@^1.2.3:
|
||||||
version "1.2.5"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||||
|
|
||||||
workbox-background-sync@7.0.0:
|
workbox-background-sync@7.0.0:
|
||||||
version "7.0.0"
|
version "7.0.0"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user