Introducing interactive canvas
Separation of Appstate and RenderConfig for InteractiveCanvas Sepration of static canvas Fixing test type-errors, removing original RenderConfig Deduplication of canvases AppState and RenderConfig Added mutation hook for shared computation between canvases Moved interaction handlers to interactive canvas and closed some fixes Added CanvasWrapper and first render optimisations Optimising selection + frame selection bottlenecks with cache/improved algo Static canvas rendering bottlenecks WIP Cursors regression moved to interactive canvas Regression, adding back render interactive scene callback, adding back throttleRAF to both canvases Fix for scroll back to content & scrollbars Separating renderInteractiveScene and renderScene Common canvas context bootstrap Groups cache fix, mutation elements fix and other smaller fixes Remove getSelectedElements cache Fixing broken tests Updated tests with expected # of renderStaticScene calls, adding group selection edge-case test, other smaller fixes
This commit is contained in:
parent
e57dc405fa
commit
903c94d2ca
@ -422,7 +422,7 @@ export const actionToggleHandTool = register({
|
|||||||
type: "hand",
|
type: "hand",
|
||||||
lastActiveToolBeforeEraser: appState.activeTool,
|
lastActiveToolBeforeEraser: appState.activeTool,
|
||||||
});
|
});
|
||||||
setCursor(app.canvas, CURSOR_TYPE.GRAB);
|
setCursor(app.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -276,6 +276,6 @@ const duplicateElements = (
|
|||||||
getNonDeletedElements(finalElements),
|
getNonDeletedElements(finalElements),
|
||||||
appState,
|
appState,
|
||||||
null,
|
null,
|
||||||
),
|
) as AppState,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -19,7 +19,12 @@ import { AppState } from "../types";
|
|||||||
export const actionFinalize = register({
|
export const actionFinalize = register({
|
||||||
name: "finalize",
|
name: "finalize",
|
||||||
trackEvent: false,
|
trackEvent: false,
|
||||||
perform: (elements, appState, _, { canvas, focusContainer, scene }) => {
|
perform: (
|
||||||
|
elements,
|
||||||
|
appState,
|
||||||
|
_,
|
||||||
|
{ interactiveCanvas, focusContainer, scene },
|
||||||
|
) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
const { elementId, startBindingElement, endBindingElement } =
|
const { elementId, startBindingElement, endBindingElement } =
|
||||||
appState.editingLinearElement;
|
appState.editingLinearElement;
|
||||||
@ -132,7 +137,7 @@ export const actionFinalize = register({
|
|||||||
appState.activeTool.type !== "freedraw") ||
|
appState.activeTool.type !== "freedraw") ||
|
||||||
!multiPointElement
|
!multiPointElement
|
||||||
) {
|
) {
|
||||||
resetCursor(canvas);
|
resetCursor(interactiveCanvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
let activeTool: AppState["activeTool"];
|
let activeTool: AppState["activeTool"];
|
||||||
|
@ -108,7 +108,7 @@ export const actionSetFrameAsActiveTool = register({
|
|||||||
type: "frame",
|
type: "frame",
|
||||||
});
|
});
|
||||||
|
|
||||||
setCursorForShape(app.canvas, {
|
setCursorForShape(app.interactiveCanvas, {
|
||||||
...appState,
|
...appState,
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
});
|
});
|
||||||
|
@ -153,7 +153,7 @@ export const actionGroup = register({
|
|||||||
newGroupId,
|
newGroupId,
|
||||||
{ ...appState, selectedGroupIds: {} },
|
{ ...appState, selectedGroupIds: {} },
|
||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
),
|
) as AppState,
|
||||||
elements: nextElements,
|
elements: nextElements,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
@ -216,7 +216,7 @@ export const actionUngroup = register({
|
|||||||
getNonDeletedElements(nextElements),
|
getNonDeletedElements(nextElements),
|
||||||
appState,
|
appState,
|
||||||
null,
|
null,
|
||||||
);
|
) as AppState;
|
||||||
|
|
||||||
frames.forEach((frame) => {
|
frames.forEach((frame) => {
|
||||||
if (frame) {
|
if (frame) {
|
||||||
|
@ -6,6 +6,7 @@ import { ExcalidrawElement } from "../element/types";
|
|||||||
import { isLinearElement } from "../element/typeChecks";
|
import { isLinearElement } from "../element/typeChecks";
|
||||||
import { LinearElementEditor } from "../element/linearElementEditor";
|
import { LinearElementEditor } from "../element/linearElementEditor";
|
||||||
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
import { excludeElementsInFramesFromSelection } from "../scene/selection";
|
||||||
|
import { AppState } from "../types";
|
||||||
|
|
||||||
export const actionSelectAll = register({
|
export const actionSelectAll = register({
|
||||||
name: "selectAll",
|
name: "selectAll",
|
||||||
@ -43,7 +44,7 @@ export const actionSelectAll = register({
|
|||||||
getNonDeletedElements(elements),
|
getNonDeletedElements(elements),
|
||||||
appState,
|
appState,
|
||||||
app,
|
app,
|
||||||
),
|
) as AppState,
|
||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -213,13 +213,13 @@ export const SelectedShapeActions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const ShapesSwitcher = ({
|
export const ShapesSwitcher = ({
|
||||||
canvas,
|
interactiveCanvas,
|
||||||
activeTool,
|
activeTool,
|
||||||
setAppState,
|
setAppState,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
appState,
|
appState,
|
||||||
}: {
|
}: {
|
||||||
canvas: HTMLCanvasElement | null;
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
activeTool: UIAppState["activeTool"];
|
activeTool: UIAppState["activeTool"];
|
||||||
setAppState: React.Component<any, UIAppState>["setState"];
|
setAppState: React.Component<any, UIAppState>["setState"];
|
||||||
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
onImageAction: (data: { pointerType: PointerType | null }) => void;
|
||||||
@ -269,7 +269,7 @@ export const ShapesSwitcher = ({
|
|||||||
multiElement: null,
|
multiElement: null,
|
||||||
selectedElementIds: {},
|
selectedElementIds: {},
|
||||||
});
|
});
|
||||||
setCursorForShape(canvas, {
|
setCursorForShape(interactiveCanvas, {
|
||||||
...appState,
|
...appState,
|
||||||
activeTool: nextActiveTool,
|
activeTool: nextActiveTool,
|
||||||
});
|
});
|
||||||
|
@ -5,14 +5,14 @@ import { render, queryByTestId } from "../tests/test-utils";
|
|||||||
|
|
||||||
import ExcalidrawApp from "../excalidraw-app";
|
import ExcalidrawApp from "../excalidraw-app";
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
describe("Test <App/>", () => {
|
describe("Test <App/>", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -173,6 +173,7 @@ import {
|
|||||||
getSelectedGroupIds,
|
getSelectedGroupIds,
|
||||||
isElementInGroup,
|
isElementInGroup,
|
||||||
isSelectedViaGroup,
|
isSelectedViaGroup,
|
||||||
|
selectGroups,
|
||||||
selectGroupsForSelectedElements,
|
selectGroupsForSelectedElements,
|
||||||
} from "../groups";
|
} from "../groups";
|
||||||
import History from "../history";
|
import History from "../history";
|
||||||
@ -186,7 +187,7 @@ import {
|
|||||||
KEYS,
|
KEYS,
|
||||||
} from "../keys";
|
} from "../keys";
|
||||||
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
import { distance2d, getGridPoint, isPathALoop } from "../math";
|
||||||
import { isVisibleElement, renderScene } from "../renderer/renderScene";
|
import { isVisibleElement } from "../renderer/renderScene";
|
||||||
import { invalidateShapeForElement } from "../renderer/renderElement";
|
import { invalidateShapeForElement } from "../renderer/renderElement";
|
||||||
import {
|
import {
|
||||||
calculateScrollCenter,
|
calculateScrollCenter,
|
||||||
@ -199,7 +200,7 @@ import {
|
|||||||
isSomeElementSelected,
|
isSomeElementSelected,
|
||||||
} from "../scene";
|
} from "../scene";
|
||||||
import Scene from "../scene/Scene";
|
import Scene from "../scene/Scene";
|
||||||
import { RenderConfig, ScrollBars } from "../scene/types";
|
import { RenderInteractiveSceneCallback, ScrollBars } from "../scene/types";
|
||||||
import { getStateForZoom } from "../scene/zoom";
|
import { getStateForZoom } from "../scene/zoom";
|
||||||
import { findShapeByKey, SHAPES } from "../shapes";
|
import { findShapeByKey, SHAPES } from "../shapes";
|
||||||
import {
|
import {
|
||||||
@ -331,6 +332,11 @@ import { actionWrapTextInContainer } from "../actions/actionBoundText";
|
|||||||
import BraveMeasureTextError from "./BraveMeasureTextError";
|
import BraveMeasureTextError from "./BraveMeasureTextError";
|
||||||
import { activeEyeDropperAtom } from "./EyeDropper";
|
import { activeEyeDropperAtom } from "./EyeDropper";
|
||||||
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
import { isSidebarDockedAtom } from "./Sidebar/Sidebar";
|
||||||
|
import { StaticCanvas, InteractiveCanvas, CanvasesWrapper } from "./canvases";
|
||||||
|
|
||||||
|
// remove this hack when we can sync render & resizeObserver (state update)
|
||||||
|
// to rAF. See #5439
|
||||||
|
window.EXCALIDRAW_THROTTLE_NEXT_RENDER = true;
|
||||||
|
|
||||||
const AppContext = React.createContext<AppClassProperties>(null!);
|
const AppContext = React.createContext<AppClassProperties>(null!);
|
||||||
const AppPropsContext = React.createContext<AppProps>(null!);
|
const AppPropsContext = React.createContext<AppProps>(null!);
|
||||||
@ -400,10 +406,6 @@ let currentScrollBars: ScrollBars = { horizontal: null, vertical: null };
|
|||||||
let touchTimeout = 0;
|
let touchTimeout = 0;
|
||||||
let invalidateContextMenu = false;
|
let invalidateContextMenu = false;
|
||||||
|
|
||||||
// remove this hack when we can sync render & resizeObserver (state update)
|
|
||||||
// to rAF. See #5439
|
|
||||||
let THROTTLE_NEXT_RENDER = true;
|
|
||||||
|
|
||||||
let IS_PLAIN_PASTE = false;
|
let IS_PLAIN_PASTE = false;
|
||||||
let IS_PLAIN_PASTE_TIMER = 0;
|
let IS_PLAIN_PASTE_TIMER = 0;
|
||||||
let PLAIN_PASTE_TOAST_SHOWN = false;
|
let PLAIN_PASTE_TOAST_SHOWN = false;
|
||||||
@ -418,6 +420,7 @@ const gesture: Gesture = {
|
|||||||
|
|
||||||
class App extends React.Component<AppProps, AppState> {
|
class App extends React.Component<AppProps, AppState> {
|
||||||
canvas: AppClassProperties["canvas"] = null;
|
canvas: AppClassProperties["canvas"] = null;
|
||||||
|
interactiveCanvas: AppClassProperties["canvas"] = null;
|
||||||
rc: RoughCanvas | null = null;
|
rc: RoughCanvas | null = null;
|
||||||
unmounted: boolean = false;
|
unmounted: boolean = false;
|
||||||
actionManager: ActionManager;
|
actionManager: ActionManager;
|
||||||
@ -538,65 +541,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.actionManager.registerAction(createRedoAction(this.history));
|
this.actionManager.registerAction(createRedoAction(this.history));
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderCanvas() {
|
|
||||||
const canvasScale = window.devicePixelRatio;
|
|
||||||
const {
|
|
||||||
width: canvasDOMWidth,
|
|
||||||
height: canvasDOMHeight,
|
|
||||||
viewModeEnabled,
|
|
||||||
} = this.state;
|
|
||||||
const canvasWidth = canvasDOMWidth * canvasScale;
|
|
||||||
const canvasHeight = canvasDOMHeight * canvasScale;
|
|
||||||
if (viewModeEnabled) {
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
className="excalidraw__canvas"
|
|
||||||
style={{
|
|
||||||
width: canvasDOMWidth,
|
|
||||||
height: canvasDOMHeight,
|
|
||||||
cursor: CURSOR_TYPE.GRAB,
|
|
||||||
}}
|
|
||||||
width={canvasWidth}
|
|
||||||
height={canvasHeight}
|
|
||||||
ref={this.handleCanvasRef}
|
|
||||||
onContextMenu={(event: React.PointerEvent<HTMLCanvasElement>) =>
|
|
||||||
this.handleCanvasContextMenu(event)
|
|
||||||
}
|
|
||||||
onPointerMove={this.handleCanvasPointerMove}
|
|
||||||
onPointerUp={this.handleCanvasPointerUp}
|
|
||||||
onPointerCancel={this.removePointer}
|
|
||||||
onTouchMove={this.handleTouchMove}
|
|
||||||
onPointerDown={this.handleCanvasPointerDown}
|
|
||||||
>
|
|
||||||
{t("labels.drawingCanvas")}
|
|
||||||
</canvas>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<canvas
|
|
||||||
className="excalidraw__canvas"
|
|
||||||
style={{
|
|
||||||
width: canvasDOMWidth,
|
|
||||||
height: canvasDOMHeight,
|
|
||||||
}}
|
|
||||||
width={canvasWidth}
|
|
||||||
height={canvasHeight}
|
|
||||||
ref={this.handleCanvasRef}
|
|
||||||
onContextMenu={(event: React.PointerEvent<HTMLCanvasElement>) =>
|
|
||||||
this.handleCanvasContextMenu(event)
|
|
||||||
}
|
|
||||||
onPointerDown={this.handleCanvasPointerDown}
|
|
||||||
onDoubleClick={this.handleCanvasDoubleClick}
|
|
||||||
onPointerMove={this.handleCanvasPointerMove}
|
|
||||||
onPointerUp={this.handleCanvasPointerUp}
|
|
||||||
onPointerCancel={this.removePointer}
|
|
||||||
onTouchMove={this.handleTouchMove}
|
|
||||||
>
|
|
||||||
{t("labels.drawingCanvas")}
|
|
||||||
</canvas>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getFrameNameDOMId = (frameElement: ExcalidrawElement) => {
|
private getFrameNameDOMId = (frameElement: ExcalidrawElement) => {
|
||||||
return `${this.id}-frame-name-${frameElement.id}`;
|
return `${this.id}-frame-name-${frameElement.id}`;
|
||||||
};
|
};
|
||||||
@ -782,9 +726,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}}
|
}}
|
||||||
onPointerDown={(event) => this.handleCanvasPointerDown(event)}
|
onPointerDown={(event) => this.handleCanvasPointerDown(event)}
|
||||||
onWheel={(event) => this.handleWheel(event)}
|
onWheel={(event) => this.handleWheel(event)}
|
||||||
onContextMenu={(event: React.PointerEvent<HTMLDivElement>) => {
|
onContextMenu={
|
||||||
this.handleCanvasContextMenu(event);
|
this.handleCanvasContextMenu as (
|
||||||
}}
|
event: React.PointerEvent<HTMLDivElement>,
|
||||||
|
) => void
|
||||||
|
}
|
||||||
onDoubleClick={() => {
|
onDoubleClick={() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
editingFrame: f.id,
|
editingFrame: f.id,
|
||||||
@ -830,6 +776,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
>
|
>
|
||||||
<LayerUI
|
<LayerUI
|
||||||
canvas={this.canvas}
|
canvas={this.canvas}
|
||||||
|
interactiveCanvas={this.interactiveCanvas}
|
||||||
appState={this.state}
|
appState={this.state}
|
||||||
files={this.files}
|
files={this.files}
|
||||||
setAppState={this.setAppState}
|
setAppState={this.setAppState}
|
||||||
@ -888,7 +835,61 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
actionManager={this.actionManager}
|
actionManager={this.actionManager}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<main>{this.renderCanvas()}</main>
|
<CanvasesWrapper
|
||||||
|
appState={this.state}
|
||||||
|
scene={this.scene}
|
||||||
|
>
|
||||||
|
{(elements, versionNonce) => (
|
||||||
|
<>
|
||||||
|
<StaticCanvas
|
||||||
|
canvas={this.canvas}
|
||||||
|
rc={this.rc}
|
||||||
|
elements={elements}
|
||||||
|
versionNonce={versionNonce}
|
||||||
|
selectionNonce={
|
||||||
|
this.state.selectionElement?.versionNonce
|
||||||
|
}
|
||||||
|
appState={this.state}
|
||||||
|
renderConfig={{
|
||||||
|
imageCache: this.imageCache,
|
||||||
|
isExporting: false,
|
||||||
|
renderGrid: true,
|
||||||
|
}}
|
||||||
|
handleCanvasRef={this.handleCanvasRef}
|
||||||
|
/>
|
||||||
|
<InteractiveCanvas
|
||||||
|
canvas={this.interactiveCanvas}
|
||||||
|
elements={elements}
|
||||||
|
versionNonce={versionNonce}
|
||||||
|
selectionNonce={
|
||||||
|
this.state.selectionElement?.versionNonce
|
||||||
|
}
|
||||||
|
appState={this.state}
|
||||||
|
renderInteractiveSceneCallback={
|
||||||
|
this.renderInteractiveSceneCallback
|
||||||
|
}
|
||||||
|
handleCanvasRef={
|
||||||
|
this.handleInteractiveCanvasRef
|
||||||
|
}
|
||||||
|
onContextMenu={
|
||||||
|
this.handleCanvasContextMenu as (
|
||||||
|
event: React.PointerEvent<HTMLCanvasElement>,
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
onPointerMove={this.handleCanvasPointerMove}
|
||||||
|
onPointerUp={this.handleCanvasPointerUp}
|
||||||
|
onPointerCancel={this.removePointer}
|
||||||
|
onTouchMove={this.handleTouchMove}
|
||||||
|
onPointerDown={this.handleCanvasPointerDown}
|
||||||
|
onDoubleClick={
|
||||||
|
this.state.viewModeEnabled
|
||||||
|
? undefined
|
||||||
|
: this.handleCanvasDoubleClick
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</CanvasesWrapper>
|
||||||
{this.renderFrameNames()}
|
{this.renderFrameNames()}
|
||||||
</ExcalidrawActionManagerContext.Provider>
|
</ExcalidrawActionManagerContext.Provider>
|
||||||
</ExcalidrawElementsContext.Provider>
|
</ExcalidrawElementsContext.Provider>
|
||||||
@ -1191,17 +1192,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
if (initialData?.scrollToContent) {
|
if (initialData?.scrollToContent) {
|
||||||
scene.appState = {
|
scene.appState = {
|
||||||
...scene.appState,
|
...scene.appState,
|
||||||
...calculateScrollCenter(
|
...calculateScrollCenter(scene.elements, {
|
||||||
scene.elements,
|
...scene.appState,
|
||||||
{
|
width: this.state.width,
|
||||||
...scene.appState,
|
height: this.state.height,
|
||||||
width: this.state.width,
|
offsetTop: this.state.offsetTop,
|
||||||
height: this.state.height,
|
offsetLeft: this.state.offsetLeft,
|
||||||
offsetTop: this.state.offsetTop,
|
}),
|
||||||
offsetLeft: this.state.offsetLeft,
|
|
||||||
},
|
|
||||||
null,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// FontFaceSet loadingdone event we listen on may not always fire
|
// FontFaceSet loadingdone event we listen on may not always fire
|
||||||
@ -1286,7 +1283,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
if ("ResizeObserver" in window && this.excalidrawContainerRef?.current) {
|
||||||
this.resizeObserver = new ResizeObserver(() => {
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
THROTTLE_NEXT_RENDER = false;
|
window.EXCALIDRAW_THROTTLE_NEXT_RENDER = false;
|
||||||
// recompute device dimensions state
|
// recompute device dimensions state
|
||||||
// ---------------------------------------------------------------------
|
// ---------------------------------------------------------------------
|
||||||
this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
this.refreshDeviceState(this.excalidrawContainerRef.current!);
|
||||||
@ -1351,6 +1348,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.library.destroy();
|
this.library.destroy();
|
||||||
clearTimeout(touchTimeout);
|
clearTimeout(touchTimeout);
|
||||||
isSomeElementSelected.clearCache();
|
isSomeElementSelected.clearCache();
|
||||||
|
selectGroups.clearCache();
|
||||||
touchTimeout = 0;
|
touchTimeout = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1519,7 +1517,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.state.activeTool.type === "eraser" &&
|
this.state.activeTool.type === "eraser" &&
|
||||||
prevState.theme !== this.state.theme
|
prevState.theme !== this.state.theme
|
||||||
) {
|
) {
|
||||||
setEraserCursor(this.canvas, this.state.theme);
|
setEraserCursor(this.interactiveCanvas, this.state.theme);
|
||||||
}
|
}
|
||||||
// Hide hyperlink popup if shown when element type is not selection
|
// Hide hyperlink popup if shown when element type is not selection
|
||||||
if (
|
if (
|
||||||
@ -1615,7 +1613,6 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.renderScene();
|
|
||||||
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
this.history.record(this.state, this.scene.getElementsIncludingDeleted());
|
||||||
|
|
||||||
// Do not notify consumers if we're still loading the scene. Among other
|
// Do not notify consumers if we're still loading the scene. Among other
|
||||||
@ -1631,114 +1628,24 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private renderScene = () => {
|
private renderInteractiveSceneCallback = ({
|
||||||
const cursorButton: {
|
atLeastOneVisibleElement,
|
||||||
[id: string]: string | undefined;
|
scrollBars,
|
||||||
} = {};
|
elements,
|
||||||
const pointerViewportCoords: RenderConfig["remotePointerViewportCoords"] =
|
}: RenderInteractiveSceneCallback) => {
|
||||||
{};
|
if (scrollBars) {
|
||||||
const remoteSelectedElementIds: RenderConfig["remoteSelectedElementIds"] =
|
currentScrollBars = scrollBars;
|
||||||
{};
|
|
||||||
const pointerUsernames: { [id: string]: string } = {};
|
|
||||||
const pointerUserStates: { [id: string]: string } = {};
|
|
||||||
this.state.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,
|
|
||||||
},
|
|
||||||
this.state,
|
|
||||||
);
|
|
||||||
cursorButton[socketId] = user.button;
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderingElements = this.scene
|
|
||||||
.getNonDeletedElements()
|
|
||||||
.filter((element) => {
|
|
||||||
if (isImageElement(element)) {
|
|
||||||
if (
|
|
||||||
// not placed on canvas yet (but in elements array)
|
|
||||||
this.state.pendingImageElementId === element.id
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// don't render text element that's being currently edited (it's
|
|
||||||
// rendered on remote only)
|
|
||||||
return (
|
|
||||||
!this.state.editingElement ||
|
|
||||||
this.state.editingElement.type !== "text" ||
|
|
||||||
element.id !== this.state.editingElement.id
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
const selectionColor = getComputedStyle(
|
|
||||||
document.querySelector(".excalidraw")!,
|
|
||||||
).getPropertyValue("--color-selection");
|
|
||||||
|
|
||||||
renderScene(
|
|
||||||
{
|
|
||||||
elements: renderingElements,
|
|
||||||
appState: this.state,
|
|
||||||
scale: window.devicePixelRatio,
|
|
||||||
rc: this.rc!,
|
|
||||||
canvas: this.canvas!,
|
|
||||||
renderConfig: {
|
|
||||||
selectionColor,
|
|
||||||
scrollX: this.state.scrollX,
|
|
||||||
scrollY: this.state.scrollY,
|
|
||||||
viewBackgroundColor: this.state.viewBackgroundColor,
|
|
||||||
zoom: this.state.zoom,
|
|
||||||
remotePointerViewportCoords: pointerViewportCoords,
|
|
||||||
remotePointerButton: cursorButton,
|
|
||||||
remoteSelectedElementIds,
|
|
||||||
remotePointerUsernames: pointerUsernames,
|
|
||||||
remotePointerUserStates: pointerUserStates,
|
|
||||||
shouldCacheIgnoreZoom: this.state.shouldCacheIgnoreZoom,
|
|
||||||
theme: this.state.theme,
|
|
||||||
imageCache: this.imageCache,
|
|
||||||
isExporting: false,
|
|
||||||
renderScrollbars: false,
|
|
||||||
},
|
|
||||||
callback: ({ atLeastOneVisibleElement, scrollBars }) => {
|
|
||||||
if (scrollBars) {
|
|
||||||
currentScrollBars = scrollBars;
|
|
||||||
}
|
|
||||||
const scrolledOutside =
|
|
||||||
// hide when editing text
|
|
||||||
isTextElement(this.state.editingElement)
|
|
||||||
? false
|
|
||||||
: !atLeastOneVisibleElement && renderingElements.length > 0;
|
|
||||||
if (this.state.scrolledOutside !== scrolledOutside) {
|
|
||||||
this.setState({ scrolledOutside });
|
|
||||||
}
|
|
||||||
|
|
||||||
this.scheduleImageRefresh();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
THROTTLE_NEXT_RENDER && window.EXCALIDRAW_THROTTLE_RENDER === true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!THROTTLE_NEXT_RENDER) {
|
|
||||||
THROTTLE_NEXT_RENDER = true;
|
|
||||||
}
|
}
|
||||||
|
const scrolledOutside =
|
||||||
|
// hide when editing text
|
||||||
|
isTextElement(this.state.editingElement)
|
||||||
|
? false
|
||||||
|
: !atLeastOneVisibleElement && elements.length > 0;
|
||||||
|
if (this.state.scrolledOutside !== scrolledOutside) {
|
||||||
|
this.setState({ scrolledOutside });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scheduleImageRefresh();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onScroll = debounce(() => {
|
private onScroll = debounce(() => {
|
||||||
@ -2301,11 +2208,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scrollY = appState.scrollY;
|
scrollY = appState.scrollY;
|
||||||
} else {
|
} else {
|
||||||
// compute only the viewport location, without any zoom adjustment
|
// compute only the viewport location, without any zoom adjustment
|
||||||
const scroll = calculateScrollCenter(
|
const scroll = calculateScrollCenter(targetElements, this.state);
|
||||||
targetElements,
|
|
||||||
this.state,
|
|
||||||
this.canvas,
|
|
||||||
);
|
|
||||||
scrollX = scroll.scrollX;
|
scrollX = scroll.scrollX;
|
||||||
scrollY = scroll.scrollY;
|
scrollY = scroll.scrollY;
|
||||||
}
|
}
|
||||||
@ -2694,7 +2597,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
|
if (event.key === KEYS.SPACE && gesture.pointers.size === 0) {
|
||||||
isHoldingSpace = true;
|
isHoldingSpace = true;
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2758,11 +2661,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
private onKeyUp = withBatchedUpdates((event: KeyboardEvent) => {
|
||||||
if (event.key === KEYS.SPACE) {
|
if (event.key === KEYS.SPACE) {
|
||||||
if (this.state.viewModeEnabled) {
|
if (this.state.viewModeEnabled) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
} else if (this.state.activeTool.type === "selection") {
|
} else if (this.state.activeTool.type === "selection") {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
this.setState({
|
this.setState({
|
||||||
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
selectedElementIds: makeNextSelectedElementIds({}, this.state),
|
||||||
selectedGroupIds: {},
|
selectedGroupIds: {},
|
||||||
@ -2790,9 +2693,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
) => {
|
) => {
|
||||||
const nextActiveTool = updateActiveTool(this.state, tool);
|
const nextActiveTool = updateActiveTool(this.state, tool);
|
||||||
if (nextActiveTool.type === "hand") {
|
if (nextActiveTool.type === "hand") {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
} else if (!isHoldingSpace) {
|
} else if (!isHoldingSpace) {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
}
|
}
|
||||||
if (isToolIcon(document.activeElement)) {
|
if (isToolIcon(document.activeElement)) {
|
||||||
this.focusContainer();
|
this.focusContainer();
|
||||||
@ -2816,11 +2719,11 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
private setCursor = (cursor: string) => {
|
private setCursor = (cursor: string) => {
|
||||||
setCursor(this.canvas, cursor);
|
setCursor(this.interactiveCanvas, cursor);
|
||||||
};
|
};
|
||||||
|
|
||||||
private resetCursor = () => {
|
private resetCursor = () => {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* returns whether user is making a gesture with >= 2 fingers (points)
|
* returns whether user is making a gesture with >= 2 fingers (points)
|
||||||
@ -2979,7 +2882,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
editingElement: null,
|
editingElement: null,
|
||||||
});
|
});
|
||||||
if (this.state.activeTool.locked) {
|
if (this.state.activeTool.locked) {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.focusContainer();
|
this.focusContainer();
|
||||||
@ -3279,7 +3182,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
|
|
||||||
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
let { x: sceneX, y: sceneY } = viewportCoordsToSceneCoords(
|
||||||
event,
|
event,
|
||||||
@ -3313,7 +3216,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
|
if (!event[KEYS.CTRL_OR_CMD] && !this.state.viewModeEnabled) {
|
||||||
const container = getTextBindableContainerAtPosition(
|
const container = getTextBindableContainerAtPosition(
|
||||||
this.scene.getNonDeletedElements(),
|
this.scene.getNonDeletedElements(),
|
||||||
@ -3528,9 +3431,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
const isOverScrollBar = isPointerOverScrollBars.isOverEither;
|
||||||
if (!this.state.draggingElement && !this.state.multiElement) {
|
if (!this.state.draggingElement && !this.state.multiElement) {
|
||||||
if (isOverScrollBar) {
|
if (isOverScrollBar) {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3552,13 +3455,8 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
editingLinearElement &&
|
editingLinearElement &&
|
||||||
editingLinearElement !== this.state.editingLinearElement
|
editingLinearElement !== this.state.editingLinearElement
|
||||||
) {
|
) {
|
||||||
// Since we are reading from previous state which is not possible with
|
this.setState({
|
||||||
// automatic batching in React 18 hence using flush sync to synchronously
|
editingLinearElement,
|
||||||
// update the state. Check https://github.com/excalidraw/excalidraw/pull/5508 for more details.
|
|
||||||
flushSync(() => {
|
|
||||||
this.setState({
|
|
||||||
editingLinearElement,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (editingLinearElement?.lastUncommittedPoint != null) {
|
if (editingLinearElement?.lastUncommittedPoint != null) {
|
||||||
@ -3593,7 +3491,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
const { points, lastCommittedPoint } = multiElement;
|
const { points, lastCommittedPoint } = multiElement;
|
||||||
const lastPoint = points[points.length - 1];
|
const lastPoint = points[points.length - 1];
|
||||||
|
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
|
|
||||||
if (lastPoint === lastCommittedPoint) {
|
if (lastPoint === lastCommittedPoint) {
|
||||||
// if we haven't yet created a temp point and we're beyond commit-zone
|
// if we haven't yet created a temp point and we're beyond commit-zone
|
||||||
@ -3610,7 +3508,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
points: [...points, [scenePointerX - rx, scenePointerY - ry]],
|
points: [...points, [scenePointerX - rx, scenePointerY - ry]],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
// in this branch, we're inside the commit zone, and no uncommitted
|
// in this branch, we're inside the commit zone, and no uncommitted
|
||||||
// point exists. Thus do nothing (don't add/remove points).
|
// point exists. Thus do nothing (don't add/remove points).
|
||||||
}
|
}
|
||||||
@ -3624,7 +3522,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
lastCommittedPoint[1],
|
lastCommittedPoint[1],
|
||||||
) < LINE_CONFIRM_THRESHOLD
|
) < LINE_CONFIRM_THRESHOLD
|
||||||
) {
|
) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
mutateElement(multiElement, {
|
mutateElement(multiElement, {
|
||||||
points: points.slice(0, -1),
|
points: points.slice(0, -1),
|
||||||
});
|
});
|
||||||
@ -3654,7 +3552,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isPathALoop(points, this.state.zoom.value)) {
|
if (isPathALoop(points, this.state.zoom.value)) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
}
|
}
|
||||||
// update last uncommitted point
|
// update last uncommitted point
|
||||||
mutateElement(multiElement, {
|
mutateElement(multiElement, {
|
||||||
@ -3702,7 +3600,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
elementWithTransformHandleType.transformHandleType
|
elementWithTransformHandleType.transformHandleType
|
||||||
) {
|
) {
|
||||||
setCursor(
|
setCursor(
|
||||||
this.canvas,
|
this.interactiveCanvas,
|
||||||
getCursorForResizingElement(elementWithTransformHandleType),
|
getCursorForResizingElement(elementWithTransformHandleType),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@ -3717,7 +3615,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
if (transformHandleType) {
|
if (transformHandleType) {
|
||||||
setCursor(
|
setCursor(
|
||||||
this.canvas,
|
this.interactiveCanvas,
|
||||||
getCursorForResizingElement({
|
getCursorForResizingElement({
|
||||||
transformHandleType,
|
transformHandleType,
|
||||||
}),
|
}),
|
||||||
@ -3741,7 +3639,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.hitLinkElement &&
|
this.hitLinkElement &&
|
||||||
!this.state.selectedElementIds[this.hitLinkElement.id]
|
!this.state.selectedElementIds[this.hitLinkElement.id]
|
||||||
) {
|
) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
showHyperlinkTooltip(this.hitLinkElement, this.state);
|
showHyperlinkTooltip(this.hitLinkElement, this.state);
|
||||||
} else {
|
} else {
|
||||||
hideHyperlinkToolip();
|
hideHyperlinkToolip();
|
||||||
@ -3755,13 +3653,13 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
this.setState({ showHyperlinkPopup: "info" });
|
this.setState({ showHyperlinkPopup: "info" });
|
||||||
} else if (this.state.activeTool.type === "text") {
|
} else if (this.state.activeTool.type === "text") {
|
||||||
setCursor(
|
setCursor(
|
||||||
this.canvas,
|
this.interactiveCanvas,
|
||||||
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
isTextElement(hitElement) ? CURSOR_TYPE.TEXT : CURSOR_TYPE.CROSSHAIR,
|
||||||
);
|
);
|
||||||
} else if (this.state.viewModeEnabled) {
|
} else if (this.state.viewModeEnabled) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
} else if (isOverScrollBar) {
|
} else if (isOverScrollBar) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (this.state.selectedLinearElement) {
|
} else if (this.state.selectedLinearElement) {
|
||||||
this.handleHoverSelectedLinearElement(
|
this.handleHoverSelectedLinearElement(
|
||||||
this.state.selectedLinearElement,
|
this.state.selectedLinearElement,
|
||||||
@ -3780,10 +3678,10 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
)) &&
|
)) &&
|
||||||
!hitElement?.locked
|
!hitElement?.locked
|
||||||
) {
|
) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -3921,9 +3819,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
if (hoverPointIndex >= 0 || segmentMidPointHoveredCoords) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
shouldShowBoundingBox([element], this.state) &&
|
shouldShowBoundingBox([element], this.state) &&
|
||||||
@ -3935,7 +3833,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scenePointerY,
|
scenePointerY,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
} else if (
|
} else if (
|
||||||
boundTextElement &&
|
boundTextElement &&
|
||||||
hitTest(
|
hitTest(
|
||||||
@ -3946,7 +3844,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
scenePointerY,
|
scenePointerY,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.MOVE);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.MOVE);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@ -3974,7 +3872,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private handleCanvasPointerDown = (
|
private handleCanvasPointerDown = (
|
||||||
@ -4128,7 +4026,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
);
|
);
|
||||||
} else if (this.state.activeTool.type === "image") {
|
} else if (this.state.activeTool.type === "image") {
|
||||||
// reset image preview on pointerdown
|
// reset image preview on pointerdown
|
||||||
setCursor(this.canvas, CURSOR_TYPE.CROSSHAIR);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.CROSSHAIR);
|
||||||
|
|
||||||
// retrieve the latest element as the state may be stale
|
// retrieve the latest element as the state may be stale
|
||||||
const pendingImageElement =
|
const pendingImageElement =
|
||||||
@ -4158,7 +4056,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
pointerDownState,
|
pointerDownState,
|
||||||
);
|
);
|
||||||
} else if (this.state.activeTool.type === "custom") {
|
} else if (this.state.activeTool.type === "custom") {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.AUTO);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.AUTO);
|
||||||
} else if (this.state.activeTool.type === "frame") {
|
} else if (this.state.activeTool.type === "frame") {
|
||||||
this.createFrameElementOnPointerDown(pointerDownState);
|
this.createFrameElementOnPointerDown(pointerDownState);
|
||||||
} else if (
|
} else if (
|
||||||
@ -4289,7 +4187,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
let nextPastePrevented = false;
|
let nextPastePrevented = false;
|
||||||
const isLinux = /Linux/.test(window.navigator.platform);
|
const isLinux = /Linux/.test(window.navigator.platform);
|
||||||
|
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRABBING);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRABBING);
|
||||||
let { clientX: lastX, clientY: lastY } = event;
|
let { clientX: lastX, clientY: lastY } = event;
|
||||||
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
|
const onPointerMove = withBatchedUpdatesThrottled((event: PointerEvent) => {
|
||||||
const deltaX = lastX - event.clientX;
|
const deltaX = lastX - event.clientX;
|
||||||
@ -4342,9 +4240,9 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
isPanning = false;
|
isPanning = false;
|
||||||
if (!isHoldingSpace) {
|
if (!isHoldingSpace) {
|
||||||
if (this.state.viewModeEnabled) {
|
if (this.state.viewModeEnabled) {
|
||||||
setCursor(this.canvas, CURSOR_TYPE.GRAB);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.GRAB);
|
||||||
} else {
|
} else {
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
@ -4467,7 +4365,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
|
|
||||||
const onPointerUp = withBatchedUpdates(() => {
|
const onPointerUp = withBatchedUpdates(() => {
|
||||||
isDraggingScrollBar = false;
|
isDraggingScrollBar = false;
|
||||||
setCursorForShape(this.canvas, this.state);
|
setCursorForShape(this.interactiveCanvas, this.state);
|
||||||
lastPointerUp = null;
|
lastPointerUp = null;
|
||||||
this.setState({
|
this.setState({
|
||||||
cursorButton: "up",
|
cursorButton: "up",
|
||||||
@ -4822,7 +4720,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
container,
|
container,
|
||||||
});
|
});
|
||||||
|
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
if (!this.state.activeTool.locked) {
|
if (!this.state.activeTool.locked) {
|
||||||
this.setState({
|
this.setState({
|
||||||
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
activeTool: updateActiveTool(this.state, { type: "selection" }),
|
||||||
@ -4984,7 +4882,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
mutateElement(multiElement, {
|
mutateElement(multiElement, {
|
||||||
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
lastCommittedPoint: multiElement.points[multiElement.points.length - 1],
|
||||||
});
|
});
|
||||||
setCursor(this.canvas, CURSOR_TYPE.POINTER);
|
setCursor(this.interactiveCanvas, CURSOR_TYPE.POINTER);
|
||||||
} else {
|
} else {
|
||||||
const [gridX, gridY] = getGridPoint(
|
const [gridX, gridY] = getGridPoint(
|
||||||
pointerDownState.origin.x,
|
pointerDownState.origin.x,
|
||||||
@ -5876,7 +5774,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
this.setState({ suggestedBindings: [], startBoundElement: null });
|
this.setState({ suggestedBindings: [], startBoundElement: null });
|
||||||
if (!activeTool.locked) {
|
if (!activeTool.locked) {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
this.setState((prevState) => ({
|
this.setState((prevState) => ({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
activeTool: updateActiveTool(this.state, {
|
activeTool: updateActiveTool(this.state, {
|
||||||
@ -6371,7 +6269,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
if (!activeTool.locked && activeTool.type !== "freedraw") {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
this.setState({
|
this.setState({
|
||||||
draggingElement: null,
|
draggingElement: null,
|
||||||
suggestedBindings: [],
|
suggestedBindings: [],
|
||||||
@ -6463,7 +6361,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
const mimeType = imageFile.type;
|
const mimeType = imageFile.type;
|
||||||
|
|
||||||
setCursor(this.canvas, "wait");
|
setCursor(this.interactiveCanvas, "wait");
|
||||||
|
|
||||||
if (mimeType === MIME_TYPES.svg) {
|
if (mimeType === MIME_TYPES.svg) {
|
||||||
try {
|
try {
|
||||||
@ -6563,7 +6461,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
reject(new Error(t("errors.imageInsertError")));
|
reject(new Error(t("errors.imageInsertError")));
|
||||||
} finally {
|
} finally {
|
||||||
if (!showCursorImagePreview) {
|
if (!showCursorImagePreview) {
|
||||||
resetCursor(this.canvas);
|
resetCursor(this.interactiveCanvas);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -6632,7 +6530,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (this.state.pendingImageElementId) {
|
if (this.state.pendingImageElementId) {
|
||||||
setCursor(this.canvas, `url(${previewDataURL}) 4 4, auto`);
|
setCursor(this.interactiveCanvas, `url(${previewDataURL}) 4 4, auto`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -6912,21 +6810,40 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private handleInteractiveCanvasRef = (canvas: HTMLCanvasElement) => {
|
||||||
|
// canvas is null when unmounting
|
||||||
|
if (canvas !== null) {
|
||||||
|
this.interactiveCanvas = canvas;
|
||||||
|
|
||||||
|
this.interactiveCanvas.addEventListener(EVENT.WHEEL, this.handleWheel, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
this.interactiveCanvas.addEventListener(
|
||||||
|
EVENT.TOUCH_START,
|
||||||
|
this.onTapStart,
|
||||||
|
);
|
||||||
|
this.interactiveCanvas.addEventListener(EVENT.TOUCH_END, this.onTapEnd);
|
||||||
|
} else {
|
||||||
|
this.interactiveCanvas?.removeEventListener(
|
||||||
|
EVENT.WHEEL,
|
||||||
|
this.handleWheel,
|
||||||
|
);
|
||||||
|
this.interactiveCanvas?.removeEventListener(
|
||||||
|
EVENT.TOUCH_START,
|
||||||
|
this.onTapStart,
|
||||||
|
);
|
||||||
|
this.interactiveCanvas?.removeEventListener(
|
||||||
|
EVENT.TOUCH_END,
|
||||||
|
this.onTapEnd,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private handleCanvasRef = (canvas: HTMLCanvasElement) => {
|
private handleCanvasRef = (canvas: HTMLCanvasElement) => {
|
||||||
// canvas is null when unmounting
|
// canvas is null when unmounting
|
||||||
if (canvas !== null) {
|
if (canvas !== null) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.rc = rough.canvas(this.canvas);
|
this.rc = rough.canvas(this.canvas);
|
||||||
|
|
||||||
this.canvas.addEventListener(EVENT.WHEEL, this.handleWheel, {
|
|
||||||
passive: false,
|
|
||||||
});
|
|
||||||
this.canvas.addEventListener(EVENT.TOUCH_START, this.onTapStart);
|
|
||||||
this.canvas.addEventListener(EVENT.TOUCH_END, this.onTapEnd);
|
|
||||||
} else {
|
|
||||||
this.canvas?.removeEventListener(EVENT.WHEEL, this.handleWheel);
|
|
||||||
this.canvas?.removeEventListener(EVENT.TOUCH_START, this.onTapStart);
|
|
||||||
this.canvas?.removeEventListener(EVENT.TOUCH_END, this.onTapEnd);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -7108,7 +7025,7 @@ class App extends React.Component<AppProps, AppState> {
|
|||||||
)
|
)
|
||||||
: this.state),
|
: this.state),
|
||||||
showHyperlinkPopup: false,
|
showHyperlinkPopup: false,
|
||||||
},
|
} as AppState,
|
||||||
() => {
|
() => {
|
||||||
this.setState({
|
this.setState({
|
||||||
contextMenu: { top, left, items: this.getContextMenuItems(type) },
|
contextMenu: { top, left, items: this.getContextMenuItems(type) },
|
||||||
|
@ -58,6 +58,7 @@ interface LayerUIProps {
|
|||||||
appState: UIAppState;
|
appState: UIAppState;
|
||||||
files: BinaryFiles;
|
files: BinaryFiles;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
setAppState: React.Component<any, AppState>["setState"];
|
setAppState: React.Component<any, AppState>["setState"];
|
||||||
elements: readonly NonDeletedExcalidrawElement[];
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
@ -117,6 +118,7 @@ const LayerUI = ({
|
|||||||
setAppState,
|
setAppState,
|
||||||
elements,
|
elements,
|
||||||
canvas,
|
canvas,
|
||||||
|
interactiveCanvas,
|
||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
@ -272,7 +274,7 @@ const LayerUI = ({
|
|||||||
|
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onImageAction={({ pointerType }) => {
|
onImageAction={({ pointerType }) => {
|
||||||
@ -413,7 +415,7 @@ const LayerUI = ({
|
|||||||
onLockToggle={onLockToggle}
|
onLockToggle={onLockToggle}
|
||||||
onHandToolToggle={onHandToolToggle}
|
onHandToolToggle={onHandToolToggle}
|
||||||
onPenModeToggle={onPenModeToggle}
|
onPenModeToggle={onPenModeToggle}
|
||||||
canvas={canvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
onImageAction={onImageAction}
|
onImageAction={onImageAction}
|
||||||
renderTopRightUI={renderTopRightUI}
|
renderTopRightUI={renderTopRightUI}
|
||||||
renderCustomStats={renderCustomStats}
|
renderCustomStats={renderCustomStats}
|
||||||
@ -464,7 +466,7 @@ const LayerUI = ({
|
|||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState((appState) => ({
|
setAppState((appState) => ({
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
...calculateScrollCenter(elements, appState),
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -507,8 +509,18 @@ const areEqual = (prevProps: LayerUIProps, nextProps: LayerUIProps) => {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { canvas: _prevCanvas, appState: prevAppState, ...prev } = prevProps;
|
const {
|
||||||
const { canvas: _nextCanvas, appState: nextAppState, ...next } = nextProps;
|
canvas: _pC,
|
||||||
|
interactiveCanvas: _pIC,
|
||||||
|
appState: prevAppState,
|
||||||
|
...prev
|
||||||
|
} = prevProps;
|
||||||
|
const {
|
||||||
|
canvas: _nC,
|
||||||
|
interactiveCanvas: _nIC,
|
||||||
|
appState: nextAppState,
|
||||||
|
...next
|
||||||
|
} = nextProps;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
isShallowEqual(
|
isShallowEqual(
|
||||||
|
@ -36,7 +36,7 @@ type MobileMenuProps = {
|
|||||||
onLockToggle: () => void;
|
onLockToggle: () => void;
|
||||||
onHandToolToggle: () => void;
|
onHandToolToggle: () => void;
|
||||||
onPenModeToggle: () => void;
|
onPenModeToggle: () => void;
|
||||||
canvas: HTMLCanvasElement | null;
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
|
|
||||||
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
onImageAction: (data: { insertOnCanvasDirectly: boolean }) => void;
|
||||||
renderTopRightUI?: (
|
renderTopRightUI?: (
|
||||||
@ -58,7 +58,7 @@ export const MobileMenu = ({
|
|||||||
onLockToggle,
|
onLockToggle,
|
||||||
onHandToolToggle,
|
onHandToolToggle,
|
||||||
onPenModeToggle,
|
onPenModeToggle,
|
||||||
canvas,
|
interactiveCanvas,
|
||||||
onImageAction,
|
onImageAction,
|
||||||
renderTopRightUI,
|
renderTopRightUI,
|
||||||
renderCustomStats,
|
renderCustomStats,
|
||||||
@ -85,7 +85,7 @@ export const MobileMenu = ({
|
|||||||
<Stack.Row gap={1}>
|
<Stack.Row gap={1}>
|
||||||
<ShapesSwitcher
|
<ShapesSwitcher
|
||||||
appState={appState}
|
appState={appState}
|
||||||
canvas={canvas}
|
interactiveCanvas={interactiveCanvas}
|
||||||
activeTool={appState.activeTool}
|
activeTool={appState.activeTool}
|
||||||
setAppState={setAppState}
|
setAppState={setAppState}
|
||||||
onImageAction={({ pointerType }) => {
|
onImageAction={({ pointerType }) => {
|
||||||
@ -202,7 +202,7 @@ export const MobileMenu = ({
|
|||||||
className="scroll-back-to-content"
|
className="scroll-back-to-content"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setAppState((appState) => ({
|
setAppState((appState) => ({
|
||||||
...calculateScrollCenter(elements, appState, canvas),
|
...calculateScrollCenter(elements, appState),
|
||||||
}));
|
}));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
25
src/components/canvases/CanvasesWrapper.tsx
Normal file
25
src/components/canvases/CanvasesWrapper.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { ReactNode } from "react";
|
||||||
|
import { useMutatedElements } from "../../hooks/useMutatedElements";
|
||||||
|
import { AppState } from "../../types";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../../element/types";
|
||||||
|
import Scene from "../../scene/Scene";
|
||||||
|
|
||||||
|
type CanvasesWrapperProps = {
|
||||||
|
appState: AppState;
|
||||||
|
scene: Scene;
|
||||||
|
children: (
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
|
versionNonce: number | undefined,
|
||||||
|
) => ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CanvasesWrapper = (props: CanvasesWrapperProps) => {
|
||||||
|
const [elements, versionNonce] = useMutatedElements({
|
||||||
|
appState: props.appState,
|
||||||
|
scene: props.scene,
|
||||||
|
});
|
||||||
|
|
||||||
|
return <main>{props.children(elements, versionNonce)}</main>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CanvasesWrapper;
|
187
src/components/canvases/InteractiveCanvas.tsx
Normal file
187
src/components/canvases/InteractiveCanvas.tsx
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { renderInteractiveScene } from "../../renderer/renderScene";
|
||||||
|
import { 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[];
|
||||||
|
versionNonce: number | undefined;
|
||||||
|
selectionNonce: number | undefined;
|
||||||
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderInteractiveSceneCallback: (
|
||||||
|
data: RenderInteractiveSceneCallback,
|
||||||
|
) => void;
|
||||||
|
handleCanvasRef: (canvas: HTMLCanvasElement) => void;
|
||||||
|
onContextMenu: DOMAttributes<HTMLCanvasElement>["onContextMenu"];
|
||||||
|
onPointerMove: DOMAttributes<HTMLCanvasElement>["onPointerMove"];
|
||||||
|
onPointerUp: DOMAttributes<HTMLCanvasElement>["onPointerUp"];
|
||||||
|
onPointerCancel: DOMAttributes<HTMLCanvasElement>["onPointerCancel"];
|
||||||
|
onTouchMove: DOMAttributes<HTMLCanvasElement>["onTouchMove"];
|
||||||
|
onPointerDown: DOMAttributes<HTMLCanvasElement>["onPointerDown"];
|
||||||
|
onDoubleClick: DOMAttributes<HTMLCanvasElement>["onDoubleClick"];
|
||||||
|
};
|
||||||
|
|
||||||
|
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(
|
||||||
|
{
|
||||||
|
scale: window.devicePixelRatio,
|
||||||
|
elements: props.elements,
|
||||||
|
canvas: props.canvas,
|
||||||
|
appState: props.appState,
|
||||||
|
renderConfig: {
|
||||||
|
remotePointerViewportCoords: pointerViewportCoords,
|
||||||
|
remotePointerButton: cursorButton,
|
||||||
|
remoteSelectedElementIds,
|
||||||
|
remotePointerUsernames: pointerUsernames,
|
||||||
|
remotePointerUserStates: pointerUserStates,
|
||||||
|
selectionColor,
|
||||||
|
renderScrollbars: false,
|
||||||
|
},
|
||||||
|
callback: props.renderInteractiveSceneCallback,
|
||||||
|
},
|
||||||
|
window.EXCALIDRAW_THROTTLE_NEXT_RENDER &&
|
||||||
|
window.EXCALIDRAW_THROTTLE_RENDER === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!window.EXCALIDRAW_THROTTLE_NEXT_RENDER) {
|
||||||
|
window.EXCALIDRAW_THROTTLE_NEXT_RENDER = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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 * window.devicePixelRatio}
|
||||||
|
height={props.appState.height * window.devicePixelRatio}
|
||||||
|
ref={props.handleCanvasRef}
|
||||||
|
onContextMenu={props.onContextMenu}
|
||||||
|
onPointerMove={props.onPointerMove}
|
||||||
|
onPointerUp={props.onPointerUp}
|
||||||
|
onPointerCancel={props.onPointerCancel}
|
||||||
|
onTouchMove={props.onTouchMove}
|
||||||
|
onPointerDown={props.onPointerDown}
|
||||||
|
onDoubleClick={props.onDoubleClick}
|
||||||
|
>
|
||||||
|
{t("labels.drawingCanvas")}
|
||||||
|
</canvas>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripIrrelevantAppStateProps = (
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
const areEqual = (
|
||||||
|
prevProps: InteractiveCanvasProps,
|
||||||
|
nextProps: InteractiveCanvasProps,
|
||||||
|
) => {
|
||||||
|
// This could be further optimised if needed, as we don't have to render interactive canvas on each mutation
|
||||||
|
if (
|
||||||
|
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
||||||
|
prevProps.versionNonce !== nextProps.versionNonce
|
||||||
|
) {
|
||||||
|
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
|
||||||
|
stripIrrelevantAppStateProps(prevProps.appState as AppState),
|
||||||
|
stripIrrelevantAppStateProps(nextProps.appState as AppState),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(InteractiveCanvas, areEqual);
|
104
src/components/canvases/StaticCanvas.tsx
Normal file
104
src/components/canvases/StaticCanvas.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React, { useEffect, useRef } from "react";
|
||||||
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
|
import { renderStaticScene } from "../../renderer/renderScene";
|
||||||
|
import { 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 | null;
|
||||||
|
rc: RoughCanvas | null;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
versionNonce: number | undefined;
|
||||||
|
selectionNonce: number | undefined;
|
||||||
|
appState: StaticCanvasAppState;
|
||||||
|
renderConfig: StaticCanvasRenderConfig;
|
||||||
|
handleCanvasRef: (canvas: HTMLCanvasElement) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const StaticCanvas = (props: StaticCanvasProps) => {
|
||||||
|
const isComponentMounted = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isComponentMounted.current) {
|
||||||
|
isComponentMounted.current = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderStaticScene(
|
||||||
|
{
|
||||||
|
scale: window.devicePixelRatio,
|
||||||
|
elements: props.elements,
|
||||||
|
canvas: props.canvas,
|
||||||
|
rc: props.rc!,
|
||||||
|
appState: props.appState,
|
||||||
|
renderConfig: props.renderConfig,
|
||||||
|
},
|
||||||
|
window.EXCALIDRAW_THROTTLE_NEXT_RENDER &&
|
||||||
|
window.EXCALIDRAW_THROTTLE_RENDER === true,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!window.EXCALIDRAW_THROTTLE_NEXT_RENDER) {
|
||||||
|
window.EXCALIDRAW_THROTTLE_NEXT_RENDER = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<canvas
|
||||||
|
className="excalidraw__canvas static"
|
||||||
|
style={{
|
||||||
|
width: props.appState.width,
|
||||||
|
height: props.appState.height,
|
||||||
|
pointerEvents: "none",
|
||||||
|
}}
|
||||||
|
width={props.appState.width * window.devicePixelRatio}
|
||||||
|
height={props.appState.height * window.devicePixelRatio}
|
||||||
|
ref={props.handleCanvasRef}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stripIrrelevantAppStateProps = (
|
||||||
|
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) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isShallowEqual(
|
||||||
|
// asserting AppState because we're being passed the whole AppState
|
||||||
|
// but resolve to only the InteractiveCanvas-relevant props
|
||||||
|
stripIrrelevantAppStateProps(prevProps.appState as AppState),
|
||||||
|
stripIrrelevantAppStateProps(nextProps.appState as AppState),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default React.memo(StaticCanvas, areEqual);
|
5
src/components/canvases/index.tsx
Normal file
5
src/components/canvases/index.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import CanvasesWrapper from "./CanvasesWrapper";
|
||||||
|
import InteractiveCanvas from "./InteractiveCanvas";
|
||||||
|
import StaticCanvas from "./StaticCanvas";
|
||||||
|
|
||||||
|
export { CanvasesWrapper, InteractiveCanvas, StaticCanvas };
|
@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--zIndex-canvas: 1;
|
--zIndex-canvas: 1;
|
||||||
--zIndex-wysiwyg: 2;
|
--zIndex-interactiveCanvas: 2;
|
||||||
--zIndex-layerUI: 3;
|
--zIndex-wysiwyg: 3;
|
||||||
|
--zIndex-layerUI: 4;
|
||||||
|
|
||||||
--zIndex-modal: 1000;
|
--zIndex-modal: 1000;
|
||||||
--zIndex-popup: 1001;
|
--zIndex-popup: 1001;
|
||||||
@ -69,6 +70,10 @@
|
|||||||
|
|
||||||
z-index: var(--zIndex-canvas);
|
z-index: var(--zIndex-canvas);
|
||||||
|
|
||||||
|
&.interactive {
|
||||||
|
z-index: var(--zIndex-interactiveCanvas);
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the main canvas from document flow to avoid resizeObserver
|
// Remove the main canvas from document flow to avoid resizeObserver
|
||||||
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
// feedback loop (see https://github.com/excalidraw/excalidraw/pull/3379)
|
||||||
}
|
}
|
||||||
|
@ -144,11 +144,7 @@ export const loadSceneOrLibraryFromBlob = async (
|
|||||||
fileHandle: fileHandle || blob.handle || null,
|
fileHandle: fileHandle || blob.handle || null,
|
||||||
...cleanAppStateForExport(data.appState || {}),
|
...cleanAppStateForExport(data.appState || {}),
|
||||||
...(localAppState
|
...(localAppState
|
||||||
? calculateScrollCenter(
|
? calculateScrollCenter(data.elements || [], localAppState)
|
||||||
data.elements || [],
|
|
||||||
localAppState,
|
|
||||||
null,
|
|
||||||
)
|
|
||||||
: {}),
|
: {}),
|
||||||
},
|
},
|
||||||
files: data.files,
|
files: data.files,
|
||||||
|
@ -293,7 +293,7 @@ export const getContextMenuLabel = (
|
|||||||
export const getLinkHandleFromCoords = (
|
export const getLinkHandleFromCoords = (
|
||||||
[x1, y1, x2, y2]: Bounds,
|
[x1, y1, x2, y2]: Bounds,
|
||||||
angle: number,
|
angle: number,
|
||||||
appState: UIAppState,
|
appState: Pick<UIAppState, "zoom">,
|
||||||
): [x: number, y: number, width: number, height: number] => {
|
): [x: number, y: number, width: number, height: number] => {
|
||||||
const size = DEFAULT_LINK_SIZE;
|
const size = DEFAULT_LINK_SIZE;
|
||||||
const linkWidth = size / appState.zoom.value;
|
const linkWidth = size / appState.zoom.value;
|
||||||
|
@ -474,6 +474,7 @@ const maybeCalculateNewGapWhenScaling = (
|
|||||||
return { elementId, gap: newGap, focus };
|
return { elementId, gap: newGap, focus };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: this is a bottleneck, optimise
|
||||||
export const getEligibleElementsForBinding = (
|
export const getEligibleElementsForBinding = (
|
||||||
elements: NonDeleted<ExcalidrawElement>[],
|
elements: NonDeleted<ExcalidrawElement>[],
|
||||||
): SuggestedBinding[] => {
|
): SuggestedBinding[] => {
|
||||||
|
@ -25,7 +25,12 @@ import {
|
|||||||
getElementPointsCoords,
|
getElementPointsCoords,
|
||||||
getMinMaxXYFromCurvePathOps,
|
getMinMaxXYFromCurvePathOps,
|
||||||
} from "./bounds";
|
} from "./bounds";
|
||||||
import { Point, AppState, PointerCoords } from "../types";
|
import {
|
||||||
|
Point,
|
||||||
|
AppState,
|
||||||
|
PointerCoords,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
import { mutateElement } from "./mutateElement";
|
import { mutateElement } from "./mutateElement";
|
||||||
import History from "../history";
|
import History from "../history";
|
||||||
|
|
||||||
@ -398,7 +403,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static getEditorMidPoints = (
|
static getEditorMidPoints = (
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
): typeof editorMidPointsCache["points"] => {
|
): typeof editorMidPointsCache["points"] => {
|
||||||
const boundText = getBoundTextElement(element);
|
const boundText = getBoundTextElement(element);
|
||||||
|
|
||||||
@ -422,7 +427,7 @@ export class LinearElementEditor {
|
|||||||
|
|
||||||
static updateEditorMidPointsCache = (
|
static updateEditorMidPointsCache = (
|
||||||
element: NonDeleted<ExcalidrawLinearElement>,
|
element: NonDeleted<ExcalidrawLinearElement>,
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
const points = LinearElementEditor.getPointsGlobalCoordinates(element);
|
||||||
|
|
||||||
|
@ -759,7 +759,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect(h.elements[1].type).toBe("text");
|
expect(h.elements[1].type).toBe("text");
|
||||||
|
|
||||||
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -903,7 +903,7 @@ describe("textWysiwyg", () => {
|
|||||||
mouse.clickAt(10, 20);
|
mouse.clickAt(10, 20);
|
||||||
mouse.down();
|
mouse.down();
|
||||||
mouse.up();
|
mouse.up();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1154,7 +1154,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
h.elements = [container, text];
|
h.elements = [container, text];
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1168,7 +1168,7 @@ describe("textWysiwyg", () => {
|
|||||||
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
|
expect((h.elements[1] as ExcalidrawTextElementWithContainer).text).toBe(
|
||||||
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
|
"Online \nwhitebo\nard \ncollabo\nration \nmade \neasy",
|
||||||
);
|
);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1406,7 +1406,7 @@ describe("textWysiwyg", () => {
|
|||||||
|
|
||||||
API.setSelectedElements([textElement]);
|
API.setSelectedElements([textElement]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
|
@ -6,7 +6,7 @@ import {
|
|||||||
|
|
||||||
import { getElementAbsoluteCoords } from "./bounds";
|
import { getElementAbsoluteCoords } from "./bounds";
|
||||||
import { rotate } from "../math";
|
import { rotate } from "../math";
|
||||||
import { AppState, Zoom } from "../types";
|
import { InteractiveCanvasAppState, Zoom } from "../types";
|
||||||
import { isTextElement } from ".";
|
import { isTextElement } from ".";
|
||||||
import { isFrameElement, isLinearElement } from "./typeChecks";
|
import { isFrameElement, isLinearElement } from "./typeChecks";
|
||||||
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
import { DEFAULT_SPACING } from "../renderer/renderScene";
|
||||||
@ -277,7 +277,7 @@ export const getTransformHandles = (
|
|||||||
|
|
||||||
export const shouldShowBoundingBox = (
|
export const shouldShowBoundingBox = (
|
||||||
elements: NonDeletedExcalidrawElement[],
|
elements: NonDeletedExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
if (appState.editingLinearElement) {
|
if (appState.editingLinearElement) {
|
||||||
return false;
|
return false;
|
||||||
|
17
src/frame.ts
17
src/frame.ts
@ -16,7 +16,7 @@ import {
|
|||||||
} from "./element/textElement";
|
} from "./element/textElement";
|
||||||
import { arrayToMap, findIndex } from "./utils";
|
import { arrayToMap, findIndex } from "./utils";
|
||||||
import { mutateElement } from "./element/mutateElement";
|
import { mutateElement } from "./element/mutateElement";
|
||||||
import { AppClassProperties, AppState } from "./types";
|
import { AppClassProperties, AppState, StaticCanvasAppState } from "./types";
|
||||||
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
import { getElementsWithinSelection, getSelectedElements } from "./scene";
|
||||||
import { isFrameElement } from "./element";
|
import { isFrameElement } from "./element";
|
||||||
import { moveOneRight } from "./zindex";
|
import { moveOneRight } from "./zindex";
|
||||||
@ -469,9 +469,13 @@ export const addElementsToFrame = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
let nextElements = allElements.slice();
|
let nextElements = allElements.slice();
|
||||||
|
// Optimisation since findIndex on "newElements" is slow
|
||||||
|
const nextElementsIndex = nextElements.reduce((acc, element, index) => {
|
||||||
|
acc[element.id] = index;
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, number>);
|
||||||
|
|
||||||
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
|
const frameBoundary = findIndex(nextElements, (e) => e.frameId === frame.id);
|
||||||
|
|
||||||
for (const element of omitGroupsContainingFrames(
|
for (const element of omitGroupsContainingFrames(
|
||||||
allElements,
|
allElements,
|
||||||
_elementsToAdd,
|
_elementsToAdd,
|
||||||
@ -485,8 +489,8 @@ export const addElementsToFrame = (
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
const frameIndex = findIndex(nextElements, (e) => e.id === frame.id);
|
const frameIndex = nextElementsIndex[frame.id];
|
||||||
const elementIndex = findIndex(nextElements, (e) => e.id === element.id);
|
const elementIndex = nextElementsIndex[element.id];
|
||||||
|
|
||||||
if (elementIndex < frameBoundary) {
|
if (elementIndex < frameBoundary) {
|
||||||
nextElements = [
|
nextElements = [
|
||||||
@ -648,7 +652,7 @@ export const omitGroupsContainingFrames = (
|
|||||||
*/
|
*/
|
||||||
export const getTargetFrame = (
|
export const getTargetFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
appState: AppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
? getContainerElement(element) || element
|
? getContainerElement(element) || element
|
||||||
@ -660,11 +664,12 @@ export const getTargetFrame = (
|
|||||||
: getContainingFrame(_element);
|
: getContainingFrame(_element);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: this a huge bottleneck for large scenes, optimise
|
||||||
// given an element, return if the element is in some frame
|
// given an element, return if the element is in some frame
|
||||||
export const isElementInFrame = (
|
export const isElementInFrame = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
allElements: ExcalidrawElementsIncludingDeleted,
|
allElements: ExcalidrawElementsIncludingDeleted,
|
||||||
appState: AppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const frame = getTargetFrame(element, appState);
|
const frame = getTargetFrame(element, appState);
|
||||||
const _element = isTextElement(element)
|
const _element = isTextElement(element)
|
||||||
|
1
src/global.d.ts
vendored
1
src/global.d.ts
vendored
@ -17,6 +17,7 @@ interface Window {
|
|||||||
EXCALIDRAW_ASSET_PATH: string | undefined;
|
EXCALIDRAW_ASSET_PATH: string | undefined;
|
||||||
EXCALIDRAW_EXPORT_SOURCE: string;
|
EXCALIDRAW_EXPORT_SOURCE: string;
|
||||||
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
EXCALIDRAW_THROTTLE_RENDER: boolean | undefined;
|
||||||
|
EXCALIDRAW_THROTTLE_NEXT_RENDER: boolean;
|
||||||
gtag: Function;
|
gtag: Function;
|
||||||
sa_event: Function;
|
sa_event: Function;
|
||||||
fathom: { trackEvent: Function };
|
fathom: { trackEvent: Function };
|
||||||
|
164
src/groups.ts
164
src/groups.ts
@ -4,21 +4,28 @@ import {
|
|||||||
NonDeleted,
|
NonDeleted,
|
||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "./element/types";
|
} from "./element/types";
|
||||||
import { AppClassProperties, AppState } from "./types";
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
AppState,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "./types";
|
||||||
import { getSelectedElements } from "./scene";
|
import { getSelectedElements } from "./scene";
|
||||||
import { getBoundTextElement } from "./element/textElement";
|
import { getBoundTextElement } from "./element/textElement";
|
||||||
import { makeNextSelectedElementIds } from "./scene/selection";
|
import { makeNextSelectedElementIds } from "./scene/selection";
|
||||||
|
|
||||||
export const selectGroup = (
|
export const selectGroup = (
|
||||||
groupId: GroupId,
|
groupId: GroupId,
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
): AppState => {
|
): InteractiveCanvasAppState => {
|
||||||
const elementsInGroup = elements.filter((element) =>
|
const elementsInGroup = elements.reduce((acc, element) => {
|
||||||
element.groupIds.includes(groupId),
|
if (element.groupIds.includes(groupId)) {
|
||||||
);
|
acc[element.id] = true;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {} as Record<string, boolean>);
|
||||||
|
|
||||||
if (elementsInGroup.length < 2) {
|
if (Object.keys(elementsInGroup).length < 2) {
|
||||||
if (
|
if (
|
||||||
appState.selectedGroupIds[groupId] ||
|
appState.selectedGroupIds[groupId] ||
|
||||||
appState.editingGroupId === groupId
|
appState.editingGroupId === groupId
|
||||||
@ -37,31 +44,120 @@ export const selectGroup = (
|
|||||||
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
|
selectedGroupIds: { ...appState.selectedGroupIds, [groupId]: true },
|
||||||
selectedElementIds: {
|
selectedElementIds: {
|
||||||
...appState.selectedElementIds,
|
...appState.selectedElementIds,
|
||||||
...Object.fromEntries(
|
...elementsInGroup,
|
||||||
elementsInGroup.map((element) => [element.id, true]),
|
} as AppState["selectedElementIds"],
|
||||||
),
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const selectGroups = (function () {
|
||||||
|
let lastSelectedElements: readonly NonDeleted<ExcalidrawElement>[] | null =
|
||||||
|
null;
|
||||||
|
let lastElements: readonly NonDeleted<ExcalidrawElement>[] | null = null;
|
||||||
|
let lastAppState: InteractiveCanvasAppState | null = null;
|
||||||
|
|
||||||
|
const ret = (
|
||||||
|
selectedElements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
): InteractiveCanvasAppState => {
|
||||||
|
if (
|
||||||
|
lastAppState !== undefined &&
|
||||||
|
elements === lastElements &&
|
||||||
|
selectedElements === lastSelectedElements &&
|
||||||
|
appState.editingGroupId === lastAppState?.editingGroupId &&
|
||||||
|
appState.selectedGroupIds === lastAppState?.selectedGroupIds
|
||||||
|
) {
|
||||||
|
return lastAppState;
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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;
|
||||||
|
}, {} as Record<string, boolean>);
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
lastAppState = {
|
||||||
|
...appState,
|
||||||
|
selectedGroupIds,
|
||||||
|
selectedElementIds: {
|
||||||
|
...appState.selectedElementIds,
|
||||||
|
...selectedElementIdsInGroups,
|
||||||
|
} as AppState["selectedElementIds"],
|
||||||
|
};
|
||||||
|
|
||||||
|
return lastAppState;
|
||||||
|
};
|
||||||
|
|
||||||
|
ret.clearCache = () => {
|
||||||
|
lastElements = null;
|
||||||
|
lastSelectedElements = null;
|
||||||
|
lastAppState = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If the element's group is selected, don't render an individual
|
* If the element's group is selected, don't render an individual
|
||||||
* selection border around it.
|
* selection border around it.
|
||||||
*/
|
*/
|
||||||
export const isSelectedViaGroup = (
|
export const isSelectedViaGroup = (
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) => getSelectedGroupForElement(appState, element) != null;
|
) => getSelectedGroupForElement(appState, element) != null;
|
||||||
|
|
||||||
export const getSelectedGroupForElement = (
|
export const getSelectedGroupForElement = (
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
) =>
|
) =>
|
||||||
element.groupIds
|
element.groupIds
|
||||||
.filter((groupId) => groupId !== appState.editingGroupId)
|
.filter((groupId) => groupId !== appState.editingGroupId)
|
||||||
.find((groupId) => appState.selectedGroupIds[groupId]);
|
.find((groupId) => appState.selectedGroupIds[groupId]);
|
||||||
|
|
||||||
export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
export const getSelectedGroupIds = (
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
): GroupId[] =>
|
||||||
Object.entries(appState.selectedGroupIds)
|
Object.entries(appState.selectedGroupIds)
|
||||||
.filter(([groupId, isSelected]) => isSelected)
|
.filter(([groupId, isSelected]) => isSelected)
|
||||||
.map(([groupId, isSelected]) => groupId);
|
.map(([groupId, isSelected]) => groupId);
|
||||||
@ -71,16 +167,19 @@ export const getSelectedGroupIds = (appState: AppState): GroupId[] =>
|
|||||||
* you're currently editing that group.
|
* you're currently editing that group.
|
||||||
*/
|
*/
|
||||||
export const selectGroupsForSelectedElements = (
|
export const selectGroupsForSelectedElements = (
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
prevAppState: AppState,
|
prevAppState: InteractiveCanvasAppState,
|
||||||
/**
|
/**
|
||||||
* supply null in cases where you don't have access to App instance and
|
* supply null in cases where you don't have access to App instance and
|
||||||
* you don't care about optimizing selectElements retrieval
|
* you don't care about optimizing selectElements retrieval
|
||||||
*/
|
*/
|
||||||
app: AppClassProperties | null,
|
app: AppClassProperties | null,
|
||||||
): AppState => {
|
): InteractiveCanvasAppState => {
|
||||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
let nextAppState: InteractiveCanvasAppState = {
|
||||||
|
...appState,
|
||||||
|
selectedGroupIds: {},
|
||||||
|
};
|
||||||
|
|
||||||
const selectedElements = app
|
const selectedElements = app
|
||||||
? app.scene.getSelectedElements({
|
? app.scene.getSelectedElements({
|
||||||
@ -101,25 +200,7 @@ export const selectGroupsForSelectedElements = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const selectedElement of selectedElements) {
|
nextAppState = selectGroups(selectedElements, elements, appState);
|
||||||
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;
|
return nextAppState;
|
||||||
};
|
};
|
||||||
@ -128,9 +209,12 @@ export const selectGroupsForSelectedElements = (
|
|||||||
// or used to update the elements
|
// or used to update the elements
|
||||||
export const selectGroupsFromGivenElements = (
|
export const selectGroupsFromGivenElements = (
|
||||||
elements: readonly NonDeleted<ExcalidrawElement>[],
|
elements: readonly NonDeleted<ExcalidrawElement>[],
|
||||||
appState: AppState,
|
appState: InteractiveCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
let nextAppState: AppState = { ...appState, selectedGroupIds: {} };
|
let nextAppState: InteractiveCanvasAppState = {
|
||||||
|
...appState,
|
||||||
|
selectedGroupIds: {},
|
||||||
|
};
|
||||||
|
|
||||||
for (const element of elements) {
|
for (const element of elements) {
|
||||||
let groupIds = element.groupIds;
|
let groupIds = element.groupIds;
|
||||||
@ -146,6 +230,8 @@ export const selectGroupsFromGivenElements = (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nextAppState = selectGroups(elements, elements, appState);
|
||||||
|
|
||||||
return nextAppState.selectedGroupIds;
|
return nextAppState.selectedGroupIds;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
42
src/hooks/useMutatedElements.ts
Normal file
42
src/hooks/useMutatedElements.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Scene from "../scene/Scene";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { InteractiveCanvasAppState, StaticCanvasAppState } from "../types";
|
||||||
|
import { isImageElement } from "../element/typeChecks";
|
||||||
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
|
|
||||||
|
export const useMutatedElements = ({
|
||||||
|
appState,
|
||||||
|
scene,
|
||||||
|
}: {
|
||||||
|
appState: InteractiveCanvasAppState | StaticCanvasAppState;
|
||||||
|
scene: Scene;
|
||||||
|
}): [readonly NonDeletedExcalidrawElement[], number | undefined] => {
|
||||||
|
const versionNonce = scene.getVersionNonce();
|
||||||
|
const nonDeletedElements = scene.getNonDeletedElements();
|
||||||
|
|
||||||
|
const elements = useMemo(() => {
|
||||||
|
return nonDeletedElements.filter((element) => {
|
||||||
|
if (isImageElement(element)) {
|
||||||
|
if (
|
||||||
|
// not placed on canvas yet (but in elements array)
|
||||||
|
appState.pendingImageElementId === element.id
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// don't render text element that's being currently edited (it's
|
||||||
|
// rendered on remote only)
|
||||||
|
return (
|
||||||
|
!appState.editingElement ||
|
||||||
|
appState.editingElement.type !== "text" ||
|
||||||
|
element.id !== appState.editingElement.id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, [
|
||||||
|
nonDeletedElements,
|
||||||
|
appState.editingElement,
|
||||||
|
appState.pendingImageElementId,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [elements, versionNonce];
|
||||||
|
};
|
@ -26,11 +26,17 @@ import { Drawable, Options } from "roughjs/bin/core";
|
|||||||
import { RoughSVG } from "roughjs/bin/svg";
|
import { RoughSVG } from "roughjs/bin/svg";
|
||||||
import { RoughGenerator } from "roughjs/bin/generator";
|
import { RoughGenerator } from "roughjs/bin/generator";
|
||||||
|
|
||||||
import { RenderConfig } from "../scene/types";
|
import { StaticCanvasRenderConfig } from "../scene/types";
|
||||||
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
import { distance, getFontString, getFontFamilyString, isRTL } from "../utils";
|
||||||
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
import { getCornerRadius, isPathALoop, isRightAngle } from "../math";
|
||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { AppState, BinaryFiles, Zoom } from "../types";
|
import {
|
||||||
|
AppState,
|
||||||
|
StaticCanvasAppState,
|
||||||
|
BinaryFiles,
|
||||||
|
Zoom,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
import { getDefaultAppState } from "../appState";
|
import { getDefaultAppState } from "../appState";
|
||||||
import {
|
import {
|
||||||
BOUND_TEXT_PADDING,
|
BOUND_TEXT_PADDING,
|
||||||
@ -62,17 +68,18 @@ const defaultAppState = getDefaultAppState();
|
|||||||
|
|
||||||
const isPendingImageElement = (
|
const isPendingImageElement = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
) =>
|
) =>
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
!renderConfig.imageCache.has(element.fileId);
|
!renderConfig.imageCache.has(element.fileId);
|
||||||
|
|
||||||
const shouldResetImageFilter = (
|
const shouldResetImageFilter = (
|
||||||
element: ExcalidrawElement,
|
element: ExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
return (
|
return (
|
||||||
renderConfig.theme === "dark" &&
|
appState.theme === "dark" &&
|
||||||
isInitializedImageElement(element) &&
|
isInitializedImageElement(element) &&
|
||||||
!isPendingImageElement(element, renderConfig) &&
|
!isPendingImageElement(element, renderConfig) &&
|
||||||
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
renderConfig.imageCache.get(element.fileId)?.mimeType !== MIME_TYPES.svg
|
||||||
@ -89,9 +96,9 @@ const getCanvasPadding = (element: ExcalidrawElement) =>
|
|||||||
export interface ExcalidrawElementWithCanvas {
|
export interface ExcalidrawElementWithCanvas {
|
||||||
element: ExcalidrawElement | ExcalidrawTextElement;
|
element: ExcalidrawElement | ExcalidrawTextElement;
|
||||||
canvas: HTMLCanvasElement;
|
canvas: HTMLCanvasElement;
|
||||||
theme: RenderConfig["theme"];
|
theme: AppState["theme"];
|
||||||
scale: number;
|
scale: number;
|
||||||
zoomValue: RenderConfig["zoom"]["value"];
|
zoomValue: AppState["zoom"]["value"];
|
||||||
canvasOffsetX: number;
|
canvasOffsetX: number;
|
||||||
canvasOffsetY: number;
|
canvasOffsetY: number;
|
||||||
boundTextElementVersion: number | null;
|
boundTextElementVersion: number | null;
|
||||||
@ -155,7 +162,8 @@ const cappedElementCanvasSize = (
|
|||||||
const generateElementCanvas = (
|
const generateElementCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
zoom: Zoom,
|
zoom: Zoom,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
): ExcalidrawElementWithCanvas => {
|
): ExcalidrawElementWithCanvas => {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const context = canvas.getContext("2d")!;
|
const context = canvas.getContext("2d")!;
|
||||||
@ -195,17 +203,17 @@ const generateElementCanvas = (
|
|||||||
const rc = rough.canvas(canvas);
|
const rc = rough.canvas(canvas);
|
||||||
|
|
||||||
// in dark theme, revert the image color filter
|
// in dark theme, revert the image color filter
|
||||||
if (shouldResetImageFilter(element, renderConfig)) {
|
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
context.filter = IMAGE_INVERT_FILTER;
|
context.filter = IMAGE_INVERT_FILTER;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
context.restore();
|
context.restore();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
element,
|
element,
|
||||||
canvas,
|
canvas,
|
||||||
theme: renderConfig.theme,
|
theme: appState.theme,
|
||||||
scale,
|
scale,
|
||||||
zoomValue: zoom.value,
|
zoomValue: zoom.value,
|
||||||
canvasOffsetX,
|
canvasOffsetX,
|
||||||
@ -256,7 +264,8 @@ const drawElementOnCanvas = (
|
|||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
context.globalAlpha =
|
context.globalAlpha =
|
||||||
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
((getContainingFrame(element)?.opacity ?? 100) * element.opacity) / 10000;
|
||||||
@ -310,7 +319,7 @@ const drawElementOnCanvas = (
|
|||||||
element.height,
|
element.height,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
drawImagePlaceholder(element, context, renderConfig.zoom.value);
|
drawImagePlaceholder(element, context, appState.zoom.value);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -715,21 +724,22 @@ const generateElementShape = (
|
|||||||
|
|
||||||
const generateElementWithCanvas = (
|
const generateElementWithCanvas = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const zoom: Zoom = renderConfig ? renderConfig.zoom : defaultAppState.zoom;
|
const zoom: Zoom = renderConfig ? appState.zoom : defaultAppState.zoom;
|
||||||
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
const prevElementWithCanvas = elementWithCanvasCache.get(element);
|
||||||
const shouldRegenerateBecauseZoom =
|
const shouldRegenerateBecauseZoom =
|
||||||
prevElementWithCanvas &&
|
prevElementWithCanvas &&
|
||||||
prevElementWithCanvas.zoomValue !== zoom.value &&
|
prevElementWithCanvas.zoomValue !== zoom.value &&
|
||||||
!renderConfig?.shouldCacheIgnoreZoom;
|
!appState?.shouldCacheIgnoreZoom;
|
||||||
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
const boundTextElementVersion = getBoundTextElement(element)?.version || null;
|
||||||
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
const containingFrameOpacity = getContainingFrame(element)?.opacity || 100;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
!prevElementWithCanvas ||
|
!prevElementWithCanvas ||
|
||||||
shouldRegenerateBecauseZoom ||
|
shouldRegenerateBecauseZoom ||
|
||||||
prevElementWithCanvas.theme !== renderConfig.theme ||
|
prevElementWithCanvas.theme !== appState.theme ||
|
||||||
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
|
prevElementWithCanvas.boundTextElementVersion !== boundTextElementVersion ||
|
||||||
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
|
prevElementWithCanvas.containingFrameOpacity !== containingFrameOpacity
|
||||||
) {
|
) {
|
||||||
@ -737,6 +747,7 @@ const generateElementWithCanvas = (
|
|||||||
element,
|
element,
|
||||||
zoom,
|
zoom,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
elementWithCanvasCache.set(element, elementWithCanvas);
|
elementWithCanvasCache.set(element, elementWithCanvas);
|
||||||
@ -748,9 +759,9 @@ const generateElementWithCanvas = (
|
|||||||
|
|
||||||
const drawElementFromCanvas = (
|
const drawElementFromCanvas = (
|
||||||
elementWithCanvas: ExcalidrawElementWithCanvas,
|
elementWithCanvas: ExcalidrawElementWithCanvas,
|
||||||
rc: RoughCanvas,
|
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const element = elementWithCanvas.element;
|
const element = elementWithCanvas.element;
|
||||||
const padding = getCanvasPadding(element);
|
const padding = getCanvasPadding(element);
|
||||||
@ -765,8 +776,8 @@ const drawElementFromCanvas = (
|
|||||||
y2 = Math.ceil(y2);
|
y2 = Math.ceil(y2);
|
||||||
}
|
}
|
||||||
|
|
||||||
const cx = ((x1 + x2) / 2 + renderConfig.scrollX) * window.devicePixelRatio;
|
const cx = ((x1 + x2) / 2 + appState.scrollX) * window.devicePixelRatio;
|
||||||
const cy = ((y1 + y2) / 2 + renderConfig.scrollY) * window.devicePixelRatio;
|
const cy = ((y1 + y2) / 2 + appState.scrollY) * window.devicePixelRatio;
|
||||||
|
|
||||||
context.save();
|
context.save();
|
||||||
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
context.scale(1 / window.devicePixelRatio, 1 / window.devicePixelRatio);
|
||||||
@ -864,9 +875,9 @@ const drawElementFromCanvas = (
|
|||||||
|
|
||||||
context.drawImage(
|
context.drawImage(
|
||||||
elementWithCanvas.canvas!,
|
elementWithCanvas.canvas!,
|
||||||
(x1 + renderConfig.scrollX) * window.devicePixelRatio -
|
(x1 + appState.scrollX) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||||
(y1 + renderConfig.scrollY) * window.devicePixelRatio -
|
(y1 + appState.scrollY) * window.devicePixelRatio -
|
||||||
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
(padding * elementWithCanvas.scale) / elementWithCanvas.scale,
|
||||||
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
|
elementWithCanvas.canvas!.width / elementWithCanvas.scale,
|
||||||
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
elementWithCanvas.canvas!.height / elementWithCanvas.scale,
|
||||||
@ -884,8 +895,8 @@ const drawElementFromCanvas = (
|
|||||||
context.strokeStyle = "#c92a2a";
|
context.strokeStyle = "#c92a2a";
|
||||||
context.lineWidth = 3;
|
context.lineWidth = 3;
|
||||||
context.strokeRect(
|
context.strokeRect(
|
||||||
(coords.x + renderConfig.scrollX) * window.devicePixelRatio,
|
(coords.x + appState.scrollX) * window.devicePixelRatio,
|
||||||
(coords.y + renderConfig.scrollY) * window.devicePixelRatio,
|
(coords.y + appState.scrollY) * window.devicePixelRatio,
|
||||||
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
getBoundTextMaxWidth(element) * window.devicePixelRatio,
|
||||||
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
getBoundTextMaxHeight(element, textElement) * window.devicePixelRatio,
|
||||||
);
|
);
|
||||||
@ -896,40 +907,38 @@ const drawElementFromCanvas = (
|
|||||||
// Clear the nested element we appended to the DOM
|
// Clear the nested element we appended to the DOM
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const renderSelectionElement = (
|
||||||
|
element: NonDeletedExcalidrawElement,
|
||||||
|
context: CanvasRenderingContext2D,
|
||||||
|
appState: InteractiveCanvasAppState,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
context.translate(element.x + appState.scrollX, element.y + appState.scrollY);
|
||||||
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
|
// render from 0.5px offset to get 1px wide line
|
||||||
|
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
|
||||||
|
// TODO can be be improved by offseting to the negative when user selects
|
||||||
|
// from right to left
|
||||||
|
const offset = 0.5 / appState.zoom.value;
|
||||||
|
|
||||||
|
context.fillRect(offset, offset, element.width, element.height);
|
||||||
|
context.lineWidth = 1 / appState.zoom.value;
|
||||||
|
context.strokeStyle = " rgb(105, 101, 219)";
|
||||||
|
context.strokeRect(offset, offset, element.width, element.height);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
export const renderElement = (
|
export const renderElement = (
|
||||||
element: NonDeletedExcalidrawElement,
|
element: NonDeletedExcalidrawElement,
|
||||||
rc: RoughCanvas,
|
rc: RoughCanvas,
|
||||||
context: CanvasRenderingContext2D,
|
context: CanvasRenderingContext2D,
|
||||||
renderConfig: RenderConfig,
|
renderConfig: StaticCanvasRenderConfig,
|
||||||
appState: AppState,
|
appState: StaticCanvasAppState,
|
||||||
) => {
|
) => {
|
||||||
const generator = rc.generator;
|
const generator = rc.generator;
|
||||||
switch (element.type) {
|
switch (element.type) {
|
||||||
case "selection": {
|
|
||||||
// do not render selection when exporting
|
|
||||||
if (!renderConfig.isExporting) {
|
|
||||||
context.save();
|
|
||||||
context.translate(
|
|
||||||
element.x + renderConfig.scrollX,
|
|
||||||
element.y + renderConfig.scrollY,
|
|
||||||
);
|
|
||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
|
||||||
|
|
||||||
// render from 0.5px offset to get 1px wide line
|
|
||||||
// https://stackoverflow.com/questions/7530593/html5-canvas-and-line-width/7531540#7531540
|
|
||||||
// TODO can be be improved by offseting to the negative when user selects
|
|
||||||
// from right to left
|
|
||||||
const offset = 0.5 / renderConfig.zoom.value;
|
|
||||||
|
|
||||||
context.fillRect(offset, offset, element.width, element.height);
|
|
||||||
context.lineWidth = 1 / renderConfig.zoom.value;
|
|
||||||
context.strokeStyle = " rgb(105, 101, 219)";
|
|
||||||
context.strokeRect(offset, offset, element.width, element.height);
|
|
||||||
|
|
||||||
context.restore();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "frame": {
|
case "frame": {
|
||||||
if (
|
if (
|
||||||
!renderConfig.isExporting &&
|
!renderConfig.isExporting &&
|
||||||
@ -938,12 +947,12 @@ export const renderElement = (
|
|||||||
) {
|
) {
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(
|
context.translate(
|
||||||
element.x + renderConfig.scrollX,
|
element.x + appState.scrollX,
|
||||||
element.y + renderConfig.scrollY,
|
element.y + appState.scrollY,
|
||||||
);
|
);
|
||||||
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
context.fillStyle = "rgba(0, 0, 200, 0.04)";
|
||||||
|
|
||||||
context.lineWidth = 2 / renderConfig.zoom.value;
|
context.lineWidth = 2 / appState.zoom.value;
|
||||||
context.strokeStyle = FRAME_STYLE.strokeColor;
|
context.strokeStyle = FRAME_STYLE.strokeColor;
|
||||||
|
|
||||||
if (FRAME_STYLE.radius && context.roundRect) {
|
if (FRAME_STYLE.radius && context.roundRect) {
|
||||||
@ -953,7 +962,7 @@ export const renderElement = (
|
|||||||
0,
|
0,
|
||||||
element.width,
|
element.width,
|
||||||
element.height,
|
element.height,
|
||||||
FRAME_STYLE.radius / renderConfig.zoom.value,
|
FRAME_STYLE.radius / appState.zoom.value,
|
||||||
);
|
);
|
||||||
context.stroke();
|
context.stroke();
|
||||||
context.closePath();
|
context.closePath();
|
||||||
@ -970,22 +979,28 @@ export const renderElement = (
|
|||||||
|
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
const cy = (y1 + y2) / 2 + appState.scrollY;
|
||||||
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
const shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||||
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
const shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||||
context.save();
|
context.save();
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
context.rotate(element.angle);
|
context.rotate(element.angle);
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
context.restore();
|
context.restore();
|
||||||
} else {
|
} else {
|
||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
element,
|
element,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
drawElementFromCanvas(
|
||||||
|
elementWithCanvas,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@ -1000,8 +1015,8 @@ export const renderElement = (
|
|||||||
generateElementShape(element, generator);
|
generateElementShape(element, generator);
|
||||||
if (renderConfig.isExporting) {
|
if (renderConfig.isExporting) {
|
||||||
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
const [x1, y1, x2, y2] = getElementAbsoluteCoords(element);
|
||||||
const cx = (x1 + x2) / 2 + renderConfig.scrollX;
|
const cx = (x1 + x2) / 2 + appState.scrollX;
|
||||||
const cy = (y1 + y2) / 2 + renderConfig.scrollY;
|
const cy = (y1 + y2) / 2 + appState.scrollY;
|
||||||
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
let shiftX = (x2 - x1) / 2 - (element.x - x1);
|
||||||
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
let shiftY = (y2 - y1) / 2 - (element.y - y1);
|
||||||
if (isTextElement(element)) {
|
if (isTextElement(element)) {
|
||||||
@ -1019,7 +1034,7 @@ export const renderElement = (
|
|||||||
context.save();
|
context.save();
|
||||||
context.translate(cx, cy);
|
context.translate(cx, cy);
|
||||||
|
|
||||||
if (shouldResetImageFilter(element, renderConfig)) {
|
if (shouldResetImageFilter(element, renderConfig, appState)) {
|
||||||
context.filter = "none";
|
context.filter = "none";
|
||||||
}
|
}
|
||||||
const boundTextElement = getBoundTextElement(element);
|
const boundTextElement = getBoundTextElement(element);
|
||||||
@ -1053,7 +1068,13 @@ export const renderElement = (
|
|||||||
|
|
||||||
tempCanvasContext.translate(-shiftX, -shiftY);
|
tempCanvasContext.translate(-shiftX, -shiftY);
|
||||||
|
|
||||||
drawElementOnCanvas(element, tempRc, tempCanvasContext, renderConfig);
|
drawElementOnCanvas(
|
||||||
|
element,
|
||||||
|
tempRc,
|
||||||
|
tempCanvasContext,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
tempCanvasContext.translate(shiftX, shiftY);
|
tempCanvasContext.translate(shiftX, shiftY);
|
||||||
|
|
||||||
@ -1090,7 +1111,7 @@ export const renderElement = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.translate(-shiftX, -shiftY);
|
context.translate(-shiftX, -shiftY);
|
||||||
drawElementOnCanvas(element, rc, context, renderConfig);
|
drawElementOnCanvas(element, rc, context, renderConfig, appState);
|
||||||
}
|
}
|
||||||
|
|
||||||
context.restore();
|
context.restore();
|
||||||
@ -1100,6 +1121,7 @@ export const renderElement = (
|
|||||||
const elementWithCanvas = generateElementWithCanvas(
|
const elementWithCanvas = generateElementWithCanvas(
|
||||||
element,
|
element,
|
||||||
renderConfig,
|
renderConfig,
|
||||||
|
appState,
|
||||||
);
|
);
|
||||||
|
|
||||||
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
|
const currentImageSmoothingStatus = context.imageSmoothingEnabled;
|
||||||
@ -1107,7 +1129,7 @@ export const renderElement = (
|
|||||||
if (
|
if (
|
||||||
// do not disable smoothing during zoom as blurry shapes look better
|
// do not disable smoothing during zoom as blurry shapes look better
|
||||||
// on low resolution (while still zooming in) than sharp ones
|
// on low resolution (while still zooming in) than sharp ones
|
||||||
!renderConfig?.shouldCacheIgnoreZoom &&
|
!appState?.shouldCacheIgnoreZoom &&
|
||||||
// angle is 0 -> always disable smoothing
|
// angle is 0 -> always disable smoothing
|
||||||
(!element.angle ||
|
(!element.angle ||
|
||||||
// or check if angle is a right angle in which case we can still
|
// or check if angle is a right angle in which case we can still
|
||||||
@ -1124,7 +1146,12 @@ export const renderElement = (
|
|||||||
context.imageSmoothingEnabled = false;
|
context.imageSmoothingEnabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
drawElementFromCanvas(elementWithCanvas, rc, context, renderConfig);
|
drawElementFromCanvas(
|
||||||
|
elementWithCanvas,
|
||||||
|
context,
|
||||||
|
renderConfig,
|
||||||
|
appState,
|
||||||
|
);
|
||||||
|
|
||||||
// reset
|
// reset
|
||||||
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
context.imageSmoothingEnabled = currentImageSmoothingStatus;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -14,6 +14,7 @@ import { isFrameElement } from "../element/typeChecks";
|
|||||||
import { getSelectedElements } from "./selection";
|
import { getSelectedElements } from "./selection";
|
||||||
import { AppState } from "../types";
|
import { AppState } from "../types";
|
||||||
import { Assert, SameType } from "../utility-types";
|
import { Assert, SameType } from "../utility-types";
|
||||||
|
import { randomInteger } from "../random";
|
||||||
|
|
||||||
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
type ElementIdKey = InstanceType<typeof LinearElementEditor>["elementId"];
|
||||||
type ElementKey = ExcalidrawElement | ElementIdKey;
|
type ElementKey = ExcalidrawElement | ElementIdKey;
|
||||||
@ -105,6 +106,7 @@ class Scene {
|
|||||||
elements: null,
|
elements: null,
|
||||||
cache: new Map(),
|
cache: new Map(),
|
||||||
};
|
};
|
||||||
|
private versionNonce: number | undefined;
|
||||||
|
|
||||||
getElementsIncludingDeleted() {
|
getElementsIncludingDeleted() {
|
||||||
return this.elements;
|
return this.elements;
|
||||||
@ -172,6 +174,10 @@ class Scene {
|
|||||||
return (this.elementsMap.get(id) as T | undefined) || null;
|
return (this.elementsMap.get(id) as T | undefined) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getVersionNonce() {
|
||||||
|
return this.versionNonce;
|
||||||
|
}
|
||||||
|
|
||||||
getNonDeletedElement(
|
getNonDeletedElement(
|
||||||
id: ExcalidrawElement["id"],
|
id: ExcalidrawElement["id"],
|
||||||
): NonDeleted<ExcalidrawElement> | null {
|
): NonDeleted<ExcalidrawElement> | null {
|
||||||
@ -230,6 +236,8 @@ class Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
informMutation() {
|
informMutation() {
|
||||||
|
this.versionNonce = randomInteger();
|
||||||
|
|
||||||
for (const callback of Array.from(this.callbacks)) {
|
for (const callback of Array.from(this.callbacks)) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import rough from "roughjs/bin/rough";
|
import rough from "roughjs/bin/rough";
|
||||||
import { NonDeletedExcalidrawElement } from "../element/types";
|
import { NonDeletedExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
import { getCommonBounds, getElementAbsoluteCoords } from "../element/bounds";
|
||||||
import { renderScene, renderSceneToSvg } from "../renderer/renderScene";
|
import { renderSceneToSvg, renderStaticScene } from "../renderer/renderScene";
|
||||||
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
import { distance, isOnlyExportingSingleFrame } from "../utils";
|
||||||
import { AppState, BinaryFiles } from "../types";
|
import { AppState, BinaryFiles } from "../types";
|
||||||
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
import { DEFAULT_EXPORT_PADDING, SVG_NS, THEME_FILTER } from "../constants";
|
||||||
@ -54,26 +54,21 @@ export const exportToCanvas = async (
|
|||||||
|
|
||||||
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
const onlyExportingSingleFrame = isOnlyExportingSingleFrame(elements);
|
||||||
|
|
||||||
renderScene({
|
renderStaticScene({
|
||||||
elements,
|
elements,
|
||||||
appState,
|
appState: {
|
||||||
|
...appState,
|
||||||
|
scrollX: -minX + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
|
scrollY: -minY + (onlyExportingSingleFrame ? 0 : exportPadding),
|
||||||
|
zoom: defaultAppState.zoom,
|
||||||
|
shouldCacheIgnoreZoom: false,
|
||||||
|
theme: appState.exportWithDarkMode ? "dark" : "light",
|
||||||
|
},
|
||||||
scale,
|
scale,
|
||||||
rc: rough.canvas(canvas),
|
rc: rough.canvas(canvas),
|
||||||
canvas,
|
canvas,
|
||||||
renderConfig: {
|
renderConfig: {
|
||||||
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",
|
|
||||||
imageCache,
|
imageCache,
|
||||||
renderScrollbars: false,
|
|
||||||
renderSelection: false,
|
|
||||||
renderGrid: false,
|
renderGrid: false,
|
||||||
isExporting: true,
|
isExporting: true,
|
||||||
},
|
},
|
||||||
|
@ -11,11 +11,7 @@ import {
|
|||||||
viewportCoordsToSceneCoords,
|
viewportCoordsToSceneCoords,
|
||||||
} from "../utils";
|
} from "../utils";
|
||||||
|
|
||||||
const isOutsideViewPort = (
|
const isOutsideViewPort = (appState: AppState, cords: Array<number>) => {
|
||||||
appState: AppState,
|
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
cords: Array<number>,
|
|
||||||
) => {
|
|
||||||
const [x1, y1, x2, y2] = cords;
|
const [x1, y1, x2, y2] = cords;
|
||||||
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
const { x: viewportX1, y: viewportY1 } = sceneCoordsToViewportCoords(
|
||||||
{ sceneX: x1, sceneY: y1 },
|
{ sceneX: x1, sceneY: y1 },
|
||||||
@ -49,7 +45,6 @@ export const centerScrollOn = ({
|
|||||||
export const calculateScrollCenter = (
|
export const calculateScrollCenter = (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
canvas: HTMLCanvasElement | null,
|
|
||||||
): { scrollX: number; scrollY: number } => {
|
): { scrollX: number; scrollY: number } => {
|
||||||
elements = getVisibleElements(elements);
|
elements = getVisibleElements(elements);
|
||||||
|
|
||||||
@ -61,7 +56,7 @@ export const calculateScrollCenter = (
|
|||||||
}
|
}
|
||||||
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
let [x1, y1, x2, y2] = getCommonBounds(elements);
|
||||||
|
|
||||||
if (isOutsideViewPort(appState, canvas, [x1, y1, x2, y2])) {
|
if (isOutsideViewPort(appState, [x1, y1, x2, y2])) {
|
||||||
[x1, y1, x2, y2] = getClosestElementBounds(
|
[x1, y1, x2, y2] = getClosestElementBounds(
|
||||||
elements,
|
elements,
|
||||||
viewportCoordsToSceneCoords(
|
viewportCoordsToSceneCoords(
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ExcalidrawElement } from "../element/types";
|
import { ExcalidrawElement } from "../element/types";
|
||||||
import { getCommonBounds } from "../element";
|
import { getCommonBounds } from "../element";
|
||||||
import { Zoom } from "../types";
|
import { InteractiveCanvasAppState } from "../types";
|
||||||
import { ScrollBars } from "./types";
|
import { ScrollBars } from "./types";
|
||||||
import { getGlobalCSSVariable } from "../utils";
|
import { getGlobalCSSVariable } from "../utils";
|
||||||
import { getLanguage } from "../i18n";
|
import { getLanguage } from "../i18n";
|
||||||
@ -13,15 +13,7 @@ export const getScrollBars = (
|
|||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
viewportWidth: number,
|
viewportWidth: number,
|
||||||
viewportHeight: number,
|
viewportHeight: number,
|
||||||
{
|
appState: InteractiveCanvasAppState,
|
||||||
scrollX,
|
|
||||||
scrollY,
|
|
||||||
zoom,
|
|
||||||
}: {
|
|
||||||
scrollX: number;
|
|
||||||
scrollY: number;
|
|
||||||
zoom: Zoom;
|
|
||||||
},
|
|
||||||
): ScrollBars => {
|
): ScrollBars => {
|
||||||
if (elements.length === 0) {
|
if (elements.length === 0) {
|
||||||
return {
|
return {
|
||||||
@ -34,8 +26,8 @@ export const getScrollBars = (
|
|||||||
getCommonBounds(elements);
|
getCommonBounds(elements);
|
||||||
|
|
||||||
// Apply zoom
|
// Apply zoom
|
||||||
const viewportWidthWithZoom = viewportWidth / zoom.value;
|
const viewportWidthWithZoom = viewportWidth / appState.zoom.value;
|
||||||
const viewportHeightWithZoom = viewportHeight / zoom.value;
|
const viewportHeightWithZoom = viewportHeight / appState.zoom.value;
|
||||||
|
|
||||||
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
const viewportWidthDiff = viewportWidth - viewportWidthWithZoom;
|
||||||
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
const viewportHeightDiff = viewportHeight - viewportHeightWithZoom;
|
||||||
@ -50,8 +42,10 @@ export const getScrollBars = (
|
|||||||
const isRTL = getLanguage().rtl;
|
const isRTL = getLanguage().rtl;
|
||||||
|
|
||||||
// The viewport is the rectangle currently visible for the user
|
// The viewport is the rectangle currently visible for the user
|
||||||
const viewportMinX = -scrollX + viewportWidthDiff / 2 + safeArea.left;
|
const viewportMinX =
|
||||||
const viewportMinY = -scrollY + viewportHeightDiff / 2 + safeArea.top;
|
-appState.scrollX + viewportWidthDiff / 2 + safeArea.left;
|
||||||
|
const viewportMinY =
|
||||||
|
-appState.scrollY + viewportHeightDiff / 2 + safeArea.top;
|
||||||
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
const viewportMaxX = viewportMinX + viewportWidthWithZoom - safeArea.right;
|
||||||
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
const viewportMaxY = viewportMinY + viewportHeightWithZoom - safeArea.bottom;
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import {
|
|||||||
NonDeletedExcalidrawElement,
|
NonDeletedExcalidrawElement,
|
||||||
} from "../element/types";
|
} from "../element/types";
|
||||||
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
import { getElementAbsoluteCoords, getElementBounds } from "../element";
|
||||||
import { AppState } from "../types";
|
import { AppState, InteractiveCanvasAppState } from "../types";
|
||||||
import { isBoundToContainer } from "../element/typeChecks";
|
import { isBoundToContainer } from "../element/typeChecks";
|
||||||
import {
|
import {
|
||||||
elementOverlapsWithFrame,
|
elementOverlapsWithFrame,
|
||||||
@ -146,7 +146,7 @@ export const getCommonAttributeOfSelectedElements = <T>(
|
|||||||
|
|
||||||
export const getSelectedElements = (
|
export const getSelectedElements = (
|
||||||
elements: readonly NonDeletedExcalidrawElement[],
|
elements: readonly NonDeletedExcalidrawElement[],
|
||||||
appState: Pick<AppState, "selectedElementIds">,
|
appState: Pick<InteractiveCanvasAppState, "selectedElementIds">,
|
||||||
opts?: {
|
opts?: {
|
||||||
includeBoundTextElement?: boolean;
|
includeBoundTextElement?: boolean;
|
||||||
includeElementsInFrames?: boolean;
|
includeElementsInFrames?: boolean;
|
||||||
|
@ -1,33 +1,60 @@
|
|||||||
import { ExcalidrawTextElement } from "../element/types";
|
import { RoughCanvas } from "roughjs/bin/canvas";
|
||||||
import { AppClassProperties, AppState } from "../types";
|
import {
|
||||||
|
ExcalidrawTextElement,
|
||||||
|
NonDeletedExcalidrawElement,
|
||||||
|
} from "../element/types";
|
||||||
|
import {
|
||||||
|
AppClassProperties,
|
||||||
|
InteractiveCanvasAppState,
|
||||||
|
StaticCanvasAppState,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
export type RenderConfig = {
|
export type StaticCanvasRenderConfig = {
|
||||||
// AppState values
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
scrollX: AppState["scrollX"];
|
|
||||||
scrollY: AppState["scrollY"];
|
|
||||||
/** null indicates transparent bg */
|
|
||||||
viewBackgroundColor: AppState["viewBackgroundColor"] | null;
|
|
||||||
zoom: AppState["zoom"];
|
|
||||||
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
|
||||||
theme: AppState["theme"];
|
|
||||||
// collab-related state
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
|
||||||
remotePointerButton?: { [id: string]: string | undefined };
|
|
||||||
remoteSelectedElementIds: { [elementId: string]: string[] };
|
|
||||||
remotePointerUsernames: { [id: string]: string };
|
|
||||||
remotePointerUserStates: { [id: string]: string };
|
|
||||||
// extra options passed to the renderer
|
// extra options passed to the renderer
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
imageCache: AppClassProperties["imageCache"];
|
imageCache: AppClassProperties["imageCache"];
|
||||||
renderScrollbars?: boolean;
|
renderGrid: boolean;
|
||||||
renderSelection?: boolean;
|
|
||||||
renderGrid?: boolean;
|
|
||||||
/** when exporting the behavior is slightly different (e.g. we can't use
|
/** when exporting the behavior is slightly different (e.g. we can't use
|
||||||
CSS filters), and we disable render optimizations for best output */
|
CSS filters), and we disable render optimizations for best output */
|
||||||
isExporting: boolean;
|
isExporting: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveCanvasRenderConfig = {
|
||||||
|
// collab-related state
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
remoteSelectedElementIds: { [elementId: string]: string[] };
|
||||||
|
remotePointerViewportCoords: { [id: string]: { x: number; y: number } };
|
||||||
|
remotePointerUserStates: { [id: string]: string };
|
||||||
|
remotePointerUsernames: { [id: string]: string };
|
||||||
|
remotePointerButton?: { [id: string]: string | undefined };
|
||||||
selectionColor?: string;
|
selectionColor?: string;
|
||||||
|
// extra options passed to the renderer
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
renderScrollbars?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderInteractiveSceneCallback = {
|
||||||
|
atLeastOneVisibleElement: boolean;
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
scrollBars?: ScrollBars;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StaticSceneRenderConfig = {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
scale: number;
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
rc: RoughCanvas;
|
||||||
|
appState: StaticCanvasAppState;
|
||||||
|
renderConfig: StaticCanvasRenderConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveSceneRenderConfig = {
|
||||||
|
elements: readonly NonDeletedExcalidrawElement[];
|
||||||
|
canvas: HTMLCanvasElement | null;
|
||||||
|
scale: number;
|
||||||
|
appState: InteractiveCanvasAppState;
|
||||||
|
renderConfig: InteractiveCanvasRenderConfig;
|
||||||
|
callback: (data: RenderInteractiveSceneCallback) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SceneScroll = {
|
export type SceneScroll = {
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -33,7 +33,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -42,7 +42,7 @@ Object {
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -69,14 +69,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -103,14 +103,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -148,7 +148,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -157,7 +157,7 @@ Object {
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -184,14 +184,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
|
@ -18,14 +18,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 1014066025,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 2019559783,
|
"versionNonce": 238820263,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 20,
|
"y": 20,
|
||||||
@ -50,14 +50,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1604849351,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": -10,
|
"x": -10,
|
||||||
"y": 60,
|
"y": 60,
|
||||||
@ -82,14 +82,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 453191,
|
"versionNonce": 1150084233,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 40,
|
"y": 40,
|
||||||
@ -119,14 +119,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 1014066025,
|
"versionNonce": 81784553,
|
||||||
"width": 100,
|
"width": 100,
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
@ -156,14 +156,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 449462985,
|
"seed": 2019559783,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 6,
|
"version": 6,
|
||||||
"versionNonce": 1723083209,
|
"versionNonce": 927333447,
|
||||||
"width": 300,
|
"width": 300,
|
||||||
"x": 201,
|
"x": 201,
|
||||||
"y": 2,
|
"y": 2,
|
||||||
@ -205,7 +205,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 401146281,
|
"seed": 238820263,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": Object {
|
"startBinding": Object {
|
||||||
"elementId": "id0",
|
"elementId": "id0",
|
||||||
@ -218,7 +218,7 @@ Object {
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 11,
|
"version": 11,
|
||||||
"versionNonce": 1006504105,
|
"versionNonce": 1051383431,
|
||||||
"width": 81,
|
"width": 81,
|
||||||
"x": 110,
|
"x": 110,
|
||||||
"y": 49.981789081137734,
|
"y": 49.981789081137734,
|
||||||
|
@ -38,7 +38,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -47,7 +47,7 @@ Object {
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1505387817,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
@ -92,7 +92,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -101,7 +101,7 @@ Object {
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 7,
|
"version": 7,
|
||||||
"versionNonce": 1150084233,
|
"versionNonce": 1505387817,
|
||||||
"width": 70,
|
"width": 70,
|
||||||
"x": 30,
|
"x": 30,
|
||||||
"y": 30,
|
"y": 30,
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -31,7 +31,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -40,7 +40,7 @@ Object {
|
|||||||
"type": "arrow",
|
"type": "arrow",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -78,7 +78,7 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"startArrowhead": null,
|
"startArrowhead": null,
|
||||||
"startBinding": null,
|
"startBinding": null,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
@ -87,7 +87,7 @@ Object {
|
|||||||
"type": "line",
|
"type": "line",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 3,
|
"version": 3,
|
||||||
"versionNonce": 449462985,
|
"versionNonce": 401146281,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -112,14 +112,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "diamond",
|
"type": "diamond",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -144,14 +144,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 2,
|
"type": 2,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "ellipse",
|
"type": "ellipse",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
@ -176,14 +176,14 @@ Object {
|
|||||||
"roundness": Object {
|
"roundness": Object {
|
||||||
"type": 3,
|
"type": 3,
|
||||||
},
|
},
|
||||||
"seed": 337897,
|
"seed": 1278240551,
|
||||||
"strokeColor": "#1e1e1e",
|
"strokeColor": "#1e1e1e",
|
||||||
"strokeStyle": "solid",
|
"strokeStyle": "solid",
|
||||||
"strokeWidth": 1,
|
"strokeWidth": 1,
|
||||||
"type": "rectangle",
|
"type": "rectangle",
|
||||||
"updated": 1,
|
"updated": 1,
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"versionNonce": 1278240551,
|
"versionNonce": 453191,
|
||||||
"width": 30,
|
"width": 30,
|
||||||
"x": 10,
|
"x": 10,
|
||||||
"y": 10,
|
"y": 10,
|
||||||
|
@ -23,7 +23,7 @@ import { setDateTimeForTests } from "../utils";
|
|||||||
import { LibraryItem } from "../types";
|
import { LibraryItem } from "../types";
|
||||||
|
|
||||||
const checkpoint = (name: string) => {
|
const checkpoint = (name: string) => {
|
||||||
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||||
`[${name}] number of renders`,
|
`[${name}] number of renders`,
|
||||||
);
|
);
|
||||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||||
@ -39,10 +39,10 @@ const mouse = new Pointer("mouse");
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,7 +51,7 @@ const { h } = window;
|
|||||||
describe("contextMenu element", () => {
|
describe("contextMenu element", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
setDateTimeForTests("201933152653");
|
setDateTimeForTests("201933152653");
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ describe("contextMenu element", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("shows context menu for canvas", () => {
|
it("shows context menu for canvas", () => {
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -104,7 +104,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -158,7 +158,7 @@ describe("contextMenu element", () => {
|
|||||||
API.setSelectedElements([rect1]);
|
API.setSelectedElements([rect1]);
|
||||||
|
|
||||||
// lower z-index
|
// lower z-index
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
@ -168,7 +168,7 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
// higher z-index
|
// higher z-index
|
||||||
API.setSelectedElements([rect2]);
|
API.setSelectedElements([rect2]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 100,
|
clientX: 100,
|
||||||
clientY: 100,
|
clientY: 100,
|
||||||
@ -192,7 +192,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.click(20, 0);
|
mouse.click(20, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -245,7 +245,7 @@ describe("contextMenu element", () => {
|
|||||||
Keyboard.keyPress(KEYS.G);
|
Keyboard.keyPress(KEYS.G);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -284,7 +284,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -332,7 +332,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.reset();
|
mouse.reset();
|
||||||
|
|
||||||
// Copy styles of second rectangle
|
// Copy styles of second rectangle
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 40,
|
||||||
clientY: 40,
|
clientY: 40,
|
||||||
@ -345,7 +345,7 @@ describe("contextMenu element", () => {
|
|||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
// Paste styles to first rectangle
|
// Paste styles to first rectangle
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 10,
|
||||||
clientY: 10,
|
clientY: 10,
|
||||||
@ -369,7 +369,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -385,7 +385,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -406,7 +406,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.down(10, 10);
|
mouse.down(10, 10);
|
||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -429,7 +429,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 40,
|
||||||
clientY: 40,
|
clientY: 40,
|
||||||
@ -451,7 +451,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 10,
|
||||||
clientY: 10,
|
clientY: 10,
|
||||||
@ -473,7 +473,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 40,
|
clientX: 40,
|
||||||
clientY: 40,
|
clientY: 40,
|
||||||
@ -494,7 +494,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.up(20, 20);
|
mouse.up(20, 20);
|
||||||
|
|
||||||
mouse.reset();
|
mouse.reset();
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 10,
|
clientX: 10,
|
||||||
clientY: 10,
|
clientY: 10,
|
||||||
@ -519,7 +519,7 @@ describe("contextMenu element", () => {
|
|||||||
mouse.click(10, 10);
|
mouse.click(10, 10);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -549,7 +549,7 @@ describe("contextMenu element", () => {
|
|||||||
Keyboard.keyPress(KEYS.G);
|
Keyboard.keyPress(KEYS.G);
|
||||||
});
|
});
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
|
@ -14,10 +14,13 @@ import { reseed } from "../random";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = jest.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -31,7 +34,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -42,7 +45,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -62,7 +66,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -73,7 +77,9 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -93,7 +99,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -104,7 +110,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -124,7 +131,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -135,7 +142,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -159,7 +167,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -170,7 +178,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
|
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
@ -202,7 +211,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -210,7 +219,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -221,7 +231,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -229,7 +239,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -240,7 +251,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -248,7 +259,8 @@ describe("Test dragCreate", () => {
|
|||||||
// finish (position does not matter)
|
// finish (position does not matter)
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -259,7 +271,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -272,7 +284,8 @@ describe("Test dragCreate", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
@ -283,7 +296,7 @@ describe("Test dragCreate", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
// start from (30, 20)
|
// start from (30, 20)
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
@ -296,7 +309,8 @@ describe("Test dragCreate", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(8);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
@ -268,7 +268,7 @@ export class API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
static drop = async (blob: Blob) => {
|
static drop = async (blob: Blob) => {
|
||||||
const fileDropEvent = createEvent.drop(GlobalTestState.canvas);
|
const fileDropEvent = createEvent.drop(GlobalTestState.interactiveCanvas);
|
||||||
const text = await new Promise<string>((resolve, reject) => {
|
const text = await new Promise<string>((resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@ -295,6 +295,6 @@ export class API {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
fireEvent(GlobalTestState.canvas, fileDropEvent);
|
fireEvent(GlobalTestState.interactiveCanvas, fileDropEvent);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -107,7 +107,7 @@ export class Pointer {
|
|||||||
restorePosition(x = 0, y = 0) {
|
restorePosition(x = 0, y = 0) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
private getEvent() {
|
private getEvent() {
|
||||||
@ -129,18 +129,18 @@ export class Pointer {
|
|||||||
if (dx !== 0 || dy !== 0) {
|
if (dx !== 0 || dy !== 0) {
|
||||||
this.clientX += dx;
|
this.clientX += dx;
|
||||||
this.clientY += dy;
|
this.clientY += dy;
|
||||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
down(dx = 0, dy = 0) {
|
down(dx = 0, dy = 0) {
|
||||||
this.move(dx, dy);
|
this.move(dx, dy);
|
||||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
up(dx = 0, dy = 0) {
|
up(dx = 0, dy = 0) {
|
||||||
this.move(dx, dy);
|
this.move(dx, dy);
|
||||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
click(dx = 0, dy = 0) {
|
click(dx = 0, dy = 0) {
|
||||||
@ -150,7 +150,7 @@ export class Pointer {
|
|||||||
|
|
||||||
doubleClick(dx = 0, dy = 0) {
|
doubleClick(dx = 0, dy = 0) {
|
||||||
this.move(dx, dy);
|
this.move(dx, dy);
|
||||||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// absolute coords
|
// absolute coords
|
||||||
@ -159,19 +159,19 @@ export class Pointer {
|
|||||||
moveTo(x: number = this.clientX, y: number = this.clientY) {
|
moveTo(x: number = this.clientX, y: number = this.clientY) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerMove(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerMove(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
downAt(x = this.clientX, y = this.clientY) {
|
downAt(x = this.clientX, y = this.clientY) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerDown(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerDown(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
upAt(x = this.clientX, y = this.clientY) {
|
upAt(x = this.clientX, y = this.clientY) {
|
||||||
this.clientX = x;
|
this.clientX = x;
|
||||||
this.clientY = y;
|
this.clientY = y;
|
||||||
fireEvent.pointerUp(GlobalTestState.canvas, this.getEvent());
|
fireEvent.pointerUp(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
clickAt(x: number, y: number) {
|
clickAt(x: number, y: number) {
|
||||||
@ -180,7 +180,7 @@ export class Pointer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rightClickAt(x: number, y: number) {
|
rightClickAt(x: number, y: number) {
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: x,
|
clientX: x,
|
||||||
clientY: y,
|
clientY: y,
|
||||||
@ -189,7 +189,7 @@ export class Pointer {
|
|||||||
|
|
||||||
doubleClickAt(x: number, y: number) {
|
doubleClickAt(x: number, y: number) {
|
||||||
this.moveTo(x, y);
|
this.moveTo(x, y);
|
||||||
fireEvent.doubleClick(GlobalTestState.canvas, this.getEvent());
|
fireEvent.doubleClick(GlobalTestState.interactiveCanvas, this.getEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -327,6 +327,13 @@ export class UI {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static ungroup(elements: ExcalidrawElement[]) {
|
||||||
|
mouse.select(elements);
|
||||||
|
Keyboard.withModifierKeys({ ctrl: true, shift: true }, () => {
|
||||||
|
Keyboard.keyPress(KEYS.G);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static queryContextMenu = () => {
|
static queryContextMenu = () => {
|
||||||
return GlobalTestState.renderResult.container.querySelector(
|
return GlobalTestState.renderResult.container.querySelector(
|
||||||
".context-menu",
|
".context-menu",
|
||||||
|
@ -25,7 +25,9 @@ import {
|
|||||||
import * as textElementUtils from "../element/textElement";
|
import * as textElementUtils from "../element/textElement";
|
||||||
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
import { ROUNDNESS, VERTICAL_ALIGN } from "../constants";
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
// FIXME I: add specific tests for render of both components (when they should both, when just one, when just second, what is the order of renders - first static, then interactive, etc., all tests)
|
||||||
|
const renderInteractiveScene = jest.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
||||||
@ -33,18 +35,23 @@ const font = "20px Cascadia, width: Segoe UI Emoji" as FontString;
|
|||||||
describe("Test Linear Elements", () => {
|
describe("Test Linear Elements", () => {
|
||||||
let container: HTMLElement;
|
let container: HTMLElement;
|
||||||
let canvas: HTMLCanvasElement;
|
let canvas: HTMLCanvasElement;
|
||||||
|
let interactiveCanvas: HTMLCanvasElement;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
const comp = await render(<ExcalidrawApp />);
|
const comp = await render(<ExcalidrawApp />);
|
||||||
container = comp.container;
|
container = comp.container;
|
||||||
canvas = container.querySelector("canvas")!;
|
canvas = container.querySelector("canvas.static")!;
|
||||||
canvas.width = 1000;
|
canvas.width = 1000;
|
||||||
canvas.height = 1000;
|
canvas.height = 1000;
|
||||||
|
interactiveCanvas = container.querySelector("canvas.interactive")!;
|
||||||
|
interactiveCanvas.width = 1000;
|
||||||
|
interactiveCanvas.height = 1000;
|
||||||
});
|
});
|
||||||
|
|
||||||
const p1: Point = [20, 20];
|
const p1: Point = [20, 20];
|
||||||
@ -119,26 +126,26 @@ describe("Test Linear Elements", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const drag = (startPoint: Point, endPoint: Point) => {
|
const drag = (startPoint: Point, endPoint: Point) => {
|
||||||
fireEvent.pointerDown(canvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: startPoint[0],
|
clientX: startPoint[0],
|
||||||
clientY: startPoint[1],
|
clientY: startPoint[1],
|
||||||
});
|
});
|
||||||
fireEvent.pointerMove(canvas, {
|
fireEvent.pointerMove(interactiveCanvas, {
|
||||||
clientX: endPoint[0],
|
clientX: endPoint[0],
|
||||||
clientY: endPoint[1],
|
clientY: endPoint[1],
|
||||||
});
|
});
|
||||||
fireEvent.pointerUp(canvas, {
|
fireEvent.pointerUp(interactiveCanvas, {
|
||||||
clientX: endPoint[0],
|
clientX: endPoint[0],
|
||||||
clientY: endPoint[1],
|
clientY: endPoint[1],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const deletePoint = (point: Point) => {
|
const deletePoint = (point: Point) => {
|
||||||
fireEvent.pointerDown(canvas, {
|
fireEvent.pointerDown(interactiveCanvas, {
|
||||||
clientX: point[0],
|
clientX: point[0],
|
||||||
clientY: point[1],
|
clientY: point[1],
|
||||||
});
|
});
|
||||||
fireEvent.pointerUp(canvas, {
|
fireEvent.pointerUp(interactiveCanvas, {
|
||||||
clientX: point[0],
|
clientX: point[0],
|
||||||
clientY: point[1],
|
clientY: point[1],
|
||||||
});
|
});
|
||||||
@ -171,12 +178,14 @@ describe("Test Linear Elements", () => {
|
|||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
const line = h.elements[0] as ExcalidrawLinearElement;
|
const line = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
expect((h.elements[0] as ExcalidrawLinearElement).points.length).toEqual(2);
|
||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
@ -198,14 +207,14 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
it("should allow entering and exiting line editor via context menu", () => {
|
it("should allow entering and exiting line editor via context menu", () => {
|
||||||
createTwoPointerLinearElement("line");
|
createTwoPointerLinearElement("line");
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
});
|
});
|
||||||
// Enter line editor
|
// Enter line editor
|
||||||
let contextMenu = document.querySelector(".context-menu");
|
let contextMenu = document.querySelector(".context-menu");
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
@ -215,13 +224,13 @@ describe("Test Linear Elements", () => {
|
|||||||
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
expect(h.state.editingLinearElement?.elementId).toEqual(h.elements[0].id);
|
||||||
|
|
||||||
// Exiting line editor
|
// Exiting line editor
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
});
|
});
|
||||||
contextMenu = document.querySelector(".context-menu");
|
contextMenu = document.querySelector(".context-menu");
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: midpoint[0],
|
clientX: midpoint[0],
|
||||||
clientY: midpoint[1],
|
clientY: midpoint[1],
|
||||||
@ -269,7 +278,8 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
// drag line from midpoint
|
// drag line from midpoint
|
||||||
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
drag(midpoint, [midpoint[0] + delta, midpoint[1] + delta]);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(12);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(line.points).toMatchInlineSnapshot(`
|
expect(line.points).toMatchInlineSnapshot(`
|
||||||
@ -306,7 +316,9 @@ describe("Test Linear Elements", () => {
|
|||||||
// update roundness
|
// update roundness
|
||||||
fireEvent.click(screen.getByTitle("Round"));
|
fireEvent.click(screen.getByTitle("Round"));
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(12);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
|
|
||||||
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
const midPointsWithRoundEdge = LinearElementEditor.getEditorMidPoints(
|
||||||
h.elements[0] as ExcalidrawLinearElement,
|
h.elements[0] as ExcalidrawLinearElement,
|
||||||
h.state,
|
h.state,
|
||||||
@ -350,7 +362,9 @@ describe("Test Linear Elements", () => {
|
|||||||
// Move the element
|
// Move the element
|
||||||
drag(startPoint, endPoint);
|
drag(startPoint, endPoint);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
expect([line.x, line.y]).toEqual([
|
expect([line.x, line.y]).toEqual([
|
||||||
points[0][0] + deltaX,
|
points[0][0] + deltaX,
|
||||||
points[0][1] + deltaY,
|
points[0][1] + deltaY,
|
||||||
@ -407,7 +421,9 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(17);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||||
@ -446,7 +462,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
drag(hitCoords, [hitCoords[0] - delta, hitCoords[1] - delta]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||||
@ -472,7 +489,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||||
@ -506,7 +524,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// delete 3rd point
|
// delete 3rd point
|
||||||
deletePoint(points[2]);
|
deletePoint(points[2]);
|
||||||
expect(line.points.length).toEqual(3);
|
expect(line.points.length).toEqual(3);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(22);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(19);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
|
|
||||||
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
const newMidPoints = LinearElementEditor.getEditorMidPoints(
|
||||||
line,
|
line,
|
||||||
@ -552,8 +571,8 @@ describe("Test Linear Elements", () => {
|
|||||||
lastSegmentMidpoint[0] + delta,
|
lastSegmentMidpoint[0] + delta,
|
||||||
lastSegmentMidpoint[1] + delta,
|
lastSegmentMidpoint[1] + delta,
|
||||||
]);
|
]);
|
||||||
expect(renderScene).toHaveBeenCalledTimes(21);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(17);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(line.points.length).toEqual(5);
|
expect(line.points.length).toEqual(5);
|
||||||
|
|
||||||
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
expect((h.elements[0] as ExcalidrawLinearElement).points)
|
||||||
@ -628,7 +647,8 @@ describe("Test Linear Elements", () => {
|
|||||||
// Drag from first point
|
// Drag from first point
|
||||||
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
drag(hitCoords, [hitCoords[0] + delta, hitCoords[1] + delta]);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(16);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(13);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
|
|
||||||
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
const newPoints = LinearElementEditor.getPointsGlobalCoordinates(line);
|
||||||
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
expect([newPoints[0][0], newPoints[0][1]]).toEqual([
|
||||||
@ -1205,7 +1225,7 @@ describe("Test Linear Elements", () => {
|
|||||||
|
|
||||||
const container = h.elements[0];
|
const container = h.elements[0];
|
||||||
API.setSelectedElements([container, text]);
|
API.setSelectedElements([container, text]);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
@ -1230,7 +1250,7 @@ describe("Test Linear Elements", () => {
|
|||||||
mouse.up();
|
mouse.up();
|
||||||
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
API.setSelectedElements([h.elements[0], h.elements[1]]);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 20,
|
clientX: 20,
|
||||||
clientY: 30,
|
clientY: 30,
|
||||||
|
@ -16,10 +16,12 @@ import { KEYS } from "../keys";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = jest.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -28,7 +30,7 @@ const { h } = window;
|
|||||||
describe("move element", () => {
|
describe("move element", () => {
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
@ -38,20 +40,23 @@ describe("move element", () => {
|
|||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||||
|
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
fireEvent.pointerMove(canvas, { clientX: 20, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(3);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(2);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([0, 40]);
|
||||||
@ -77,7 +82,8 @@ describe("move element", () => {
|
|||||||
// select the second rectangles
|
// select the second rectangles
|
||||||
new Pointer("mouse").clickOn(rectB);
|
new Pointer("mouse").clickOn(rectB);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(23);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(21);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(13);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
@ -86,7 +92,8 @@ describe("move element", () => {
|
|||||||
expect([line.x, line.y]).toEqual([110, 50]);
|
expect([line.x, line.y]).toEqual([110, 50]);
|
||||||
expect([line.width, line.height]).toEqual([80, 80]);
|
expect([line.width, line.height]).toEqual([80, 80]);
|
||||||
|
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
|
|
||||||
// Move selected rectangle
|
// Move selected rectangle
|
||||||
Keyboard.keyDown(KEYS.ARROW_RIGHT);
|
Keyboard.keyDown(KEYS.ARROW_RIGHT);
|
||||||
@ -94,7 +101,8 @@ describe("move element", () => {
|
|||||||
Keyboard.keyDown(KEYS.ARROW_DOWN);
|
Keyboard.keyDown(KEYS.ARROW_DOWN);
|
||||||
|
|
||||||
// Check that the arrow size has been changed according to moving the rectangle
|
// Check that the arrow size has been changed according to moving the rectangle
|
||||||
expect(renderScene).toHaveBeenCalledTimes(3);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(3);
|
expect(h.elements.length).toEqual(3);
|
||||||
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
expect(h.state.selectedElementIds[rectB.id]).toBeTruthy();
|
||||||
@ -110,7 +118,7 @@ describe("move element", () => {
|
|||||||
describe("duplicate element on move when ALT is clicked", () => {
|
describe("duplicate element on move when ALT is clicked", () => {
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
|
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
@ -120,13 +128,15 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(9);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(6);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
expect([h.elements[0].x, h.elements[0].y]).toEqual([30, 20]);
|
||||||
|
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
}
|
}
|
||||||
|
|
||||||
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 50, clientY: 20 });
|
||||||
@ -140,7 +150,8 @@ describe("duplicate element on move when ALT is clicked", () => {
|
|||||||
|
|
||||||
// TODO: This used to be 4, but binding made it go up to 5. Do we need
|
// TODO: This used to be 4, but binding made it go up to 5. Do we need
|
||||||
// that additional render?
|
// that additional render?
|
||||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(3);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(2);
|
expect(h.elements.length).toEqual(2);
|
||||||
|
|
||||||
|
@ -14,10 +14,12 @@ import { reseed } from "../random";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = jest.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -38,11 +40,12 @@ describe("remove shape in non linear elements", () => {
|
|||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -52,11 +55,12 @@ describe("remove shape in non linear elements", () => {
|
|||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -66,11 +70,12 @@ describe("remove shape in non linear elements", () => {
|
|||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerUp(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(4);
|
||||||
expect(h.elements.length).toEqual(0);
|
expect(h.elements.length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -82,7 +87,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
// first point is added on pointer down
|
// first point is added on pointer down
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
@ -102,7 +107,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
@ -125,7 +131,7 @@ describe("multi point mode in linear elements", () => {
|
|||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
// first point is added on pointer down
|
// first point is added on pointer down
|
||||||
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
fireEvent.pointerDown(canvas, { clientX: 30, clientY: 30 });
|
||||||
|
|
||||||
@ -145,7 +151,8 @@ describe("multi point mode in linear elements", () => {
|
|||||||
key: KEYS.ENTER,
|
key: KEYS.ENTER,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(15);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(10);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(9);
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
|
|
||||||
const element = h.elements[0] as ExcalidrawLinearElement;
|
const element = h.elements[0] as ExcalidrawLinearElement;
|
||||||
|
@ -23,7 +23,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
).toBe(0);
|
).toBe(0);
|
||||||
expect(h.state.zenModeEnabled).toBe(false);
|
expect(h.state.zenModeEnabled).toBe(false);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -43,7 +43,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
).toBe(0);
|
).toBe(0);
|
||||||
expect(h.state.zenModeEnabled).toBe(true);
|
expect(h.state.zenModeEnabled).toBe(true);
|
||||||
|
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -93,7 +93,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
expect(
|
expect(
|
||||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||||
).toBe(0);
|
).toBe(0);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
@ -112,7 +112,7 @@ describe("<Excalidraw/>", () => {
|
|||||||
expect(
|
expect(
|
||||||
container.getElementsByClassName("disable-zen-mode--visible").length,
|
container.getElementsByClassName("disable-zen-mode--visible").length,
|
||||||
).toBe(0);
|
).toBe(0);
|
||||||
fireEvent.contextMenu(GlobalTestState.canvas, {
|
fireEvent.contextMenu(GlobalTestState.interactiveCanvas, {
|
||||||
button: 2,
|
button: 2,
|
||||||
clientX: 1,
|
clientX: 1,
|
||||||
clientY: 1,
|
clientY: 1,
|
||||||
|
@ -20,7 +20,7 @@ import { FONT_FAMILY } from "../constants";
|
|||||||
|
|
||||||
const { h } = window;
|
const { h } = window;
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
|
|
||||||
const mouse = new Pointer("mouse");
|
const mouse = new Pointer("mouse");
|
||||||
const finger1 = new Pointer("touch", 1);
|
const finger1 = new Pointer("touch", 1);
|
||||||
@ -32,7 +32,7 @@ const finger2 = new Pointer("touch", 2);
|
|||||||
* to debug where a test failure came from.
|
* to debug where a test failure came from.
|
||||||
*/
|
*/
|
||||||
const checkpoint = (name: string) => {
|
const checkpoint = (name: string) => {
|
||||||
expect(renderScene.mock.calls.length).toMatchSnapshot(
|
expect(renderStaticScene.mock.calls.length).toMatchSnapshot(
|
||||||
`[${name}] number of renders`,
|
`[${name}] number of renders`,
|
||||||
);
|
);
|
||||||
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
expect(h.state).toMatchSnapshot(`[${name}] appState`);
|
||||||
@ -47,7 +47,7 @@ beforeEach(async () => {
|
|||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
setDateTimeForTests("201933152653");
|
setDateTimeForTests("201933152653");
|
||||||
|
|
||||||
@ -1054,6 +1054,28 @@ describe("regression tests", () => {
|
|||||||
expect(API.getSelectedElements()).toEqual(selectedElements_prev);
|
expect(API.getSelectedElements()).toEqual(selectedElements_prev);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("deleting last but one element in editing group should unselect the group", () => {
|
||||||
|
const rect1 = UI.createElement("rectangle", { x: 10 });
|
||||||
|
const rect2 = UI.createElement("rectangle", { x: 50 });
|
||||||
|
|
||||||
|
UI.group([rect1, rect2]);
|
||||||
|
|
||||||
|
mouse.doubleClickOn(rect1);
|
||||||
|
Keyboard.keyDown(KEYS.DELETE);
|
||||||
|
|
||||||
|
// Clicking on the deleted element, hence in the empty space
|
||||||
|
mouse.clickOn(rect1);
|
||||||
|
|
||||||
|
expect(h.state.selectedGroupIds).toEqual({});
|
||||||
|
expect(API.getSelectedElements()).toEqual([]);
|
||||||
|
|
||||||
|
// Clicking back in and expecting no group selection
|
||||||
|
mouse.clickOn(rect2);
|
||||||
|
|
||||||
|
expect(h.state.selectedGroupIds).toEqual({ [rect2.groupIds[0]]: false });
|
||||||
|
expect(API.getSelectedElements()).toEqual([rect2.get()]);
|
||||||
|
});
|
||||||
|
|
||||||
it("Cmd/Ctrl-click exclusively select element under pointer", () => {
|
it("Cmd/Ctrl-click exclusively select element under pointer", () => {
|
||||||
const rect1 = UI.createElement("rectangle", { x: 0 });
|
const rect1 = UI.createElement("rectangle", { x: 0 });
|
||||||
const rect2 = UI.createElement("rectangle", { x: 30 });
|
const rect2 = UI.createElement("rectangle", { x: 30 });
|
||||||
|
@ -13,10 +13,10 @@ import { KEYS } from "../keys";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -17,10 +17,12 @@ import { SHAPES } from "../shapes";
|
|||||||
// Unmount ReactDOM from root
|
// Unmount ReactDOM from root
|
||||||
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
ReactDOM.unmountComponentAtNode(document.getElementById("root")!);
|
||||||
|
|
||||||
const renderScene = jest.spyOn(Renderer, "renderScene");
|
const renderInteractiveScene = jest.spyOn(Renderer, "renderInteractiveScene");
|
||||||
|
const renderStaticScene = jest.spyOn(Renderer, "renderStaticScene");
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
renderScene.mockClear();
|
renderInteractiveScene.mockClear();
|
||||||
|
renderStaticScene.mockClear();
|
||||||
reseed(7);
|
reseed(7);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -132,7 +134,7 @@ describe("inner box-selection", () => {
|
|||||||
});
|
});
|
||||||
h.elements = [rect1, rect2, rect3];
|
h.elements = [rect1, rect2, rect3];
|
||||||
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
Keyboard.withModifierKeys({ ctrl: true }, () => {
|
||||||
mouse.downAt(rect2.x - 20, rect2.x - 20);
|
mouse.downAt(rect2.x - 20, rect2.y - 20);
|
||||||
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
mouse.moveTo(rect2.x + rect2.width + 10, rect2.y + rect2.height + 10);
|
||||||
assertSelectedElements([rect2.id, rect3.id]);
|
assertSelectedElements([rect2.id, rect3.id]);
|
||||||
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
expect(h.state.selectedGroupIds).toEqual({ A: true });
|
||||||
@ -151,10 +153,11 @@ describe("selection element", () => {
|
|||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(5);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(3);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(2);
|
||||||
const selectionElement = h.state.selectionElement!;
|
const selectionElement = h.state.selectionElement!;
|
||||||
expect(selectionElement).not.toBeNull();
|
expect(selectionElement).not.toBeNull();
|
||||||
expect(selectionElement.type).toEqual("selection");
|
expect(selectionElement.type).toEqual("selection");
|
||||||
@ -171,11 +174,12 @@ describe("selection element", () => {
|
|||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(6);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(4);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(2);
|
||||||
const selectionElement = h.state.selectionElement!;
|
const selectionElement = h.state.selectionElement!;
|
||||||
expect(selectionElement).not.toBeNull();
|
expect(selectionElement).not.toBeNull();
|
||||||
expect(selectionElement.type).toEqual("selection");
|
expect(selectionElement.type).toEqual("selection");
|
||||||
@ -192,12 +196,13 @@ describe("selection element", () => {
|
|||||||
const tool = getByToolName("selection");
|
const tool = getByToolName("selection");
|
||||||
fireEvent.click(tool);
|
fireEvent.click(tool);
|
||||||
|
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
|
||||||
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(7);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(5);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(2);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -213,7 +218,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("rectangle", async () => {
|
it("rectangle", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("rectangle");
|
const tool = getByToolName("rectangle");
|
||||||
@ -232,7 +237,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -242,7 +248,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("diamond", async () => {
|
it("diamond", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("diamond");
|
const tool = getByToolName("diamond");
|
||||||
@ -261,7 +267,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -271,7 +278,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("ellipse", async () => {
|
it("ellipse", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("ellipse");
|
const tool = getByToolName("ellipse");
|
||||||
@ -290,7 +297,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
fireEvent.pointerDown(canvas, { clientX: 45, clientY: 20 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(5);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -300,7 +308,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("arrow", async () => {
|
it("arrow", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("arrow");
|
const tool = getByToolName("arrow");
|
||||||
@ -332,7 +340,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
@ -341,7 +350,7 @@ describe("select single element on the scene", () => {
|
|||||||
|
|
||||||
it("arrow escape", async () => {
|
it("arrow escape", async () => {
|
||||||
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
const { getByToolName, container } = await render(<ExcalidrawApp />);
|
||||||
const canvas = container.querySelector("canvas")!;
|
const canvas = container.querySelector("canvas.interactive")!;
|
||||||
{
|
{
|
||||||
// create element
|
// create element
|
||||||
const tool = getByToolName("line");
|
const tool = getByToolName("line");
|
||||||
@ -373,7 +382,8 @@ describe("select single element on the scene", () => {
|
|||||||
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
fireEvent.pointerDown(canvas, { clientX: 40, clientY: 40 });
|
||||||
fireEvent.pointerUp(canvas);
|
fireEvent.pointerUp(canvas);
|
||||||
|
|
||||||
expect(renderScene).toHaveBeenCalledTimes(11);
|
expect(renderInteractiveScene).toHaveBeenCalledTimes(9);
|
||||||
|
expect(renderStaticScene).toHaveBeenCalledTimes(6);
|
||||||
expect(h.state.selectionElement).toBeNull();
|
expect(h.state.selectionElement).toBeNull();
|
||||||
expect(h.elements.length).toEqual(1);
|
expect(h.elements.length).toEqual(1);
|
||||||
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
expect(h.state.selectedElementIds[h.elements[0].id]).toBeTruthy();
|
||||||
|
@ -49,15 +49,30 @@ const renderApp: TestRenderFn = async (ui, options) => {
|
|||||||
// child App component isn't likely mounted yet (and thus canvas not
|
// child App component isn't likely mounted yet (and thus canvas not
|
||||||
// present in DOM)
|
// present in DOM)
|
||||||
get() {
|
get() {
|
||||||
return renderResult.container.querySelector("canvas")!;
|
return renderResult.container.querySelector("canvas.static")!;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(GlobalTestState, "interactiveCanvas", {
|
||||||
|
// must be a getter because at the time of ExcalidrawApp render the
|
||||||
|
// child App component isn't likely mounted yet (and thus canvas not
|
||||||
|
// present in DOM)
|
||||||
|
get() {
|
||||||
|
return renderResult.container.querySelector("canvas.interactive")!;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
const canvas = renderResult.container.querySelector("canvas");
|
const canvas = renderResult.container.querySelector("canvas.static");
|
||||||
if (!canvas) {
|
if (!canvas) {
|
||||||
throw new Error("not initialized yet");
|
throw new Error("not initialized yet");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const interactiveCanvas =
|
||||||
|
renderResult.container.querySelector("canvas.interactive");
|
||||||
|
if (!interactiveCanvas) {
|
||||||
|
throw new Error("not initialized yet");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return renderResult;
|
return renderResult;
|
||||||
@ -81,11 +96,17 @@ export class GlobalTestState {
|
|||||||
*/
|
*/
|
||||||
static renderResult: RenderResult<typeof customQueries> = null!;
|
static renderResult: RenderResult<typeof customQueries> = null!;
|
||||||
/**
|
/**
|
||||||
* retrieves canvas for currently rendered app instance
|
* retrieves static canvas for currently rendered app instance
|
||||||
*/
|
*/
|
||||||
static get canvas(): HTMLCanvasElement {
|
static get canvas(): HTMLCanvasElement {
|
||||||
return null!;
|
return null!;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* retrieves interactive canvas for currently rendered app instance
|
||||||
|
*/
|
||||||
|
static get interactiveCanvas(): HTMLCanvasElement {
|
||||||
|
return null!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const initLocalStorage = (data: ImportedDataState) => {
|
const initLocalStorage = (data: ImportedDataState) => {
|
||||||
|
@ -17,7 +17,9 @@ describe("view mode", () => {
|
|||||||
|
|
||||||
it("after switching to view mode – cursor type should be pointer", async () => {
|
it("after switching to view mode – cursor type should be pointer", async () => {
|
||||||
h.setState({ viewModeEnabled: true });
|
h.setState({ viewModeEnabled: true });
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => {
|
it("after switching to view mode, moving, clicking, and pressing space key – cursor type should be pointer", async () => {
|
||||||
@ -29,7 +31,9 @@ describe("view mode", () => {
|
|||||||
pointer.move(100, 100);
|
pointer.move(100, 100);
|
||||||
pointer.click();
|
pointer.click();
|
||||||
Keyboard.keyPress(KEYS.SPACE);
|
Keyboard.keyPress(KEYS.SPACE);
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -45,13 +49,19 @@ describe("view mode", () => {
|
|||||||
pointer.moveTo(50, 50);
|
pointer.moveTo(50, 50);
|
||||||
// eslint-disable-next-line dot-notation
|
// eslint-disable-next-line dot-notation
|
||||||
if (pointerType["pointerType"] === "mouse") {
|
if (pointerType["pointerType"] === "mouse") {
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.MOVE);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.MOVE,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
h.setState({ viewModeEnabled: true });
|
h.setState({ viewModeEnabled: true });
|
||||||
expect(GlobalTestState.canvas.style.cursor).toBe(CURSOR_TYPE.GRAB);
|
expect(GlobalTestState.interactiveCanvas.style.cursor).toBe(
|
||||||
|
CURSOR_TYPE.GRAB,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -94,7 +94,7 @@ const populateElements = (
|
|||||||
),
|
),
|
||||||
...appState,
|
...appState,
|
||||||
selectedElementIds,
|
selectedElementIds,
|
||||||
});
|
} as AppState);
|
||||||
|
|
||||||
return selectedElementIds;
|
return selectedElementIds;
|
||||||
};
|
};
|
||||||
|
46
src/types.ts
46
src/types.ts
@ -98,6 +98,51 @@ export type LastActiveTool =
|
|||||||
export type SidebarName = string;
|
export type SidebarName = string;
|
||||||
export type SidebarTabName = string;
|
export type SidebarTabName = string;
|
||||||
|
|
||||||
|
export type CommonCanvasAppState = {
|
||||||
|
zoom: AppState["zoom"];
|
||||||
|
scrollX: AppState["scrollX"];
|
||||||
|
scrollY: AppState["scrollY"];
|
||||||
|
width: AppState["width"];
|
||||||
|
height: AppState["height"];
|
||||||
|
viewModeEnabled: AppState["viewModeEnabled"];
|
||||||
|
editingElement: AppState["editingElement"];
|
||||||
|
editingGroupId: AppState["editingGroupId"]; // TODO: move to interactive canvas if possible
|
||||||
|
selectedElementIds: AppState["selectedElementIds"]; // TODO: move to interactive canvas if possible
|
||||||
|
frameToHighlight: AppState["frameToHighlight"]; // TODO: move to interactive canvas if possible
|
||||||
|
offsetLeft: AppState["offsetLeft"];
|
||||||
|
offsetTop: AppState["offsetTop"];
|
||||||
|
theme: AppState["theme"];
|
||||||
|
pendingImageElementId: AppState["pendingImageElementId"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StaticCanvasAppState = CommonCanvasAppState & {
|
||||||
|
shouldCacheIgnoreZoom: AppState["shouldCacheIgnoreZoom"];
|
||||||
|
/** null indicates transparent bg */
|
||||||
|
viewBackgroundColor?: AppState["viewBackgroundColor"];
|
||||||
|
exportScale: AppState["exportScale"];
|
||||||
|
selectedElementsAreBeingDragged: AppState["selectedElementsAreBeingDragged"];
|
||||||
|
gridSize: AppState["gridSize"];
|
||||||
|
frameRendering: AppState["frameRendering"];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type InteractiveCanvasAppState = CommonCanvasAppState & {
|
||||||
|
// renderInteractiveScene
|
||||||
|
editingLinearElement: AppState["editingLinearElement"];
|
||||||
|
selectionElement: AppState["selectionElement"];
|
||||||
|
selectedGroupIds: AppState["selectedGroupIds"];
|
||||||
|
selectedLinearElement: AppState["selectedLinearElement"];
|
||||||
|
multiElement: AppState["multiElement"];
|
||||||
|
isBindingEnabled: AppState["isBindingEnabled"];
|
||||||
|
suggestedBindings: AppState["suggestedBindings"];
|
||||||
|
isRotating: AppState["isRotating"];
|
||||||
|
elementsToHighlight: AppState["elementsToHighlight"];
|
||||||
|
// App
|
||||||
|
openSidebar: AppState["openSidebar"];
|
||||||
|
showHyperlinkPopup: AppState["showHyperlinkPopup"];
|
||||||
|
// Collaborators
|
||||||
|
collaborators: AppState["collaborators"];
|
||||||
|
};
|
||||||
|
|
||||||
export type AppState = {
|
export type AppState = {
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
items: ContextMenuItems;
|
items: ContextMenuItems;
|
||||||
@ -434,6 +479,7 @@ export type AppProps = Merge<
|
|||||||
export type AppClassProperties = {
|
export type AppClassProperties = {
|
||||||
props: AppProps;
|
props: AppProps;
|
||||||
canvas: HTMLCanvasElement | null;
|
canvas: HTMLCanvasElement | null;
|
||||||
|
interactiveCanvas: HTMLCanvasElement | null;
|
||||||
focusContainer(): void;
|
focusContainer(): void;
|
||||||
library: Library;
|
library: Library;
|
||||||
imageCache: Map<
|
imageCache: Map<
|
||||||
|
31
src/utils.ts
31
src/utils.ts
@ -392,22 +392,25 @@ export const updateActiveTool = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const resetCursor = (canvas: HTMLCanvasElement | null) => {
|
export const resetCursor = (interactiveCanvas: HTMLCanvasElement | null) => {
|
||||||
if (canvas) {
|
if (interactiveCanvas) {
|
||||||
canvas.style.cursor = "";
|
interactiveCanvas.style.cursor = "";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setCursor = (canvas: HTMLCanvasElement | null, cursor: string) => {
|
export const setCursor = (
|
||||||
if (canvas) {
|
interactiveCanvas: HTMLCanvasElement | null,
|
||||||
canvas.style.cursor = cursor;
|
cursor: string,
|
||||||
|
) => {
|
||||||
|
if (interactiveCanvas) {
|
||||||
|
interactiveCanvas.style.cursor = cursor;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let eraserCanvasCache: any;
|
let eraserCanvasCache: any;
|
||||||
let previewDataURL: string;
|
let previewDataURL: string;
|
||||||
export const setEraserCursor = (
|
export const setEraserCursor = (
|
||||||
canvas: HTMLCanvasElement | null,
|
interactiveCanvas: HTMLCanvasElement | null,
|
||||||
theme: AppState["theme"],
|
theme: AppState["theme"],
|
||||||
) => {
|
) => {
|
||||||
const cursorImageSizePx = 20;
|
const cursorImageSizePx = 20;
|
||||||
@ -439,7 +442,7 @@ export const setEraserCursor = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
setCursor(
|
setCursor(
|
||||||
canvas,
|
interactiveCanvas,
|
||||||
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
`url(${previewDataURL}) ${cursorImageSizePx / 2} ${
|
||||||
cursorImageSizePx / 2
|
cursorImageSizePx / 2
|
||||||
}, auto`,
|
}, auto`,
|
||||||
@ -447,23 +450,23 @@ export const setEraserCursor = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const setCursorForShape = (
|
export const setCursorForShape = (
|
||||||
canvas: HTMLCanvasElement | null,
|
interactiveCanvas: HTMLCanvasElement | null,
|
||||||
appState: Pick<AppState, "activeTool" | "theme">,
|
appState: Pick<AppState, "activeTool" | "theme">,
|
||||||
) => {
|
) => {
|
||||||
if (!canvas) {
|
if (!interactiveCanvas) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (appState.activeTool.type === "selection") {
|
if (appState.activeTool.type === "selection") {
|
||||||
resetCursor(canvas);
|
resetCursor(interactiveCanvas);
|
||||||
} else if (isHandToolActive(appState)) {
|
} else if (isHandToolActive(appState)) {
|
||||||
canvas.style.cursor = CURSOR_TYPE.GRAB;
|
interactiveCanvas.style.cursor = CURSOR_TYPE.GRAB;
|
||||||
} else if (isEraserActive(appState)) {
|
} else if (isEraserActive(appState)) {
|
||||||
setEraserCursor(canvas, appState.theme);
|
setEraserCursor(interactiveCanvas, appState.theme);
|
||||||
// do nothing if image tool is selected which suggests there's
|
// do nothing if image tool is selected which suggests there's
|
||||||
// a image-preview set as the cursor
|
// a image-preview set as the cursor
|
||||||
// Ignore custom type as well and let host decide
|
// Ignore custom type as well and let host decide
|
||||||
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
} else if (!["image", "custom"].includes(appState.activeTool.type)) {
|
||||||
canvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
interactiveCanvas.style.cursor = CURSOR_TYPE.CROSSHAIR;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user