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-pwa": "0.16.4",
|
||||
"vite-plugin-svgr": "2.4.0",
|
||||
"vitest": "0.32.2",
|
||||
"vitest": "0.34.1",
|
||||
"vitest-canvas-mock": "0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -423,7 +423,7 @@ export const actionToggleHandTool = register({
|
||||
type: "hand",
|
||||
lastActiveToolBeforeEraser: appState.activeTool,
|
||||
});
|
||||
setCursor(app.canvas, CURSOR_TYPE.GRAB);
|
||||
setCursor(app.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||
}
|
||||
|
||||
return {
|
||||
|
@ -259,23 +259,25 @@ const duplicateElements = (
|
||||
|
||||
return {
|
||||
elements: finalElements,
|
||||
appState: selectGroupsForSelectedElements(
|
||||
{
|
||||
...appState,
|
||||
selectedGroupIds: {},
|
||||
selectedElementIds: nextElementsToSelect.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(finalElements),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedElementIds: nextElementsToSelect.reduce(
|
||||
(acc: Record<ExcalidrawElement["id"], true>, element) => {
|
||||
if (!isBoundToContainer(element)) {
|
||||
acc[element.id] = true;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
},
|
||||
getNonDeletedElements(finalElements),
|
||||
appState,
|
||||
null,
|
||||
),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
@ -19,7 +19,12 @@ import { AppState } from "../types";
|
||||
export const actionFinalize = register({
|
||||
name: "finalize",
|
||||
trackEvent: false,
|
||||
perform: (elements, appState, _, { canvas, focusContainer, scene }) => {
|
||||
perform: (
|
||||
elements,
|
||||
appState,
|
||||
_,
|
||||
{ interactiveCanvas, focusContainer, scene },
|
||||
) => {
|
||||
if (appState.editingLinearElement) {
|
||||
const { elementId, startBindingElement, endBindingElement } =
|
||||
appState.editingLinearElement;
|
||||
@ -132,7 +137,7 @@ export const actionFinalize = register({
|
||||
appState.activeTool.type !== "freedraw") ||
|
||||
!multiPointElement
|
||||
) {
|
||||
resetCursor(canvas);
|
||||
resetCursor(interactiveCanvas);
|
||||
}
|
||||
|
||||
let activeTool: AppState["activeTool"];
|
||||
|
@ -108,7 +108,7 @@ export const actionSetFrameAsActiveTool = register({
|
||||
type: "frame",
|
||||
});
|
||||
|
||||
setCursorForShape(app.canvas, {
|
||||
setCursorForShape(app.interactiveCanvas, {
|
||||
...appState,
|
||||
activeTool: nextActiveTool,
|
||||
});
|
||||
|
@ -149,11 +149,14 @@ export const actionGroup = register({
|
||||
];
|
||||
|
||||
return {
|
||||
appState: selectGroup(
|
||||
newGroupId,
|
||||
{ ...appState, selectedGroupIds: {} },
|
||||
getNonDeletedElements(nextElements),
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
...selectGroup(
|
||||
newGroupId,
|
||||
{ ...appState, selectedGroupIds: {} },
|
||||
getNonDeletedElements(nextElements),
|
||||
),
|
||||
},
|
||||
elements: nextElements,
|
||||
commitToHistory: true,
|
||||
};
|
||||
@ -212,7 +215,7 @@ export const actionUngroup = register({
|
||||
});
|
||||
|
||||
const updateAppState = selectGroupsForSelectedElements(
|
||||
{ ...appState, selectedGroupIds: {} },
|
||||
appState,
|
||||
getNonDeletedElements(nextElements),
|
||||
appState,
|
||||
null,
|
||||
@ -243,7 +246,7 @@ export const actionUngroup = register({
|
||||
);
|
||||
|
||||
return {
|
||||
appState: updateAppState,
|
||||
appState: { ...appState, ...updateAppState },
|
||||
elements: nextElements,
|
||||
commitToHistory: true,
|
||||
};
|
||||
|
@ -28,22 +28,24 @@ export const actionSelectAll = register({
|
||||
}, {});
|
||||
|
||||
return {
|
||||
appState: selectGroupsForSelectedElements(
|
||||
{
|
||||
...appState,
|
||||
selectedLinearElement:
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0], app.scene)
|
||||
: null,
|
||||
editingGroupId: null,
|
||||
selectedElementIds,
|
||||
},
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
appState: {
|
||||
...appState,
|
||||
...selectGroupsForSelectedElements(
|
||||
{
|
||||
editingGroupId: null,
|
||||
selectedElementIds,
|
||||
},
|
||||
getNonDeletedElements(elements),
|
||||
appState,
|
||||
app,
|
||||
),
|
||||
selectedLinearElement:
|
||||
// single linear element selected
|
||||
Object.keys(selectedElementIds).length === 1 &&
|
||||
isLinearElement(elements[0])
|
||||
? new LinearElementEditor(elements[0], app.scene)
|
||||
: null,
|
||||
},
|
||||
commitToHistory: true,
|
||||
};
|
||||
},
|
||||
|
@ -213,13 +213,13 @@ export const SelectedShapeActions = ({
|
||||
};
|
||||
|
||||
export const ShapesSwitcher = ({
|
||||
canvas,
|
||||
interactiveCanvas,
|
||||
activeTool,
|
||||
setAppState,
|
||||
onImageAction,
|
||||
appState,
|
||||
}: {
|
||||
canvas: HTMLCanvasElement | null;
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
activeTool: UIAppState["activeTool"];
|
||||
setAppState: React.Component<any, UIAppState>["setState"];
|
||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||
@ -270,7 +270,7 @@ export const ShapesSwitcher = ({
|
||||
multiElement: null,
|
||||
selectedElementIds: {},
|
||||
});
|
||||
setCursorForShape(canvas, {
|
||||
setCursorForShape(interactiveCanvas, {
|
||||
...appState,
|
||||
activeTool: nextActiveTool,
|
||||
});
|
||||
|
@ -6,14 +6,14 @@ import { render, queryByTestId } from "../tests/test-utils";
|
||||
import ExcalidrawApp from "../excalidraw-app";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||
|
||||
describe("Test <App/>", () => {
|
||||
beforeEach(async () => {
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
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 { useOutsideClick } from "../hooks/useOutsideClick";
|
||||
import { KEYS } from "../keys";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { getSelectedElements } from "../scene";
|
||||
import Scene from "../scene/Scene";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
import { useApp, useExcalidrawContainer, useExcalidrawElements } from "./App";
|
||||
|
||||
import "./EyeDropper.scss";
|
||||
@ -98,7 +98,7 @@ export const EyeDropper: React.FC<{
|
||||
},
|
||||
false,
|
||||
);
|
||||
invalidateShapeForElement(element);
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
Scene.getScene(
|
||||
metaStuffRef.current.selectedElements[0],
|
||||
|
@ -34,7 +34,7 @@ const JSONExportModal = ({
|
||||
actionManager: ActionManager;
|
||||
onCloseRequest: () => void;
|
||||
exportOpts: ExportOpts;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
canvas: HTMLCanvasElement;
|
||||
}) => {
|
||||
const { onExportToBackend } = exportOpts;
|
||||
return (
|
||||
@ -100,7 +100,7 @@ export const JSONExportDialog = ({
|
||||
files: BinaryFiles;
|
||||
actionManager: ActionManager;
|
||||
exportOpts: ExportOpts;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
canvas: HTMLCanvasElement;
|
||||
setAppState: React.Component<any, UIAppState>["setState"];
|
||||
}) => {
|
||||
const handleClose = React.useCallback(() => {
|
||||
|
@ -57,7 +57,8 @@ interface LayerUIProps {
|
||||
actionManager: ActionManager;
|
||||
appState: UIAppState;
|
||||
files: BinaryFiles;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
canvas: HTMLCanvasElement;
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
setAppState: React.Component<any, AppState>["setState"];
|
||||
elements: readonly NonDeletedExcalidrawElement[];
|
||||
onLockToggle: () => void;
|
||||
@ -117,6 +118,7 @@ const LayerUI = ({
|
||||
setAppState,
|
||||
elements,
|
||||
canvas,
|
||||
interactiveCanvas,
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
@ -272,7 +274,7 @@ const LayerUI = ({
|
||||
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
setAppState={setAppState}
|
||||
onImageAction={({ pointerType }) => {
|
||||
@ -413,7 +415,7 @@ const LayerUI = ({
|
||||
onLockToggle={onLockToggle}
|
||||
onHandToolToggle={onHandToolToggle}
|
||||
onPenModeToggle={onPenModeToggle}
|
||||
canvas={canvas}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
onImageAction={onImageAction}
|
||||
renderTopRightUI={renderTopRightUI}
|
||||
renderCustomStats={renderCustomStats}
|
||||
@ -464,7 +466,7 @@ const LayerUI = ({
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
setAppState((appState) => ({
|
||||
...calculateScrollCenter(elements, appState, canvas),
|
||||
...calculateScrollCenter(elements, appState),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
@ -507,8 +509,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
|
||||
const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
|
||||
const {
|
||||
canvas: _pC,
|
||||
interactiveCanvas: _pIC,
|
||||
appState: prevAppState,
|
||||
...prev
|
||||
} = prevProps;
|
||||
const {
|
||||
canvas: _nC,
|
||||
interactiveCanvas: _nIC,
|
||||
appState: nextAppState,
|
||||
...next
|
||||
} = nextProps;
|
||||
|
||||
return (
|
||||
isShallowEqual(
|
||||
|
@ -36,7 +36,7 @@ type MobileMenuProps = {
|
||||
onLockToggle: () => void;
|
||||
onHandToolToggle: () => void;
|
||||
onPenModeToggle: () => void;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
|
||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||
renderTopRightUI?: (
|
||||
@ -58,7 +58,7 @@ export const MobileMenu = ({
|
||||
onLockToggle,
|
||||
onHandToolToggle,
|
||||
onPenModeToggle,
|
||||
canvas,
|
||||
interactiveCanvas,
|
||||
onImageAction,
|
||||
renderTopRightUI,
|
||||
renderCustomStats,
|
||||
@ -85,7 +85,7 @@ export const MobileMenu = ({
|
||||
<Stack.Row gap={1}>
|
||||
<ShapesSwitcher
|
||||
appState={appState}
|
||||
canvas={canvas}
|
||||
interactiveCanvas={interactiveCanvas}
|
||||
activeTool={appState.activeTool}
|
||||
setAppState={setAppState}
|
||||
onImageAction={({ pointerType }) => {
|
||||
@ -202,7 +202,7 @@ export const MobileMenu = ({
|
||||
className="scroll-back-to-content"
|
||||
onClick={() => {
|
||||
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 {
|
||||
--zIndex-canvas: 1;
|
||||
--zIndex-wysiwyg: 2;
|
||||
--zIndex-layerUI: 3;
|
||||
--zIndex-interactiveCanvas: 2;
|
||||
--zIndex-wysiwyg: 3;
|
||||
--zIndex-layerUI: 4;
|
||||
|
||||
--zIndex-modal: 1000;
|
||||
--zIndex-popup: 1001;
|
||||
@ -69,10 +70,19 @@
|
||||
|
||||
z-index: var(--zIndex-canvas);
|
||||
|
||||
&.interactive {
|
||||
z-index: var(--zIndex-interactiveCanvas);
|
||||
}
|
||||
|
||||
// Remove the main canvas from document flow to avoid resizeObserver
|
||||
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
||||
}
|
||||
|
||||
&__canvas-wrapper,
|
||||
&__canvas.static {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&__canvas {
|
||||
position: absolute;
|
||||
}
|
||||
|
@ -144,11 +144,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
||||
fileHandle: fileHandle || blob.handle || null,
|
||||
...cleanAppStateForExport(data.appState || {}),
|
||||
...(localAppState
|
||||
? calculateScrollCenter(
|
||||
data.elements || [],
|
||||
localAppState,
|
||||
null,
|
||||
)
|
||||
? calculateScrollCenter(data.elements || [], localAppState)
|
||||
: {}),
|
||||
},
|
||||
files: data.files,
|
||||
|
@ -25,10 +25,7 @@ import {
|
||||
} from "react";
|
||||
import clsx from "clsx";
|
||||
import { KEYS } from "../keys";
|
||||
import {
|
||||
DEFAULT_LINK_SIZE,
|
||||
invalidateShapeForElement,
|
||||
} from "../renderer/renderElement";
|
||||
import { DEFAULT_LINK_SIZE } from "../renderer/renderElement";
|
||||
import { rotate } from "../math";
|
||||
import { EVENT, HYPERLINK_TOOLTIP_DELAY, MIME_TYPES } from "../constants";
|
||||
import { Bounds } from "./bounds";
|
||||
@ -42,6 +39,7 @@ import "./Hyperlink.scss";
|
||||
import { trackEvent } from "../analytics";
|
||||
import { useAppProps, useExcalidrawAppState } from "../components/App";
|
||||
import { isEmbeddableElement } from "./typeChecks";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
const CONTAINER_WIDTH = 320;
|
||||
const SPACE_BOTTOM = 85;
|
||||
@ -115,7 +113,7 @@ export const Hyperlink = ({
|
||||
validated: false,
|
||||
link,
|
||||
});
|
||||
invalidateShapeForElement(element);
|
||||
ShapeCache.delete(element);
|
||||
} else {
|
||||
const { width, height } = element;
|
||||
const embedLink = getEmbedLink(link);
|
||||
@ -147,7 +145,7 @@ export const Hyperlink = ({
|
||||
validated: true,
|
||||
link,
|
||||
});
|
||||
invalidateShapeForElement(element);
|
||||
ShapeCache.delete(element);
|
||||
if (embeddableLinkCache.has(element.id)) {
|
||||
embeddableLinkCache.delete(element.id);
|
||||
}
|
||||
@ -393,7 +391,7 @@ export const getContextMenuLabel = (
|
||||
export const getLinkHandleFromCoords = (
|
||||
[x1, y1, x2, y2]: Bounds,
|
||||
angle: number,
|
||||
appState: UIAppState,
|
||||
appState: Pick<UIAppState, "zoom">,
|
||||
): [x: number, y: number, width: number, height: number] => {
|
||||
const size = DEFAULT_LINK_SIZE;
|
||||
const linkWidth = size / appState.zoom.value;
|
||||
|
@ -474,6 +474,7 @@ const maybeCalculateNewGapWhenScaling = (
|
||||
return { elementId, gap: newGap, focus };
|
||||
};
|
||||
|
||||
// TODO: this is a bottleneck, optimise
|
||||
export const getEligibleElementsForBinding = (
|
||||
elements: NonDeleted<ExcalidrawElement>[],
|
||||
): SuggestedBinding[] => {
|
||||
|
@ -10,10 +10,7 @@ import { distance2d, rotate, rotatePoint } from "../math";
|
||||
import rough from "roughjs/bin/rough";
|
||||
import { Drawable, Op } from "roughjs/bin/core";
|
||||
import { Point } from "../types";
|
||||
import {
|
||||
getShapeForElement,
|
||||
generateRoughOptions,
|
||||
} from "../renderer/renderElement";
|
||||
import { generateRoughOptions } from "../renderer/renderElement";
|
||||
import {
|
||||
isArrowElement,
|
||||
isFreeDrawElement,
|
||||
@ -24,6 +21,7 @@ import { rescalePoints } from "../points";
|
||||
import { getBoundTextElement, getContainerElement } from "./textElement";
|
||||
import { LinearElementEditor } from "./linearElementEditor";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
export type RectangleBox = {
|
||||
x: number;
|
||||
@ -621,7 +619,7 @@ const getLinearElementRotatedBounds = (
|
||||
}
|
||||
|
||||
// first element is always the curve
|
||||
const cachedShape = getShapeForElement(element)?.[0];
|
||||
const cachedShape = ShapeCache.get(element)?.[0];
|
||||
const shape = cachedShape ?? generateLinearElementShape(element);
|
||||
const ops = getCurvePathOps(shape);
|
||||
const transformXY = (x: number, y: number) =>
|
||||
|
@ -39,7 +39,6 @@ import {
|
||||
import { FrameNameBoundsCache, Point } from "../types";
|
||||
import { Drawable } from "roughjs/bin/core";
|
||||
import { AppState } from "../types";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import {
|
||||
hasBoundTextElement,
|
||||
isEmbeddableElement,
|
||||
@ -50,6 +49,7 @@ import { isTransparent } from "../utils";
|
||||
import { shouldShowBoundingBox } from "./transformHandles";
|
||||
import { getBoundTextElement } from "./textElement";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
const isElementDraggableFromInside = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
@ -489,7 +489,7 @@ const hitTestFreeDrawElement = (
|
||||
B = element.points[i + 1];
|
||||
}
|
||||
|
||||
const shape = getShapeForElement(element);
|
||||
const shape = ShapeCache.get(element);
|
||||
|
||||
// for filled freedraw shapes, support
|
||||
// selecting from inside
|
||||
@ -502,7 +502,7 @@ const hitTestFreeDrawElement = (
|
||||
|
||||
const hitTestLinear = (args: HitTestArgs): boolean => {
|
||||
const { element, threshold } = args;
|
||||
if (!getShapeForElement(element)) {
|
||||
if (!ShapeCache.get(element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -520,7 +520,7 @@ const hitTestLinear = (args: HitTestArgs): boolean => {
|
||||
}
|
||||
const [relX, relY] = GAPoint.toTuple(point);
|
||||
|
||||
const shape = getShapeForElement(element as ExcalidrawLinearElement);
|
||||
const shape = ShapeCache.get(element as ExcalidrawLinearElement);
|
||||
|
||||
if (!shape) {
|
||||
return false;
|
||||
|
@ -25,7 +25,12 @@ import {
|
||||
getElementPointsCoords,
|
||||
getMinMaxXYFromCurvePathOps,
|
||||
} from "./bounds";
|
||||
import { Point, AppState, PointerCoords } from "../types";
|
||||
import {
|
||||
Point,
|
||||
AppState,
|
||||
PointerCoords,
|
||||
InteractiveCanvasAppState,
|
||||
} from "../types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import History from "../history";
|
||||
|
||||
@ -39,9 +44,9 @@ import { tupleToCoors } from "../utils";
|
||||
import { isBindingElement } from "./typeChecks";
|
||||
import { shouldRotateWithDiscreteAngle } from "../keys";
|
||||
import { getBoundTextElement, handleBindTextResize } from "./textElement";
|
||||
import { getShapeForElement } from "../renderer/renderElement";
|
||||
import { DRAGGING_THRESHOLD } from "../constants";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
const editorMidPointsCache: {
|
||||
version: number | null;
|
||||
@ -398,7 +403,7 @@ export class LinearElementEditor {
|
||||
|
||||
static getEditorMidPoints = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
): typeof editorMidPointsCache["points"] => {
|
||||
const boundText = getBoundTextElement(element);
|
||||
|
||||
@ -422,7 +427,7 @@ export class LinearElementEditor {
|
||||
|
||||
static updateEditorMidPointsCache = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
appState: AppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||
|
||||
@ -1418,7 +1423,7 @@ export class LinearElementEditor {
|
||||
let y1;
|
||||
let x2;
|
||||
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
|
||||
const { minX, minY, maxX, maxY } = element.points.reduce(
|
||||
(limits, [x, y]) => {
|
||||
@ -1437,7 +1442,7 @@ export class LinearElementEditor {
|
||||
x2 = maxX + element.x;
|
||||
y2 = maxY + element.y;
|
||||
} else {
|
||||
const shape = getShapeForElement(element)!;
|
||||
const shape = ShapeCache.generateElementShape(element);
|
||||
|
||||
// first element is always the curve
|
||||
const ops = getCurvePathOps(shape[0]);
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { ExcalidrawElement } from "./types";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import Scene from "../scene/Scene";
|
||||
import { getSizeFromPoints } from "../points";
|
||||
import { randomInteger } from "../random";
|
||||
import { Point } from "../types";
|
||||
import { getUpdatedTimestamp } from "../utils";
|
||||
import { Mutable } from "../utility-types";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
type ElementUpdate<TElement extends ExcalidrawElement> = Omit<
|
||||
Partial<TElement>,
|
||||
@ -89,7 +89,7 @@ export const mutateElement = <TElement extends Mutable<ExcalidrawElement>>(
|
||||
typeof fileId != "undefined" ||
|
||||
typeof points !== "undefined"
|
||||
) {
|
||||
invalidateShapeForElement(element);
|
||||
ShapeCache.delete(element);
|
||||
}
|
||||
|
||||
element.version++;
|
||||
|
@ -2,7 +2,9 @@ import { ExcalidrawElement } from "./types";
|
||||
import { mutateElement } from "./mutateElement";
|
||||
import { isFreeDrawElement, isLinearElement } from "./typeChecks";
|
||||
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 = (
|
||||
element: ExcalidrawElement,
|
||||
@ -13,6 +15,42 @@ export const isInvisiblySmallElement = (
|
||||
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
|
||||
*/
|
||||
|
@ -759,7 +759,7 @@ describe("textWysiwyg", () => {
|
||||
expect(h.elements[1].type).toBe("text");
|
||||
|
||||
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
@ -903,7 +903,7 @@ describe("textWysiwyg", () => {
|
||||
mouse.clickAt(10, 20);
|
||||
mouse.down();
|
||||
mouse.up();
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
@ -1154,7 +1154,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
h.elements = [container, text];
|
||||
API.setSelectedElements([container, text]);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
@ -1168,7 +1168,7 @@ describe("textWysiwyg", () => {
|
||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
|
||||
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
|
||||
);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
@ -1406,7 +1406,7 @@ describe("textWysiwyg", () => {
|
||||
|
||||
API.setSelectedElements([textElement]);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
|
@ -116,7 +116,7 @@ export const textWysiwyg = ({
|
||||
}) => void;
|
||||
getViewportCoords: (x: number, y: number) => [number, number];
|
||||
element: ExcalidrawTextElement;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
canvas: HTMLCanvasElement;
|
||||
excalidrawContainer: HTMLDivElement | null;
|
||||
app: App;
|
||||
}) => {
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
|
||||
import { getElementAbsoluteCoords } from "./bounds";
|
||||
import { rotate } from "../math";
|
||||
import { AppState, Zoom } from "../types";
|
||||
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||
import { isTextElement } from ".";
|
||||
import { isFrameElement, isLinearElement } from "./typeChecks";
|
||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||
@ -276,8 +276,8 @@ export const getTransformHandles = (
|
||||
};
|
||||
|
||||
export const shouldShowBoundingBox = (
|
||||
elements: NonDeletedExcalidrawElement[],
|
||||
appState: AppState,
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
if (appState.editingLinearElement) {
|
||||
return false;
|
||||
|
@ -16,14 +16,24 @@ export const isBrowserStorageStateNewer = (type: BrowserStateTypes) => {
|
||||
|
||||
export const updateBrowserStateVersion = (type: BrowserStateTypes) => {
|
||||
const timestamp = Date.now();
|
||||
localStorage.setItem(type, JSON.stringify(timestamp));
|
||||
LOCAL_STATE_VERSIONS[type] = timestamp;
|
||||
try {
|
||||
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 = () => {
|
||||
for (const key of Object.keys(LOCAL_STATE_VERSIONS) as BrowserStateTypes[]) {
|
||||
const timestamp = -1;
|
||||
localStorage.setItem(key, JSON.stringify(timestamp));
|
||||
LOCAL_STATE_VERSIONS[key] = timestamp;
|
||||
try {
|
||||
for (const key of Object.keys(
|
||||
LOCAL_STATE_VERSIONS,
|
||||
) 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[],
|
||||
appState: Partial<AppState>,
|
||||
files: BinaryFiles,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
canvas: HTMLCanvasElement,
|
||||
) => {
|
||||
if (exportedElements.length === 0) {
|
||||
return window.alert(t("alerts.cannotExportEmptyCanvas"));
|
||||
|
20
src/frame.ts
20
src/frame.ts
@ -16,7 +16,7 @@ import {
|
||||
} from "./element/textElement";
|
||||
import { arrayToMap, findIndex } from "./utils";
|
||||
import { mutateElement } from "./element/mutateElement";
|
||||
import { AppClassProperties, AppState } from "./types";
|
||||
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||
import { isFrameElement } from "./element";
|
||||
import { moveOneRight } from "./zindex";
|
||||
@ -469,9 +469,16 @@ export const addElementsToFrame = (
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
for (const element of omitGroupsContainingFrames(
|
||||
allElements,
|
||||
_elementsToAdd,
|
||||
@ -485,8 +492,8 @@ export const addElementsToFrame = (
|
||||
false,
|
||||
);
|
||||
|
||||
const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
|
||||
const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
|
||||
const frameIndex = nextElementsIndex[frame.id] ?? -1;
|
||||
const elementIndex = nextElementsIndex[element.id] ?? -1;
|
||||
|
||||
if (elementIndex < frameBoundary) {
|
||||
nextElements = [
|
||||
@ -648,7 +655,7 @@ export const omitGroupsContainingFrames = (
|
||||
*/
|
||||
export const getTargetFrame = (
|
||||
element: ExcalidrawElement,
|
||||
appState: AppState,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const _element = isTextElement(element)
|
||||
? getContainerElement(element) || element
|
||||
@ -660,11 +667,12 @@ export const getTargetFrame = (
|
||||
: getContainingFrame(_element);
|
||||
};
|
||||
|
||||
// TODO: this a huge bottleneck for large scenes, optimise
|
||||
// given an element, return if the element is in some frame
|
||||
export const isElementInFrame = (
|
||||
element: ExcalidrawElement,
|
||||
allElements: ExcalidrawElementsIncludingDeleted,
|
||||
appState: AppState,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const frame = getTargetFrame(element, appState);
|
||||
const _element = isTextElement(element)
|
||||
|
246
src/groups.ts
246
src/groups.ts
@ -4,27 +4,40 @@ import {
|
||||
NonDeleted,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "./element/types";
|
||||
import { AppClassProperties, AppState } from "./types";
|
||||
import {
|
||||
AppClassProperties,
|
||||
AppState,
|
||||
InteractiveCanvasAppState,
|
||||
} from "./types";
|
||||
import { getSelectedElements } from "./scene";
|
||||
import { getBoundTextElement } from "./element/textElement";
|
||||
import { makeNextSelectedElementIds } from "./scene/selection";
|
||||
|
||||
export const selectGroup = (
|
||||
groupId: GroupId,
|
||||
appState: AppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||
): AppState => {
|
||||
const elementsInGroup = elements.filter((element) =>
|
||||
element.groupIds.includes(groupId),
|
||||
): Pick<
|
||||
InteractiveCanvasAppState,
|
||||
"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 (
|
||||
appState.selectedGroupIds[groupId] ||
|
||||
appState.editingGroupId === groupId
|
||||
) {
|
||||
return {
|
||||
...appState,
|
||||
selectedElementIds: appState.selectedElementIds,
|
||||
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: false },
|
||||
editingGroupId: null,
|
||||
};
|
||||
@ -33,104 +46,184 @@ export const selectGroup = (
|
||||
}
|
||||
|
||||
return {
|
||||
...appState,
|
||||
editingGroupId: appState.editingGroupId,
|
||||
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
|
||||
selectedElementIds: {
|
||||
...appState.selectedElementIds,
|
||||
...Object.fromEntries(
|
||||
elementsInGroup.map((element) => [element.id, true]),
|
||||
),
|
||||
...elementsInGroup,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
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
|
||||
* selection border around it.
|
||||
*/
|
||||
export const isSelectedViaGroup = (
|
||||
appState: AppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
element: ExcalidrawElement,
|
||||
) => getSelectedGroupForElement(appState, element) != null;
|
||||
|
||||
export const getSelectedGroupForElement = (
|
||||
appState: AppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
element: ExcalidrawElement,
|
||||
) =>
|
||||
element.groupIds
|
||||
.filter((groupId) => groupId !== appState.editingGroupId)
|
||||
.find((groupId) => appState.selectedGroupIds[groupId]);
|
||||
|
||||
export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
||||
export const getSelectedGroupIds = (
|
||||
appState: InteractiveCanvasAppState,
|
||||
): GroupId[] =>
|
||||
Object.entries(appState.selectedGroupIds)
|
||||
.filter(([groupId, isSelected]) => isSelected)
|
||||
.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
|
||||
// or used to update the elements
|
||||
export const selectGroupsFromGivenElements = (
|
||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||
appState: AppState,
|
||||
appState: InteractiveCanvasAppState,
|
||||
) => {
|
||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
||||
let nextAppState: InteractiveCanvasAppState = {
|
||||
...appState,
|
||||
selectedGroupIds: {},
|
||||
};
|
||||
|
||||
for (const element of elements) {
|
||||
let groupIds = element.groupIds;
|
||||
@ -142,7 +235,10 @@ export const selectGroupsFromGivenElements = (
|
||||
}
|
||||
if (groupIds.length > 0) {
|
||||
const groupId = groupIds[groupIds.length - 1];
|
||||
nextAppState = selectGroup(groupId, nextAppState, elements);
|
||||
nextAppState = {
|
||||
...nextAppState,
|
||||
...selectGroup(groupId, nextAppState, elements),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,9 +10,9 @@ import {
|
||||
ExcalidrawLinearElement,
|
||||
NonDeleted,
|
||||
} from "./element/types";
|
||||
import { getShapeForElement } from "./renderer/renderElement";
|
||||
import { getCurvePathOps } from "./element/bounds";
|
||||
import { Mutable } from "./utility-types";
|
||||
import { ShapeCache } from "./scene/ShapeCache";
|
||||
|
||||
export const rotate = (
|
||||
x1: number,
|
||||
@ -303,7 +303,7 @@ export const getControlPointsForBezierCurve = (
|
||||
element: NonDeleted<ExcalidrawLinearElement>,
|
||||
endPoint: Point,
|
||||
) => {
|
||||
const shape = getShapeForElement(element as ExcalidrawLinearElement);
|
||||
const shape = ShapeCache.generateElementShape(element);
|
||||
if (!shape) {
|
||||
return null;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ import { Drawable, Options } from "roughjs/bin/core";
|
||||
import { RoughSVG } from "roughjs/bin/svg";
|
||||
import { RoughGenerator } from "roughjs/bin/generator";
|
||||
|
||||
import { RenderConfig } from "../scene/types";
|
||||
import { StaticCanvasRenderConfig } from "../scene/types";
|
||||
import {
|
||||
distance,
|
||||
getFontString,
|
||||
@ -36,7 +36,13 @@ import {
|
||||
} from "../utils";
|
||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
||||
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 {
|
||||
BOUND_TEXT_PADDING,
|
||||
@ -61,6 +67,7 @@ import {
|
||||
} from "../element/embeddable";
|
||||
import { getContainingFrame } from "../frame";
|
||||
import { normalizeLink, toValidURL } from "../data/url";
|
||||
import { ShapeCache } from "../scene/ShapeCache";
|
||||
|
||||
// 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
|
||||
@ -72,17 +79,18 @@ const defaultAppState = getDefaultAppState();
|
||||
|
||||
const isPendingImageElement = (
|
||||
element: ExcalidrawElement,
|
||||
renderConfig: RenderConfig,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
) =>
|
||||
isInitializedImageElement(element) &&
|
||||
!renderConfig.imageCache.has(element.fileId);
|
||||
|
||||
const shouldResetImageFilter = (
|
||||
element: ExcalidrawElement,
|
||||
renderConfig: RenderConfig,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
return (
|
||||
renderConfig.theme === "dark" &&
|
||||
appState.theme === "dark" &&
|
||||
isInitializedImageElement(element) &&
|
||||
!isPendingImageElement(element, renderConfig) &&
|
||||
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
||||
@ -99,9 +107,9 @@ const getCanvasPadding = (element: ExcalidrawElement) =>
|
||||
export interface ExcalidrawElementWithCanvas {
|
||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||
canvas: HTMLCanvasElement;
|
||||
theme: RenderConfig["theme"];
|
||||
theme: AppState["theme"];
|
||||
scale: number;
|
||||
zoomValue: RenderConfig["zoom"]["value"];
|
||||
zoomValue: AppState["zoom"]["value"];
|
||||
canvasOffsetX: number;
|
||||
canvasOffsetY: number;
|
||||
boundTextElementVersion: number | null;
|
||||
@ -165,7 +173,8 @@ const cappedElementCanvasSize = (
|
||||
const generateElementCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
zoom: Zoom,
|
||||
renderConfig: RenderConfig,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
): ExcalidrawElementWithCanvas => {
|
||||
const canvas = document.createElement("canvas");
|
||||
const context = canvas.getContext("2d")!;
|
||||
@ -205,17 +214,17 @@ const generateElementCanvas = (
|
||||
const rc = rough.canvas(canvas);
|
||||
|
||||
// in dark theme, revert the image color filter
|
||||
if (shouldResetImageFilter(element, renderConfig)) {
|
||||
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||
context.filter = IMAGE_INVERT_FILTER;
|
||||
}
|
||||
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
context.restore();
|
||||
|
||||
return {
|
||||
element,
|
||||
canvas,
|
||||
theme: renderConfig.theme,
|
||||
theme: appState.theme,
|
||||
scale,
|
||||
zoomValue: zoom.value,
|
||||
canvasOffsetX,
|
||||
@ -262,11 +271,13 @@ const drawImagePlaceholder = (
|
||||
size,
|
||||
);
|
||||
};
|
||||
|
||||
const drawElementOnCanvas = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: RenderConfig,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
context.globalAlpha =
|
||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||
@ -277,7 +288,7 @@ const drawElementOnCanvas = (
|
||||
case "ellipse": {
|
||||
context.lineJoin = "round";
|
||||
context.lineCap = "round";
|
||||
rc.draw(getShapeForElement(element)!);
|
||||
rc.draw(ShapeCache.get(element)!);
|
||||
break;
|
||||
}
|
||||
case "arrow":
|
||||
@ -285,7 +296,7 @@ const drawElementOnCanvas = (
|
||||
context.lineJoin = "round";
|
||||
context.lineCap = "round";
|
||||
|
||||
getShapeForElement(element)!.forEach((shape) => {
|
||||
ShapeCache.get(element)!.forEach((shape) => {
|
||||
rc.draw(shape);
|
||||
});
|
||||
break;
|
||||
@ -296,7 +307,7 @@ const drawElementOnCanvas = (
|
||||
context.fillStyle = element.strokeColor;
|
||||
|
||||
const path = getFreeDrawPath2D(element) as Path2D;
|
||||
const fillShape = getShapeForElement(element);
|
||||
const fillShape = ShapeCache.get(element);
|
||||
|
||||
if (fillShape) {
|
||||
rc.draw(fillShape);
|
||||
@ -321,7 +332,7 @@ const drawElementOnCanvas = (
|
||||
element.height,
|
||||
);
|
||||
} else {
|
||||
drawImagePlaceholder(element, context, renderConfig.zoom.value);
|
||||
drawImagePlaceholder(element, context, appState.zoom.value);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -378,33 +389,6 @@ const elementWithCanvasCache = new WeakMap<
|
||||
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 = (
|
||||
element: ExcalidrawElement,
|
||||
continuousPath = false,
|
||||
@ -494,16 +478,22 @@ const modifyEmbeddableForRoughOptions = (
|
||||
* @param element
|
||||
* @param generator
|
||||
*/
|
||||
const generateElementShape = (
|
||||
export const generateElementShape = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
generator: RoughGenerator,
|
||||
isExporting: boolean = false,
|
||||
) => {
|
||||
let shape = isExporting ? undefined : shapeCache.get(element);
|
||||
): Drawable | Drawable[] | null => {
|
||||
const cachedShape = isExporting ? undefined : ShapeCache.get(element);
|
||||
|
||||
if (cachedShape) {
|
||||
return cachedShape;
|
||||
}
|
||||
|
||||
// `null` indicates no rc shape applicable for this element type
|
||||
// (= do not generate anything)
|
||||
if (shape === undefined) {
|
||||
if (cachedShape === undefined) {
|
||||
let shape: Drawable | Drawable[] | null = null;
|
||||
|
||||
elementWithCanvasCache.delete(element);
|
||||
|
||||
switch (element.type) {
|
||||
@ -539,7 +529,7 @@ const generateElementShape = (
|
||||
),
|
||||
);
|
||||
}
|
||||
setShapeForElement(element, shape);
|
||||
ShapeCache.set(element, shape);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -589,7 +579,7 @@ const generateElementShape = (
|
||||
generateRoughOptions(element),
|
||||
);
|
||||
}
|
||||
setShapeForElement(element, shape);
|
||||
ShapeCache.set(element, shape);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -601,7 +591,7 @@ const generateElementShape = (
|
||||
element.height,
|
||||
generateRoughOptions(element),
|
||||
);
|
||||
setShapeForElement(element, shape);
|
||||
ShapeCache.set(element, shape);
|
||||
|
||||
break;
|
||||
case "line":
|
||||
@ -726,7 +716,7 @@ const generateElementShape = (
|
||||
}
|
||||
}
|
||||
|
||||
setShapeForElement(element, shape);
|
||||
ShapeCache.set(element, shape);
|
||||
|
||||
break;
|
||||
}
|
||||
@ -742,36 +732,39 @@ const generateElementShape = (
|
||||
} else {
|
||||
shape = null;
|
||||
}
|
||||
setShapeForElement(element, shape);
|
||||
ShapeCache.set(element, shape);
|
||||
break;
|
||||
}
|
||||
case "text":
|
||||
case "image": {
|
||||
// just to ensure we don't regenerate element.canvas on rerenders
|
||||
setShapeForElement(element, null);
|
||||
ShapeCache.set(element, null);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return shape;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const generateElementWithCanvas = (
|
||||
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 shouldRegenerateBecauseZoom =
|
||||
prevElementWithCanvas &&
|
||||
prevElementWithCanvas.zoomValue !== zoom.value &&
|
||||
!renderConfig?.shouldCacheIgnoreZoom;
|
||||
!appState?.shouldCacheIgnoreZoom;
|
||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
||||
|
||||
if (
|
||||
!prevElementWithCanvas ||
|
||||
shouldRegenerateBecauseZoom ||
|
||||
prevElementWithCanvas.theme !== renderConfig.theme ||
|
||||
prevElementWithCanvas.theme !== appState.theme ||
|
||||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
|
||||
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
|
||||
) {
|
||||
@ -779,6 +772,7 @@ const generateElementWithCanvas = (
|
||||
element,
|
||||
zoom,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||
@ -790,9 +784,9 @@ const generateElementWithCanvas = (
|
||||
|
||||
const drawElementFromCanvas = (
|
||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: RenderConfig,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const element = elementWithCanvas.element;
|
||||
const padding = getCanvasPadding(element);
|
||||
@ -807,8 +801,8 @@ const drawElementFromCanvas = (
|
||||
y2 = Math.ceil(y2);
|
||||
}
|
||||
|
||||
const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio;
|
||||
const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio;
|
||||
const cx = ((x1 + x2) / 2 + appState.scrollX) * window.devicePixelRatio;
|
||||
const cy = ((y1 + y2) / 2 + appState.scrollY) * window.devicePixelRatio;
|
||||
|
||||
context.save();
|
||||
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||
@ -906,9 +900,9 @@ const drawElementFromCanvas = (
|
||||
|
||||
context.drawImage(
|
||||
elementWithCanvas.canvas!,
|
||||
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
||||
(x1 + appState.scrollX) * window.devicePixelRatio -
|
||||
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
||||
(y1 + appState.scrollY) * window.devicePixelRatio -
|
||||
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
|
||||
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
||||
@ -926,8 +920,8 @@ const drawElementFromCanvas = (
|
||||
context.strokeStyle = "#c92a2a";
|
||||
context.lineWidth = 3;
|
||||
context.strokeRect(
|
||||
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
||||
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
||||
(coords.x + appState.scrollX) * window.devicePixelRatio,
|
||||
(coords.y + appState.scrollY) * window.devicePixelRatio,
|
||||
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||
);
|
||||
@ -938,40 +932,38 @@ const drawElementFromCanvas = (
|
||||
// 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 = (
|
||||
element: NonDeletedExcalidrawElement,
|
||||
rc: RoughCanvas,
|
||||
context: CanvasRenderingContext2D,
|
||||
renderConfig: RenderConfig,
|
||||
appState: AppState,
|
||||
renderConfig: StaticCanvasRenderConfig,
|
||||
appState: StaticCanvasAppState,
|
||||
) => {
|
||||
const generator = rc.generator;
|
||||
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": {
|
||||
if (
|
||||
!renderConfig.isExporting &&
|
||||
@ -980,12 +972,12 @@ export const renderElement = (
|
||||
) {
|
||||
context.save();
|
||||
context.translate(
|
||||
element.x + renderConfig.scrollX,
|
||||
element.y + renderConfig.scrollY,
|
||||
element.x + appState.scrollX,
|
||||
element.y + appState.scrollY,
|
||||
);
|
||||
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;
|
||||
|
||||
if (FRAME_STYLE.radius && context.roundRect) {
|
||||
@ -995,7 +987,7 @@ export const renderElement = (
|
||||
0,
|
||||
element.width,
|
||||
element.height,
|
||||
FRAME_STYLE.radius / renderConfig.zoom.value,
|
||||
FRAME_STYLE.radius / appState.zoom.value,
|
||||
);
|
||||
context.stroke();
|
||||
context.closePath();
|
||||
@ -1012,22 +1004,28 @@ export const renderElement = (
|
||||
|
||||
if (renderConfig.isExporting) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
||||
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||
const cy = (y1 + y2) / 2 + appState.scrollY;
|
||||
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
context.rotate(element.angle);
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
context.restore();
|
||||
} else {
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
drawElementFromCanvas(
|
||||
elementWithCanvas,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
||||
}
|
||||
|
||||
break;
|
||||
@ -1043,8 +1041,8 @@ export const renderElement = (
|
||||
generateElementShape(element, generator, renderConfig.isExporting);
|
||||
if (renderConfig.isExporting) {
|
||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
||||
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||
const cy = (y1 + y2) / 2 + appState.scrollY;
|
||||
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||
if (isTextElement(element)) {
|
||||
@ -1062,7 +1060,7 @@ export const renderElement = (
|
||||
context.save();
|
||||
context.translate(cx, cy);
|
||||
|
||||
if (shouldResetImageFilter(element, renderConfig)) {
|
||||
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||
context.filter = "none";
|
||||
}
|
||||
const boundTextElement = getBoundTextElement(element);
|
||||
@ -1096,7 +1094,13 @@ export const renderElement = (
|
||||
|
||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||
|
||||
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
|
||||
drawElementOnCanvas(
|
||||
element,
|
||||
tempRc,
|
||||
tempCanvasContext,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
||||
tempCanvasContext.translate(shiftX, shiftY);
|
||||
|
||||
@ -1133,7 +1137,7 @@ export const renderElement = (
|
||||
}
|
||||
|
||||
context.translate(-shiftX, -shiftY);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
||||
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||
}
|
||||
|
||||
context.restore();
|
||||
@ -1143,6 +1147,7 @@ export const renderElement = (
|
||||
const elementWithCanvas = generateElementWithCanvas(
|
||||
element,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
||||
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
|
||||
@ -1150,7 +1155,7 @@ export const renderElement = (
|
||||
if (
|
||||
// do not disable smoothing during zoom as blurry shapes look better
|
||||
// on low resolution (while still zooming in) than sharp ones
|
||||
!renderConfig?.shouldCacheIgnoreZoom &&
|
||||
!appState?.shouldCacheIgnoreZoom &&
|
||||
// angle is 0 -> always disable smoothing
|
||||
(!element.angle ||
|
||||
// or check if angle is a right angle in which case we can still
|
||||
@ -1167,7 +1172,12 @@ export const renderElement = (
|
||||
context.imageSmoothingEnabled = false;
|
||||
}
|
||||
|
||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
||||
drawElementFromCanvas(
|
||||
elementWithCanvas,
|
||||
context,
|
||||
renderConfig,
|
||||
appState,
|
||||
);
|
||||
|
||||
// reset
|
||||
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
||||
@ -1273,7 +1283,7 @@ export const renderElementToSvg = (
|
||||
generateElementShape(element, generator);
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
getShapeForElement(element)!,
|
||||
ShapeCache.get(element)!,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
if (opacity !== 1) {
|
||||
@ -1303,7 +1313,7 @@ export const renderElementToSvg = (
|
||||
generateElementShape(element, generator, true);
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
getShapeForElement(element)!,
|
||||
ShapeCache.get(element)!,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
const opacity = element.opacity / 100;
|
||||
@ -1337,7 +1347,7 @@ export const renderElementToSvg = (
|
||||
// render embeddable element + iframe
|
||||
const embeddableNode = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
getShapeForElement(element)!,
|
||||
ShapeCache.get(element)!,
|
||||
MAX_DECIMALS_FOR_SVG_EXPORT,
|
||||
);
|
||||
embeddableNode.setAttribute("stroke-linecap", "round");
|
||||
@ -1450,7 +1460,7 @@ export const renderElementToSvg = (
|
||||
}
|
||||
group.setAttribute("stroke-linecap", "round");
|
||||
|
||||
getShapeForElement(element)!.forEach((shape) => {
|
||||
ShapeCache.get(element)!.forEach((shape) => {
|
||||
const node = roughSVGDrawWithPrecision(
|
||||
rsvg,
|
||||
shape,
|
||||
@ -1493,7 +1503,7 @@ export const renderElementToSvg = (
|
||||
case "freedraw": {
|
||||
generateElementShape(element, generator);
|
||||
generateFreeDrawShape(element);
|
||||
const shape = getShapeForElement(element);
|
||||
const shape = ShapeCache.get(element);
|
||||
const node = shape
|
||||
? roughSVGDrawWithPrecision(rsvg, shape, MAX_DECIMALS_FOR_SVG_EXPORT)
|
||||
: 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 { isBoundToContainer } from "../element/typeChecks";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement } from "../element/types";
|
||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||
import { getFontString } from "../utils";
|
||||
import type Scene from "./Scene";
|
||||
import { ShapeCache } from "./ShapeCache";
|
||||
|
||||
export class Fonts {
|
||||
private scene: Scene;
|
||||
@ -54,7 +54,7 @@ export class Fonts {
|
||||
|
||||
this.scene.mapElements((element) => {
|
||||
if (isTextElement(element) && !isBoundToContainer(element)) {
|
||||
invalidateShapeForElement(element);
|
||||
ShapeCache.delete(element);
|
||||
didUpdate = true;
|
||||
return newElementWith(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 { AppState } from "../types";
|
||||
import { Assert, SameType } from "../utility-types";
|
||||
import { randomInteger } from "../random";
|
||||
|
||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||
@ -105,6 +106,7 @@ class Scene {
|
||||
elements: null,
|
||||
cache: new Map(),
|
||||
};
|
||||
private versionNonce: number | undefined;
|
||||
|
||||
getElementsIncludingDeleted() {
|
||||
return this.elements;
|
||||
@ -172,6 +174,10 @@ class Scene {
|
||||
return (this.elementsMap.get(id) as T | undefined) || null;
|
||||
}
|
||||
|
||||
getVersionNonce() {
|
||||
return this.versionNonce;
|
||||
}
|
||||
|
||||
getNonDeletedElement(
|
||||
id: ExcalidrawElement["id"],
|
||||
): NonDeleted<ExcalidrawElement> | null {
|
||||
@ -230,6 +236,8 @@ class Scene {
|
||||
}
|
||||
|
||||
informMutation() {
|
||||
this.versionNonce = randomInteger();
|
||||
|
||||
for (const callback of Array.from(this.callbacks)) {
|
||||
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 { NonDeletedExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
||||
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
||||
import { AppState, BinaryFiles } from "../types";
|
||||
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
||||
@ -54,26 +54,23 @@ export const exportToCanvas = async (
|
||||
|
||||
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
||||
|
||||
renderScene({
|
||||
elements,
|
||||
appState,
|
||||
scale,
|
||||
rc: rough.canvas(canvas),
|
||||
renderStaticScene({
|
||||
canvas,
|
||||
renderConfig: {
|
||||
rc: rough.canvas(canvas),
|
||||
elements,
|
||||
visibleElements: elements,
|
||||
scale,
|
||||
appState: {
|
||||
...appState,
|
||||
viewBackgroundColor: exportBackground ? viewBackgroundColor : null,
|
||||
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||
zoom: defaultAppState.zoom,
|
||||
remotePointerViewportCoords: {},
|
||||
remoteSelectedElementIds: {},
|
||||
shouldCacheIgnoreZoom: false,
|
||||
remotePointerUsernames: {},
|
||||
remotePointerUserStates: {},
|
||||
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||
},
|
||||
renderConfig: {
|
||||
imageCache,
|
||||
renderScrollbars: false,
|
||||
renderSelection: false,
|
||||
renderGrid: false,
|
||||
isExporting: true,
|
||||
},
|
||||
|
@ -11,11 +11,7 @@ import {
|
||||
viewportCoordsToSceneCoords,
|
||||
} from "../utils";
|
||||
|
||||
const isOutsideViewPort = (
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
cords: Array<number>,
|
||||
) => {
|
||||
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
|
||||
const [x1, y1, x2, y2] = cords;
|
||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||
{ sceneX: x1, sceneY: y1 },
|
||||
@ -49,7 +45,6 @@ export const centerScrollOn = ({
|
||||
export const calculateScrollCenter = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
appState: AppState,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
): { scrollX: number; scrollY: number } => {
|
||||
elements = getVisibleElements(elements);
|
||||
|
||||
@ -61,7 +56,7 @@ export const calculateScrollCenter = (
|
||||
}
|
||||
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(
|
||||
elements,
|
||||
viewportCoordsToSceneCoords(
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { ExcalidrawElement } from "../element/types";
|
||||
import { getCommonBounds } from "../element";
|
||||
import { Zoom } from "../types";
|
||||
import { InteractiveCanvasAppState } from "../types";
|
||||
import { ScrollBars } from "./types";
|
||||
import { getGlobalCSSVariable } from "../utils";
|
||||
import { getLanguage } from "../i18n";
|
||||
@ -13,15 +13,7 @@ export const getScrollBars = (
|
||||
elements: readonly ExcalidrawElement[],
|
||||
viewportWidth: number,
|
||||
viewportHeight: number,
|
||||
{
|
||||
scrollX,
|
||||
scrollY,
|
||||
zoom,
|
||||
}: {
|
||||
scrollX: number;
|
||||
scrollY: number;
|
||||
zoom: Zoom;
|
||||
},
|
||||
appState: InteractiveCanvasAppState,
|
||||
): ScrollBars => {
|
||||
if (elements.length === 0) {
|
||||
return {
|
||||
@ -34,8 +26,8 @@ export const getScrollBars = (
|
||||
getCommonBounds(elements);
|
||||
|
||||
// Apply zoom
|
||||
const viewportWidthWithZoom = viewportWidth / zoom.value;
|
||||
const viewportHeightWithZoom = viewportHeight / zoom.value;
|
||||
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||
|
||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||
@ -50,8 +42,10 @@ export const getScrollBars = (
|
||||
const isRTL = getLanguage().rtl;
|
||||
|
||||
// The viewport is the rectangle currently visible for the user
|
||||
const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||
const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||
const viewportMinX =
|
||||
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||
const viewportMinY =
|
||||
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||
import { AppState } from "../types";
|
||||
import { AppState, InteractiveCanvasAppState } from "../types";
|
||||
import { isBoundToContainer } from "../element/typeChecks";
|
||||
import {
|
||||
elementOverlapsWithFrame,
|
||||
@ -146,7 +146,7 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
||||
|
||||
export const getSelectedElements = (
|
||||
elements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: Pick<AppState, "selectedElementIds">,
|
||||
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||
opts?: {
|
||||
includeBoundTextElement?: boolean;
|
||||
includeElementsInFrames?: boolean;
|
||||
|
@ -1,33 +1,63 @@
|
||||
import { ExcalidrawTextElement } from "../element/types";
|
||||
import { AppClassProperties, AppState } from "../types";
|
||||
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||
import {
|
||||
ExcalidrawTextElement,
|
||||
NonDeletedExcalidrawElement,
|
||||
} from "../element/types";
|
||||
import {
|
||||
AppClassProperties,
|
||||
InteractiveCanvasAppState,
|
||||
StaticCanvasAppState,
|
||||
} from "../types";
|
||||
|
||||
export type RenderConfig = {
|
||||
// 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 };
|
||||
export type StaticCanvasRenderConfig = {
|
||||
// extra options passed to the renderer
|
||||
// ---------------------------------------------------------------------------
|
||||
imageCache: AppClassProperties["imageCache"];
|
||||
renderScrollbars?: boolean;
|
||||
renderSelection?: boolean;
|
||||
renderGrid?: boolean;
|
||||
renderGrid: boolean;
|
||||
/** 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;
|
||||
};
|
||||
|
||||
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;
|
||||
// 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 = {
|
||||
|
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": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@ -42,7 +42,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 401146281,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
@ -69,14 +69,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "diamond",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1278240551,
|
||||
"versionNonce": 453191,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
@ -103,14 +103,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1278240551,
|
||||
"versionNonce": 453191,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
@ -148,7 +148,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@ -157,7 +157,7 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 401146281,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
@ -184,14 +184,14 @@ exports[`Test dragCreate > add element to the scene when pointer dragging long e
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1278240551,
|
||||
"versionNonce": 453191,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
|
@ -5,7 +5,7 @@ exports[`Test Linear Elements > Test bound text element > should match styles fo
|
||||
class="excalidraw-wysiwyg"
|
||||
data-type="wysiwyg"
|
||||
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"
|
||||
wrap="off"
|
||||
/>
|
||||
|
@ -18,14 +18,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 1`] = `
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 401146281,
|
||||
"seed": 1014066025,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 2019559783,
|
||||
"versionNonce": 238820263,
|
||||
"width": 30,
|
||||
"x": 30,
|
||||
"y": 20,
|
||||
@ -50,14 +50,14 @@ exports[`duplicate element on move when ALT is clicked > rectangle 2`] = `
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 4,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 1604849351,
|
||||
"width": 30,
|
||||
"x": -10,
|
||||
"y": 60,
|
||||
@ -82,14 +82,14 @@ exports[`move element > rectangle 1`] = `
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 453191,
|
||||
"versionNonce": 1150084233,
|
||||
"width": 30,
|
||||
"x": 0,
|
||||
"y": 40,
|
||||
@ -119,14 +119,14 @@ exports[`move element > rectangles with binding arrow 1`] = `
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 1014066025,
|
||||
"versionNonce": 81784553,
|
||||
"width": 100,
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
@ -156,14 +156,14 @@ exports[`move element > rectangles with binding arrow 2`] = `
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 449462985,
|
||||
"seed": 2019559783,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 6,
|
||||
"versionNonce": 1723083209,
|
||||
"versionNonce": 927333447,
|
||||
"width": 300,
|
||||
"x": 201,
|
||||
"y": 2,
|
||||
@ -205,7 +205,7 @@ exports[`move element > rectangles with binding arrow 3`] = `
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 401146281,
|
||||
"seed": 238820263,
|
||||
"startArrowhead": null,
|
||||
"startBinding": {
|
||||
"elementId": "id0",
|
||||
@ -218,7 +218,7 @@ exports[`move element > rectangles with binding arrow 3`] = `
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 11,
|
||||
"versionNonce": 1006504105,
|
||||
"versionNonce": 1051383431,
|
||||
"width": 81,
|
||||
"x": 110,
|
||||
"y": 49.981789081137734,
|
||||
|
@ -38,7 +38,7 @@ exports[`multi point mode in linear elements > arrow 1`] = `
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@ -47,7 +47,7 @@ exports[`multi point mode in linear elements > arrow 1`] = `
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 1505387817,
|
||||
"width": 70,
|
||||
"x": 30,
|
||||
"y": 30,
|
||||
@ -92,7 +92,7 @@ exports[`multi point mode in linear elements > line 1`] = `
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@ -101,7 +101,7 @@ exports[`multi point mode in linear elements > line 1`] = `
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 7,
|
||||
"versionNonce": 1150084233,
|
||||
"versionNonce": 1505387817,
|
||||
"width": 70,
|
||||
"x": 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": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@ -40,7 +40,7 @@ exports[`select single element on the scene > arrow 1`] = `
|
||||
"type": "arrow",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 401146281,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@ -78,7 +78,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"startArrowhead": null,
|
||||
"startBinding": null,
|
||||
"strokeColor": "#1e1e1e",
|
||||
@ -87,7 +87,7 @@ exports[`select single element on the scene > arrow escape 1`] = `
|
||||
"type": "line",
|
||||
"updated": 1,
|
||||
"version": 3,
|
||||
"versionNonce": 449462985,
|
||||
"versionNonce": 401146281,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@ -112,14 +112,14 @@ exports[`select single element on the scene > diamond 1`] = `
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "diamond",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1278240551,
|
||||
"versionNonce": 453191,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@ -144,14 +144,14 @@ exports[`select single element on the scene > ellipse 1`] = `
|
||||
"roundness": {
|
||||
"type": 2,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "ellipse",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1278240551,
|
||||
"versionNonce": 453191,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
@ -176,14 +176,14 @@ exports[`select single element on the scene > rectangle 1`] = `
|
||||
"roundness": {
|
||||
"type": 3,
|
||||
},
|
||||
"seed": 337897,
|
||||
"seed": 1278240551,
|
||||
"strokeColor": "#1e1e1e",
|
||||
"strokeStyle": "solid",
|
||||
"strokeWidth": 1,
|
||||
"type": "rectangle",
|
||||
"updated": 1,
|
||||
"version": 2,
|
||||
"versionNonce": 1278240551,
|
||||
"versionNonce": 453191,
|
||||
"width": 30,
|
||||
"x": 10,
|
||||
"y": 10,
|
||||
|
@ -24,7 +24,7 @@ import { LibraryItem } from "../types";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const checkpoint = (name: string) => {
|
||||
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
||||
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||
`[${name}] number of renders`,
|
||||
);
|
||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||
@ -40,10 +40,10 @@ const mouse = new Pointer("mouse");
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
});
|
||||
|
||||
@ -52,7 +52,7 @@ const { h } = window;
|
||||
describe("contextMenu element", () => {
|
||||
beforeEach(async () => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
setDateTimeForTests("201933152653");
|
||||
|
||||
@ -75,7 +75,7 @@ describe("contextMenu element", () => {
|
||||
});
|
||||
|
||||
it("shows context menu for canvas", () => {
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -105,7 +105,7 @@ describe("contextMenu element", () => {
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -159,7 +159,7 @@ describe("contextMenu element", () => {
|
||||
API.setSelectedElements([rect1]);
|
||||
|
||||
// lower z-index
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
@ -169,7 +169,7 @@ describe("contextMenu element", () => {
|
||||
|
||||
// higher z-index
|
||||
API.setSelectedElements([rect2]);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 100,
|
||||
clientY: 100,
|
||||
@ -193,7 +193,7 @@ describe("contextMenu element", () => {
|
||||
mouse.click(20, 0);
|
||||
});
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -246,7 +246,7 @@ describe("contextMenu element", () => {
|
||||
Keyboard.keyPress(KEYS.G);
|
||||
});
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -285,7 +285,7 @@ describe("contextMenu element", () => {
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -333,7 +333,7 @@ describe("contextMenu element", () => {
|
||||
mouse.reset();
|
||||
|
||||
// Copy styles of second rectangle
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 40,
|
||||
clientY: 40,
|
||||
@ -346,7 +346,7 @@ describe("contextMenu element", () => {
|
||||
|
||||
mouse.reset();
|
||||
// Paste styles to first rectangle
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
@ -370,7 +370,7 @@ describe("contextMenu element", () => {
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -386,7 +386,7 @@ describe("contextMenu element", () => {
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -407,7 +407,7 @@ describe("contextMenu element", () => {
|
||||
mouse.down(10, 10);
|
||||
mouse.up(20, 20);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -430,7 +430,7 @@ describe("contextMenu element", () => {
|
||||
mouse.up(20, 20);
|
||||
|
||||
mouse.reset();
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 40,
|
||||
clientY: 40,
|
||||
@ -452,7 +452,7 @@ describe("contextMenu element", () => {
|
||||
mouse.up(20, 20);
|
||||
|
||||
mouse.reset();
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
@ -474,7 +474,7 @@ describe("contextMenu element", () => {
|
||||
mouse.up(20, 20);
|
||||
|
||||
mouse.reset();
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 40,
|
||||
clientY: 40,
|
||||
@ -495,7 +495,7 @@ describe("contextMenu element", () => {
|
||||
mouse.up(20, 20);
|
||||
|
||||
mouse.reset();
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 10,
|
||||
clientY: 10,
|
||||
@ -520,7 +520,7 @@ describe("contextMenu element", () => {
|
||||
mouse.click(10, 10);
|
||||
});
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -550,7 +550,7 @@ describe("contextMenu element", () => {
|
||||
Keyboard.keyPress(KEYS.G);
|
||||
});
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
|
@ -15,10 +15,13 @@ import { vi } from "vitest";
|
||||
// Unmount ReactDOM from 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(() => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
});
|
||||
|
||||
@ -32,7 +35,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("rectangle");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -43,7 +46,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -63,7 +67,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("ellipse");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -74,7 +78,9 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -94,7 +100,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("diamond");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -105,7 +111,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -125,7 +132,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("arrow");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -136,7 +143,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -160,7 +168,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("line");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -171,7 +179,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
|
||||
expect(h.elements.length).toEqual(1);
|
||||
@ -203,7 +212,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("rectangle");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -211,7 +220,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -222,7 +232,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("ellipse");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -230,7 +240,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -241,7 +252,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("diamond");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -249,7 +260,8 @@ describe("Test dragCreate", () => {
|
||||
// finish (position does not matter)
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -260,7 +272,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("arrow");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -273,7 +285,8 @@ describe("Test dragCreate", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
@ -284,7 +297,7 @@ describe("Test dragCreate", () => {
|
||||
const tool = getByToolName("line");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
// start from (30, 20)
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
@ -297,7 +310,8 @@ describe("Test dragCreate", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(0);
|
||||
});
|
||||
|
@ -279,7 +279,7 @@ export class API {
|
||||
};
|
||||
|
||||
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) => {
|
||||
try {
|
||||
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) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
private getEvent() {
|
||||
@ -129,18 +129,18 @@ export class Pointer {
|
||||
if (dx !== 0 || dy !== 0) {
|
||||
this.clientX += dx;
|
||||
this.clientY += dy;
|
||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
}
|
||||
|
||||
down(dx = 0, dy = 0) {
|
||||
this.move(dx, dy);
|
||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
up(dx = 0, dy = 0) {
|
||||
this.move(dx, dy);
|
||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
click(dx = 0, dy = 0) {
|
||||
@ -150,7 +150,7 @@ export class Pointer {
|
||||
|
||||
doubleClick(dx = 0, dy = 0) {
|
||||
this.move(dx, dy);
|
||||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
// absolute coords
|
||||
@ -159,19 +159,19 @@ export class Pointer {
|
||||
moveTo(x: number = this.clientX, y: number = this.clientY) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
downAt(x = this.clientX, y = this.clientY) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
upAt(x = this.clientX, y = this.clientY) {
|
||||
this.clientX = x;
|
||||
this.clientY = y;
|
||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
||||
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||
}
|
||||
|
||||
clickAt(x: number, y: number) {
|
||||
@ -180,7 +180,7 @@ export class Pointer {
|
||||
}
|
||||
|
||||
rightClickAt(x: number, y: number) {
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
@ -189,7 +189,7 @@ export class Pointer {
|
||||
|
||||
doubleClickAt(x: number, y: number) {
|
||||
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 = () => {
|
||||
return GlobalTestState.renderResult.container.querySelector(
|
||||
".context-menu",
|
||||
|
@ -26,26 +26,28 @@ import * as textElementUtils from "../element/textElement";
|
||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||
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 font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||
|
||||
describe("Test Linear Elements", () => {
|
||||
let container: HTMLElement;
|
||||
let canvas: HTMLCanvasElement;
|
||||
let interactiveCanvas: HTMLCanvasElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
const comp = await render(<ExcalidrawApp />);
|
||||
h.state.width = 1000;
|
||||
h.state.height = 1000;
|
||||
container = comp.container;
|
||||
canvas = container.querySelector("canvas")!;
|
||||
canvas.width = 1000;
|
||||
canvas.height = 1000;
|
||||
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
||||
});
|
||||
|
||||
const p1: Point = [20, 20];
|
||||
@ -120,26 +122,26 @@ describe("Test Linear Elements", () => {
|
||||
};
|
||||
|
||||
const drag = (startPoint: Point, endPoint: Point) => {
|
||||
fireEvent.pointerDown(canvas, {
|
||||
fireEvent.pointerDown(interactiveCanvas, {
|
||||
clientX: startPoint[0],
|
||||
clientY: startPoint[1],
|
||||
});
|
||||
fireEvent.pointerMove(canvas, {
|
||||
fireEvent.pointerMove(interactiveCanvas, {
|
||||
clientX: endPoint[0],
|
||||
clientY: endPoint[1],
|
||||
});
|
||||
fireEvent.pointerUp(canvas, {
|
||||
fireEvent.pointerUp(interactiveCanvas, {
|
||||
clientX: endPoint[0],
|
||||
clientY: endPoint[1],
|
||||
});
|
||||
};
|
||||
|
||||
const deletePoint = (point: Point) => {
|
||||
fireEvent.pointerDown(canvas, {
|
||||
fireEvent.pointerDown(interactiveCanvas, {
|
||||
clientX: point[0],
|
||||
clientY: point[1],
|
||||
});
|
||||
fireEvent.pointerUp(canvas, {
|
||||
fireEvent.pointerUp(interactiveCanvas, {
|
||||
clientX: point[0],
|
||||
clientY: point[1],
|
||||
});
|
||||
@ -172,12 +174,14 @@ describe("Test Linear Elements", () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
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);
|
||||
|
||||
// drag line from midpoint
|
||||
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).toMatchInlineSnapshot(`
|
||||
[
|
||||
@ -199,14 +203,14 @@ describe("Test Linear Elements", () => {
|
||||
|
||||
it("should allow entering and exiting line editor via context menu", () => {
|
||||
createTwoPointerLinearElement("line");
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: midpoint[0],
|
||||
clientY: midpoint[1],
|
||||
});
|
||||
// Enter line editor
|
||||
let contextMenu = document.querySelector(".context-menu");
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: midpoint[0],
|
||||
clientY: midpoint[1],
|
||||
@ -216,13 +220,13 @@ describe("Test Linear Elements", () => {
|
||||
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||
|
||||
// Exiting line editor
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: midpoint[0],
|
||||
clientY: midpoint[1],
|
||||
});
|
||||
contextMenu = document.querySelector(".context-menu");
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: midpoint[0],
|
||||
clientY: midpoint[1],
|
||||
@ -270,7 +274,8 @@ describe("Test Linear Elements", () => {
|
||||
|
||||
// drag line from midpoint
|
||||
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).toMatchInlineSnapshot(`
|
||||
@ -307,7 +312,9 @@ describe("Test Linear Elements", () => {
|
||||
// update roundness
|
||||
fireEvent.click(screen.getByTitle("Round"));
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(12);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
|
||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||
h.elements[0] as ExcalidrawLinearElement,
|
||||
h.state,
|
||||
@ -351,7 +358,9 @@ describe("Test Linear Elements", () => {
|
||||
// Move the element
|
||||
drag(startPoint, endPoint);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(14);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
|
||||
expect([line.x, line.y]).toEqual([
|
||||
points[0][0] + deltaX,
|
||||
points[0][1] + deltaY,
|
||||
@ -408,7 +417,9 @@ describe("Test Linear Elements", () => {
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(21);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
@ -447,7 +458,8 @@ describe("Test Linear Elements", () => {
|
||||
// Drag from first point
|
||||
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);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
@ -473,7 +485,8 @@ describe("Test Linear Elements", () => {
|
||||
// Drag from first point
|
||||
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);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
@ -507,7 +520,8 @@ describe("Test Linear Elements", () => {
|
||||
// delete 3rd point
|
||||
deletePoint(points[2]);
|
||||
expect(line.points.length).toEqual(3);
|
||||
expect(renderScene).toHaveBeenCalledTimes(22);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
|
||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||
line,
|
||||
@ -553,8 +567,8 @@ describe("Test Linear Elements", () => {
|
||||
lastSegmentMidpoint[0] + delta,
|
||||
lastSegmentMidpoint[1] + delta,
|
||||
]);
|
||||
expect(renderScene).toHaveBeenCalledTimes(21);
|
||||
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(line.points.length).toEqual(5);
|
||||
|
||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||
@ -629,7 +643,8 @@ describe("Test Linear Elements", () => {
|
||||
// Drag from first point
|
||||
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);
|
||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||
@ -870,10 +885,10 @@ describe("Test Linear Elements", () => {
|
||||
]);
|
||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
});
|
||||
|
||||
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)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
"Online whiteboard
|
||||
collaboration made
|
||||
easy"
|
||||
`);
|
||||
});
|
||||
|
||||
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)
|
||||
.toMatchInlineSnapshot(`
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`);
|
||||
"Online whiteboard
|
||||
collaboration made easy"
|
||||
`);
|
||||
expect(LinearElementEditor.getElementAbsoluteCoords(container, true))
|
||||
.toMatchInlineSnapshot(`
|
||||
[
|
||||
@ -1206,7 +1221,7 @@ describe("Test Linear Elements", () => {
|
||||
|
||||
const container = h.elements[0];
|
||||
API.setSelectedElements([container, text]);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
@ -1231,7 +1246,7 @@ describe("Test Linear Elements", () => {
|
||||
mouse.up();
|
||||
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 20,
|
||||
clientY: 30,
|
||||
|
@ -17,10 +17,13 @@ import { vi } from "vitest";
|
||||
// Unmount ReactDOM from 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(() => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
});
|
||||
|
||||
@ -29,7 +32,7 @@ const { h } = window;
|
||||
describe("move element", () => {
|
||||
it("rectangle", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
{
|
||||
// create element
|
||||
@ -39,20 +42,23 @@ describe("move element", () => {
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
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.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(3);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(2);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
||||
@ -78,7 +84,8 @@ describe("move element", () => {
|
||||
// select the second rectangles
|
||||
new Pointer("mouse").clickOn(rectB);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(23);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(16);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(3);
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
@ -87,7 +94,8 @@ describe("move element", () => {
|
||||
expect([line.x, line.y]).toEqual([110, 50]);
|
||||
expect([line.width, line.height]).toEqual([80, 80]);
|
||||
|
||||
renderScene.mockClear();
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
|
||||
// Move selected rectangle
|
||||
Keyboard.keyDown(KEYS.ARROW_RIGHT);
|
||||
@ -95,7 +103,8 @@ describe("move element", () => {
|
||||
Keyboard.keyDown(KEYS.ARROW_DOWN);
|
||||
|
||||
// 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.elements.length).toEqual(3);
|
||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||
@ -111,7 +120,7 @@ describe("move element", () => {
|
||||
describe("duplicate element on move when ALT is clicked", () => {
|
||||
it("rectangle", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
|
||||
{
|
||||
// create element
|
||||
@ -121,13 +130,15 @@ describe("duplicate element on move when ALT is clicked", () => {
|
||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
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 });
|
||||
@ -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
|
||||
// that additional render?
|
||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(2);
|
||||
|
||||
|
@ -15,10 +15,13 @@ import { vi } from "vitest";
|
||||
// Unmount ReactDOM from 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(() => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
});
|
||||
|
||||
@ -39,11 +42,12 @@ describe("remove shape in non linear elements", () => {
|
||||
const tool = getByToolName("rectangle");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
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);
|
||||
});
|
||||
|
||||
@ -53,11 +57,12 @@ describe("remove shape in non linear elements", () => {
|
||||
const tool = getByToolName("ellipse");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
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);
|
||||
});
|
||||
|
||||
@ -67,11 +72,12 @@ describe("remove shape in non linear elements", () => {
|
||||
const tool = getByToolName("diamond");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||
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);
|
||||
});
|
||||
});
|
||||
@ -83,7 +89,7 @@ describe("multi point mode in linear elements", () => {
|
||||
const tool = getByToolName("arrow");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
// first point is added on pointer down
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
||||
|
||||
@ -103,7 +109,8 @@ describe("multi point mode in linear elements", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
@ -126,7 +133,7 @@ describe("multi point mode in linear elements", () => {
|
||||
const tool = getByToolName("line");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
// first point is added on pointer down
|
||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
||||
|
||||
@ -146,7 +153,8 @@ describe("multi point mode in linear elements", () => {
|
||||
key: KEYS.ENTER,
|
||||
});
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(10);
|
||||
expect(h.elements.length).toEqual(1);
|
||||
|
||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { fireEvent, GlobalTestState, toggleMenu, render } from "../test-utils";
|
||||
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 { t } from "../../i18n";
|
||||
import { useMemo } from "react";
|
||||
@ -23,7 +23,7 @@ describe("<Excalidraw/>", () => {
|
||||
).toBe(0);
|
||||
expect(h.state.zenModeEnabled).toBe(false);
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -42,8 +42,8 @@ describe("<Excalidraw/>", () => {
|
||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||
).toBe(0);
|
||||
expect(h.state.zenModeEnabled).toBe(true);
|
||||
screen.debug();
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -95,7 +95,7 @@ describe("<Excalidraw/>", () => {
|
||||
expect(
|
||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||
).toBe(0);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
@ -114,7 +114,7 @@ describe("<Excalidraw/>", () => {
|
||||
expect(
|
||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||
).toBe(0);
|
||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
||||
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||
button: 2,
|
||||
clientX: 1,
|
||||
clientY: 1,
|
||||
|
@ -21,7 +21,7 @@ import { vi } from "vitest";
|
||||
|
||||
const { h } = window;
|
||||
|
||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||
|
||||
const mouse = new Pointer("mouse");
|
||||
const finger1 = new Pointer("touch", 1);
|
||||
@ -33,7 +33,7 @@ const finger2 = new Pointer("touch", 2);
|
||||
* to debug where a test failure came from.
|
||||
*/
|
||||
const checkpoint = (name: string) => {
|
||||
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
||||
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||
`[${name}] number of renders`,
|
||||
);
|
||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||
@ -48,7 +48,7 @@ beforeEach(async () => {
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
setDateTimeForTests("201933152653");
|
||||
|
||||
@ -1056,6 +1056,28 @@ describe("regression tests", () => {
|
||||
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", () => {
|
||||
const rect1 = UI.createElement("rectangle", { x: 0 });
|
||||
const rect2 = UI.createElement("rectangle", { x: 30 });
|
||||
|
@ -14,10 +14,11 @@ import { vi } from "vitest";
|
||||
// Unmount ReactDOM from root
|
||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||
|
||||
const renderScene = vi.spyOn(Renderer, "renderScene");
|
||||
const renderStaticScene = vi.spyOn(Renderer, "renderStaticScene");
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
});
|
||||
|
||||
|
@ -18,10 +18,13 @@ import { vi } from "vitest";
|
||||
// Unmount ReactDOM from 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(() => {
|
||||
localStorage.clear();
|
||||
renderScene.mockClear();
|
||||
renderInteractiveScene.mockClear();
|
||||
renderStaticScene.mockClear();
|
||||
reseed(7);
|
||||
});
|
||||
|
||||
@ -201,7 +204,7 @@ describe("inner box-selection", () => {
|
||||
});
|
||||
h.elements = [rect1, rect2, rect3];
|
||||
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);
|
||||
assertSelectedElements([rect2.id, rect3.id]);
|
||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||
@ -220,10 +223,11 @@ describe("selection element", () => {
|
||||
const tool = getByToolName("selection");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||
const selectionElement = h.state.selectionElement!;
|
||||
expect(selectionElement).not.toBeNull();
|
||||
expect(selectionElement.type).toEqual("selection");
|
||||
@ -240,11 +244,12 @@ describe("selection element", () => {
|
||||
const tool = getByToolName("selection");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(4);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||
const selectionElement = h.state.selectionElement!;
|
||||
expect(selectionElement).not.toBeNull();
|
||||
expect(selectionElement.type).toEqual("selection");
|
||||
@ -261,12 +266,13 @@ describe("selection element", () => {
|
||||
const tool = getByToolName("selection");
|
||||
fireEvent.click(tool);
|
||||
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
});
|
||||
});
|
||||
@ -282,7 +288,7 @@ describe("select single element on the scene", () => {
|
||||
|
||||
it("rectangle", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
{
|
||||
// create element
|
||||
const tool = getByToolName("rectangle");
|
||||
@ -301,7 +307,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -311,7 +318,7 @@ describe("select single element on the scene", () => {
|
||||
|
||||
it("diamond", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
{
|
||||
// create element
|
||||
const tool = getByToolName("diamond");
|
||||
@ -330,7 +337,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -340,7 +348,7 @@ describe("select single element on the scene", () => {
|
||||
|
||||
it("ellipse", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
{
|
||||
// create element
|
||||
const tool = getByToolName("ellipse");
|
||||
@ -359,7 +367,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -369,7 +378,7 @@ describe("select single element on the scene", () => {
|
||||
|
||||
it("arrow", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
{
|
||||
// create element
|
||||
const tool = getByToolName("arrow");
|
||||
@ -401,7 +410,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||
@ -410,7 +420,7 @@ describe("select single element on the scene", () => {
|
||||
|
||||
it("arrow escape", async () => {
|
||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||
const canvas = container.querySelector("canvas")!;
|
||||
const canvas = container.querySelector("canvas.interactive")!;
|
||||
{
|
||||
// create element
|
||||
const tool = getByToolName("line");
|
||||
@ -442,7 +452,8 @@ describe("select single element on the scene", () => {
|
||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||
fireEvent.pointerUp(canvas);
|
||||
|
||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
||||
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||
expect(renderStaticScene).toHaveBeenCalledTimes(7);
|
||||
expect(h.state.selectionElement).toBeNull();
|
||||
expect(h.elements.length).toEqual(1);
|
||||
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
|
||||
// present in DOM)
|
||||
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(() => {
|
||||
const canvas = renderResult.container.querySelector("canvas");
|
||||
const canvas = renderResult.container.querySelector("canvas.static");
|
||||
if (!canvas) {
|
||||
throw new Error("not initialized yet");
|
||||
}
|
||||
|
||||
const interactiveCanvas =
|
||||
renderResult.container.querySelector("canvas.interactive");
|
||||
if (!interactiveCanvas) {
|
||||
throw new Error("not initialized yet");
|
||||
}
|
||||
});
|
||||
|
||||
return renderResult;
|
||||
@ -81,11 +96,17 @@ export class GlobalTestState {
|
||||
*/
|
||||
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 {
|
||||
return null!;
|
||||
}
|
||||
/**
|
||||
* retrieves interactive canvas for currently rendered app instance
|
||||
*/
|
||||
static get interactiveCanvas(): HTMLCanvasElement {
|
||||
return null!;
|
||||
}
|
||||
}
|
||||
|
||||
const initLocalStorage = (data: ImportedDataState) => {
|
||||
|
@ -17,7 +17,9 @@ describe("view mode", () => {
|
||||
|
||||
it("after switching to view mode – cursor type should be pointer", async () => {
|
||||
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 () => {
|
||||
@ -29,7 +31,9 @@ describe("view mode", () => {
|
||||
pointer.move(100, 100);
|
||||
pointer.click();
|
||||
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);
|
||||
// eslint-disable-next-line dot-notation
|
||||
if (pointerType["pointerType"] === "mouse") {
|
||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.MOVE);
|
||||
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||
CURSOR_TYPE.MOVE,
|
||||
);
|
||||
} else {
|
||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
||||
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||
CURSOR_TYPE.GRAB,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
selectedElementIds,
|
||||
});
|
||||
} as AppState);
|
||||
|
||||
return selectedElementIds;
|
||||
};
|
||||
|
53
src/types.ts
53
src/types.ts
@ -104,6 +104,52 @@ export type LastActiveTool =
|
||||
export type SidebarName = 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 = {
|
||||
contextMenu: {
|
||||
items: ContextMenuItems;
|
||||
@ -407,13 +453,13 @@ export type ExportOpts = {
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: UIAppState,
|
||||
files: BinaryFiles,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
canvas: HTMLCanvasElement,
|
||||
) => void;
|
||||
renderCustomUI?: (
|
||||
exportedElements: readonly NonDeletedExcalidrawElement[],
|
||||
appState: UIAppState,
|
||||
files: BinaryFiles,
|
||||
canvas: HTMLCanvasElement | null,
|
||||
canvas: HTMLCanvasElement,
|
||||
) => JSX.Element;
|
||||
};
|
||||
|
||||
@ -458,7 +504,8 @@ export type AppProps = Merge<
|
||||
* in the app, eg Manager. Factored out into a separate type to keep DRY. */
|
||||
export type AppClassProperties = {
|
||||
props: AppProps;
|
||||
canvas: HTMLCanvasElement | null;
|
||||
canvas: HTMLCanvasElement;
|
||||
interactiveCanvas: HTMLCanvasElement | null;
|
||||
focusContainer(): void;
|
||||
library: Library;
|
||||
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 { isEraserActive, isHandToolActive } from "./appState";
|
||||
import { ResolutionType } from "./utility-types";
|
||||
import React from "react";
|
||||
|
||||
let mockDateTime: string | null = null;
|
||||
|
||||
@ -399,22 +400,25 @@ export const updateActiveTool = (
|
||||
};
|
||||
};
|
||||
|
||||
export const resetCursor = (canvas: HTMLCanvasElement | null) => {
|
||||
if (canvas) {
|
||||
canvas.style.cursor = "";
|
||||
export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
|
||||
if (interactiveCanvas) {
|
||||
interactiveCanvas.style.cursor = "";
|
||||
}
|
||||
};
|
||||
|
||||
export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
|
||||
if (canvas) {
|
||||
canvas.style.cursor = cursor;
|
||||
export const setCursor = (
|
||||
interactiveCanvas: HTMLCanvasElement | null,
|
||||
cursor: string,
|
||||
) => {
|
||||
if (interactiveCanvas) {
|
||||
interactiveCanvas.style.cursor = cursor;
|
||||
}
|
||||
};
|
||||
|
||||
let eraserCanvasCache: any;
|
||||
let previewDataURL: string;
|
||||
export const setEraserCursor = (
|
||||
canvas: HTMLCanvasElement | null,
|
||||
interactiveCanvas: HTMLCanvasElement | null,
|
||||
theme: AppState["theme"],
|
||||
) => {
|
||||
const cursorImageSizePx = 20;
|
||||
@ -446,7 +450,7 @@ export const setEraserCursor = (
|
||||
}
|
||||
|
||||
setCursor(
|
||||
canvas,
|
||||
interactiveCanvas,
|
||||
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
||||
cursorImageSizePx / 2
|
||||
}, auto`,
|
||||
@ -454,23 +458,23 @@ export const setEraserCursor = (
|
||||
};
|
||||
|
||||
export const setCursorForShape = (
|
||||
canvas: HTMLCanvasElement | null,
|
||||
interactiveCanvas: HTMLCanvasElement | null,
|
||||
appState: Pick<AppState, "activeTool" | "theme">,
|
||||
) => {
|
||||
if (!canvas) {
|
||||
if (!interactiveCanvas) {
|
||||
return;
|
||||
}
|
||||
if (appState.activeTool.type === "selection") {
|
||||
resetCursor(canvas);
|
||||
resetCursor(interactiveCanvas);
|
||||
} else if (isHandToolActive(appState)) {
|
||||
canvas.style.cursor = CURSOR_TYPE.GRAB;
|
||||
interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
|
||||
} else if (isEraserActive(appState)) {
|
||||
setEraserCursor(canvas, appState.theme);
|
||||
setEraserCursor(interactiveCanvas, appState.theme);
|
||||
// do nothing if image tool is selected which suggests there's
|
||||
// a image-preview set as the cursor
|
||||
// Ignore custom type as well and let host decide
|
||||
} 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);
|
||||
};
|
||||
|
||||
/**
|
||||
* 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"
|
||||
v8-to-istanbul "^9.1.0"
|
||||
|
||||
"@vitest/expect@0.32.2":
|
||||
version "0.32.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.32.2.tgz#8111f6ab1ff3b203efbe3a25e8bb2d160ce4b720"
|
||||
integrity sha512-6q5yzweLnyEv5Zz1fqK5u5E83LU+gOMVBDuxBl2d2Jfx1BAp5M+rZgc5mlyqdnxquyoiOXpXmFNkcGcfFnFH3Q==
|
||||
"@vitest/expect@0.34.1":
|
||||
version "0.34.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/expect/-/expect-0.34.1.tgz#2ba6cb96695f4b4388c6d955423a81afc79b8da0"
|
||||
integrity sha512-q2CD8+XIsQ+tHwypnoCk8Mnv5e6afLFvinVGCq3/BOT4kQdVQmY6rRfyKkwcg635lbliLPqbunXZr+L1ssUWiQ==
|
||||
dependencies:
|
||||
"@vitest/spy" "0.32.2"
|
||||
"@vitest/utils" "0.32.2"
|
||||
"@vitest/spy" "0.34.1"
|
||||
"@vitest/utils" "0.34.1"
|
||||
chai "^4.3.7"
|
||||
|
||||
"@vitest/runner@0.32.2":
|
||||
version "0.32.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.32.2.tgz#18dd979ce4e8766bcc90948d11b4c8ae6ed90b89"
|
||||
integrity sha512-06vEL0C1pomOEktGoLjzZw+1Fb+7RBRhmw/06WkDrd1akkT9i12su0ku+R/0QM69dfkIL/rAIDTG+CSuQVDcKw==
|
||||
"@vitest/runner@0.34.1":
|
||||
version "0.34.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/runner/-/runner-0.34.1.tgz#23c21ba1db8bff610988c72744db590d0fb6c4ba"
|
||||
integrity sha512-YfQMpYzDsYB7yqgmlxZ06NI4LurHWfrH7Wy3Pvf/z/vwUSgq1zLAb1lWcItCzQG+NVox+VvzlKQrYEXb47645g==
|
||||
dependencies:
|
||||
"@vitest/utils" "0.32.2"
|
||||
concordance "^5.0.4"
|
||||
"@vitest/utils" "0.34.1"
|
||||
p-limit "^4.0.0"
|
||||
pathe "^1.1.0"
|
||||
pathe "^1.1.1"
|
||||
|
||||
"@vitest/snapshot@0.32.2":
|
||||
version "0.32.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.32.2.tgz#500b6453e88e4c50a0aded39839352c16b519b9e"
|
||||
integrity sha512-JwhpeH/PPc7GJX38vEfCy9LtRzf9F4er7i4OsAJyV7sjPwjj+AIR8cUgpMTWK4S3TiamzopcTyLsZDMuldoi5A==
|
||||
"@vitest/snapshot@0.34.1":
|
||||
version "0.34.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/snapshot/-/snapshot-0.34.1.tgz#814c65f8e714eaf255f47838541004b2a2ba28e6"
|
||||
integrity sha512-0O9LfLU0114OqdF8lENlrLsnn024Tb1CsS9UwG0YMWY2oGTQfPtkW+B/7ieyv0X9R2Oijhi3caB1xgGgEgclSQ==
|
||||
dependencies:
|
||||
magic-string "^0.30.0"
|
||||
pathe "^1.1.0"
|
||||
pretty-format "^27.5.1"
|
||||
magic-string "^0.30.1"
|
||||
pathe "^1.1.1"
|
||||
pretty-format "^29.5.0"
|
||||
|
||||
"@vitest/spy@0.32.2":
|
||||
version "0.32.2"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.32.2.tgz#f3ef7afe0d34e863b90df7c959fa5af540a6aaf9"
|
||||
integrity sha512-Q/ZNILJ4ca/VzQbRM8ur3Si5Sardsh1HofatG9wsJY1RfEaw0XKP8IVax2lI1qnrk9YPuG9LA2LkZ0EI/3d4ug==
|
||||
"@vitest/spy@0.34.1":
|
||||
version "0.34.1"
|
||||
resolved "https://registry.yarnpkg.com/@vitest/spy/-/spy-0.34.1.tgz#2f77234a3d554c5dea664943f2caaab92d304f3c"
|
||||
integrity sha512-UT4WcI3EAPUNO8n6y9QoEqynGGEPmmRxC+cLzneFFXpmacivjHZsNbiKD88KUScv5DCHVDgdBsLD7O7s1enFcQ==
|
||||
dependencies:
|
||||
tinyspy "^2.1.0"
|
||||
tinyspy "^2.1.1"
|
||||
|
||||
"@vitest/ui@0.32.2":
|
||||
version "0.32.2"
|
||||
@ -2870,6 +2869,15 @@
|
||||
loupe "^2.3.6"
|
||||
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:
|
||||
version "2.0.6"
|
||||
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"
|
||||
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:
|
||||
version "1.1.11"
|
||||
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"
|
||||
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:
|
||||
version "1.0.11"
|
||||
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-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:
|
||||
version "4.3.4"
|
||||
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"
|
||||
integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
|
||||
|
||||
esutils@^2.0.2, esutils@^2.0.3:
|
||||
esutils@^2.0.2:
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
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"
|
||||
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:
|
||||
version "3.2.12"
|
||||
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"
|
||||
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:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
|
||||
@ -5561,13 +5533,6 @@ magic-string@^0.27.0:
|
||||
dependencies:
|
||||
"@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:
|
||||
version "0.30.2"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.2.tgz#dcf04aad3d0d1314bc743d076c50feb29b3c7aca"
|
||||
@ -5582,13 +5547,6 @@ make-dir@^4.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
@ -5660,7 +5618,7 @@ mkdirp@^0.5.6:
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mlly@^1.2.0:
|
||||
mlly@^1.2.0, mlly@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.4.0.tgz#830c10d63f1f97bd8785377b24dc2a15d972832b"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.4.0.tgz#8481c92feffc531ab1e012a8ffc15bdd3a0f4318"
|
||||
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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.3.tgz#a54f06eb245fdcfef53d56f3c0251f1d5c3d01fe"
|
||||
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"
|
||||
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:
|
||||
version "1.3.1"
|
||||
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"
|
||||
integrity sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==
|
||||
|
||||
tinypool@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.5.0.tgz#3861c3069bf71e4f1f5aa2d2e6b3aaacc278961e"
|
||||
integrity sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==
|
||||
tinypool@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/tinypool/-/tinypool-0.7.0.tgz#88053cc99b4a594382af23190c609d93fddf8021"
|
||||
integrity sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==
|
||||
|
||||
tinyspy@^2.1.0:
|
||||
tinyspy@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/tinyspy/-/tinyspy-2.1.1.tgz#9e6371b00c259e5c5b301917ca18c01d40ae558c"
|
||||
integrity sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==
|
||||
@ -7231,15 +7184,15 @@ v8-to-istanbul@^9.1.0:
|
||||
"@types/istanbul-lib-coverage" "^2.0.1"
|
||||
convert-source-map "^1.6.0"
|
||||
|
||||
vite-node@0.32.2:
|
||||
version "0.32.2"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.32.2.tgz#bfccdfeb708b2309ea9e5fe424951c75bb9c0096"
|
||||
integrity sha512-dTQ1DCLwl2aEseov7cfQ+kDMNJpM1ebpyMMMwWzBvLbis8Nla/6c9WQcqpPssTwS6Rp/+U6KwlIj8Eapw4bLdA==
|
||||
vite-node@0.34.1:
|
||||
version "0.34.1"
|
||||
resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.34.1.tgz#144900ca4bd54cc419c501d671350bcbc07eb1ee"
|
||||
integrity sha512-odAZAL9xFMuAg8aWd7nSPT+hU8u2r9gU3LRm9QKjxBEF2rRdWpMuqkrkjvyVQEdNFiBctqr2Gg4uJYizm5Le6w==
|
||||
dependencies:
|
||||
cac "^6.7.14"
|
||||
debug "^4.3.4"
|
||||
mlly "^1.2.0"
|
||||
pathe "^1.1.0"
|
||||
mlly "^1.4.0"
|
||||
pathe "^1.1.1"
|
||||
picocolors "^1.0.0"
|
||||
vite "^3.0.0 || ^4.0.0"
|
||||
|
||||
@ -7321,35 +7274,34 @@ vitest-canvas-mock@0.3.2:
|
||||
dependencies:
|
||||
jest-canvas-mock "~2.4.0"
|
||||
|
||||
vitest@0.32.2:
|
||||
version "0.32.2"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.32.2.tgz#758ce2220f609e240ac054eca7ad11a5140679ab"
|
||||
integrity sha512-hU8GNNuQfwuQmqTLfiKcqEhZY72Zxb7nnN07koCUNmntNxbKQnVbeIS6sqUgR3eXSlbOpit8+/gr1KpqoMgWCQ==
|
||||
vitest@0.34.1:
|
||||
version "0.34.1"
|
||||
resolved "https://registry.yarnpkg.com/vitest/-/vitest-0.34.1.tgz#3ad7f845e7a9fb0d72ab703cae832a54b8469e1e"
|
||||
integrity sha512-G1PzuBEq9A75XSU88yO5G4vPT20UovbC/2osB2KEuV/FisSIIsw7m5y2xMdB7RsAGHAfg2lPmp2qKr3KWliVlQ==
|
||||
dependencies:
|
||||
"@types/chai" "^4.3.5"
|
||||
"@types/chai-subset" "^1.3.3"
|
||||
"@types/node" "*"
|
||||
"@vitest/expect" "0.32.2"
|
||||
"@vitest/runner" "0.32.2"
|
||||
"@vitest/snapshot" "0.32.2"
|
||||
"@vitest/spy" "0.32.2"
|
||||
"@vitest/utils" "0.32.2"
|
||||
acorn "^8.8.2"
|
||||
"@vitest/expect" "0.34.1"
|
||||
"@vitest/runner" "0.34.1"
|
||||
"@vitest/snapshot" "0.34.1"
|
||||
"@vitest/spy" "0.34.1"
|
||||
"@vitest/utils" "0.34.1"
|
||||
acorn "^8.9.0"
|
||||
acorn-walk "^8.2.0"
|
||||
cac "^6.7.14"
|
||||
chai "^4.3.7"
|
||||
concordance "^5.0.4"
|
||||
debug "^4.3.4"
|
||||
local-pkg "^0.4.3"
|
||||
magic-string "^0.30.0"
|
||||
pathe "^1.1.0"
|
||||
magic-string "^0.30.1"
|
||||
pathe "^1.1.1"
|
||||
picocolors "^1.0.0"
|
||||
std-env "^3.3.2"
|
||||
std-env "^3.3.3"
|
||||
strip-literal "^1.0.1"
|
||||
tinybench "^2.5.0"
|
||||
tinypool "^0.5.0"
|
||||
tinypool "^0.7.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"
|
||||
|
||||
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"
|
||||
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:
|
||||
version "2.0.0"
|
||||
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"
|
||||
|
||||
word-wrap@^1.2.3:
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
|
||||
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
|
||||
version "1.2.3"
|
||||
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c"
|
||||
integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==
|
||||
|
||||
workbox-background-sync@7.0.0:
|
||||
version "7.0.0"
|
||||
|
Loading…
x
Reference in New Issue
Block a user