calculate coords based on container viewport position (#1955)

* feat: calculate coords based on parent left and top so it renders correctly in host App

* fix text

* move offsets to state & fix bugs

* fix text jumping

* account for zoom in textWysiwyg & undo incorrect offsetting

Co-authored-by: dwelle <luzar.david@gmail.com>
This commit is contained in:
Aakansha Doshi 2020-07-27 17:18:49 +05:30 committed by GitHub
parent 63edbb9517
commit 7eff6893c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 361 additions and 179 deletions

View File

@ -135,7 +135,7 @@ export const actionLoadScene = register({
commitToHistory: false, commitToHistory: false,
}; };
}, },
PanelComponent: ({ updateData }) => ( PanelComponent: ({ updateData, appState }) => (
<ToolButton <ToolButton
type="button" type="button"
icon={load} icon={load}
@ -143,7 +143,7 @@ export const actionLoadScene = register({
aria-label={t("buttons.load")} aria-label={t("buttons.load")}
showAriaLabel={useIsMobile()} showAriaLabel={useIsMobile()}
onClick={() => { onClick={() => {
loadFromJSON() loadFromJSON(appState)
.then(({ elements, appState }) => { .then(({ elements, appState }) => {
updateData({ elements: elements, appState: appState }); updateData({ elements: elements, appState: appState });
}) })

View File

@ -6,7 +6,7 @@ import { AppState } from "../types";
export type ActionResult = export type ActionResult =
| { | {
elements?: readonly ExcalidrawElement[] | null; elements?: readonly ExcalidrawElement[] | null;
appState?: AppState | null; appState?: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
commitToHistory: boolean; commitToHistory: boolean;
syncHistory?: boolean; syncHistory?: boolean;
} }

View File

@ -8,7 +8,10 @@ import {
DEFAULT_TEXT_ALIGN, DEFAULT_TEXT_ALIGN,
} from "./constants"; } from "./constants";
export const getDefaultAppState = (): AppState => { export const getDefaultAppState = (): Omit<
AppState,
"offsetTop" | "offsetLeft"
> => {
return { return {
isLoading: false, isLoading: false,
errorMessage: null, errorMessage: null,
@ -126,6 +129,8 @@ const APP_STATE_STORAGE_CONF = (<
width: { browser: false, export: false }, width: { browser: false, export: false },
zenModeEnabled: { browser: true, export: false }, zenModeEnabled: { browser: true, export: false },
zoom: { browser: true, export: false }, zoom: { browser: true, export: false },
offsetTop: { browser: false, export: false },
offsetLeft: { browser: false, export: false },
}); });
const _clearAppStateForStorage = <ExportType extends "export" | "browser">( const _clearAppStateForStorage = <ExportType extends "export" | "browser">(

View File

@ -3,7 +3,7 @@ import React from "react";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import { RoughCanvas } from "roughjs/bin/canvas"; import { RoughCanvas } from "roughjs/bin/canvas";
import { simplify, Point } from "points-on-curve"; import { simplify, Point } from "points-on-curve";
import { FlooredNumber, SocketUpdateData } from "../types"; import { SocketUpdateData } from "../types";
import { import {
newElement, newElement,
@ -244,6 +244,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
removeSceneCallback: SceneStateCallbackRemover | null = null; removeSceneCallback: SceneStateCallbackRemover | null = null;
unmounted: boolean = false; unmounted: boolean = false;
actionManager: ActionManager; actionManager: ActionManager;
private excalidrawRef: any;
public static defaultProps: Partial<ExcalidrawProps> = { public static defaultProps: Partial<ExcalidrawProps> = {
width: window.innerWidth, width: window.innerWidth,
@ -260,8 +261,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
isLoading: true, isLoading: true,
width, width,
height, height,
...this.getCanvasOffsets(),
}; };
this.excalidrawRef = React.createRef();
this.actionManager = new ActionManager( this.actionManager = new ActionManager(
this.syncActionResult, this.syncActionResult,
() => this.state, () => this.state,
@ -278,6 +281,8 @@ class App extends React.Component<ExcalidrawProps, AppState> {
zenModeEnabled, zenModeEnabled,
width: canvasDOMWidth, width: canvasDOMWidth,
height: canvasDOMHeight, height: canvasDOMHeight,
offsetTop,
offsetLeft,
} = this.state; } = this.state;
const canvasScale = window.devicePixelRatio; const canvasScale = window.devicePixelRatio;
@ -286,7 +291,16 @@ class App extends React.Component<ExcalidrawProps, AppState> {
const canvasHeight = canvasDOMHeight * canvasScale; const canvasHeight = canvasDOMHeight * canvasScale;
return ( return (
<div className="excalidraw"> <div
className="excalidraw"
ref={this.excalidrawRef}
style={{
width: canvasDOMWidth,
height: canvasDOMHeight,
top: offsetTop,
left: offsetLeft,
}}
>
<LayerUI <LayerUI
canvas={this.canvas} canvas={this.canvas}
appState={this.state} appState={this.state}
@ -369,6 +383,10 @@ class App extends React.Component<ExcalidrawProps, AppState> {
editingElement || actionResult.appState?.editingElement || null, editingElement || actionResult.appState?.editingElement || null,
isCollaborating: state.isCollaborating, isCollaborating: state.isCollaborating,
collaborators: state.collaborators, collaborators: state.collaborators,
width: state.width,
height: state.height,
offsetTop: state.offsetTop,
offsetLeft: state.offsetLeft,
}), }),
() => { () => {
if (actionResult.syncHistory) { if (actionResult.syncHistory) {
@ -498,6 +516,20 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (isCollaborationScene) { if (isCollaborationScene) {
this.initializeSocketClient({ showLoadingState: true }); this.initializeSocketClient({ showLoadingState: true });
} else if (scene) { } else if (scene) {
if (scene.appState) {
scene.appState = {
...scene.appState,
...calculateScrollCenter(
scene.elements,
{
...scene.appState,
offsetTop: this.state.offsetTop,
offsetLeft: this.state.offsetLeft,
},
null,
),
};
}
this.syncActionResult(scene); this.syncActionResult(scene);
} }
}; };
@ -533,7 +565,9 @@ class App extends React.Component<ExcalidrawProps, AppState> {
); );
this.addEventListeners(); this.addEventListeners();
this.initializeScene(); this.setState(this.getCanvasOffsets(), () => {
this.initializeScene();
});
} }
public componentWillUnmount() { public componentWillUnmount() {
@ -667,6 +701,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.setState({ this.setState({
width: currentWidth, width: currentWidth,
height: currentHeight, height: currentHeight,
...this.getCanvasOffsets(),
}); });
} }
@ -1548,10 +1583,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
textWysiwyg({ textWysiwyg({
id: element.id, id: element.id,
zoom: this.state.zoom, appState: this.state,
getViewportCoords: (x, y) => { getViewportCoords: (x, y) => {
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: x, sceneY: y }, {
sceneX: x,
sceneY: y,
},
this.state, this.state,
this.canvas, this.canvas,
window.devicePixelRatio, window.devicePixelRatio,
@ -3185,7 +3223,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
file?.name.endsWith(".excalidraw") file?.name.endsWith(".excalidraw")
) { ) {
this.setState({ isLoading: true }); this.setState({ isLoading: true });
loadFromBlob(file) loadFromBlob(file, this.state)
.then(({ elements, appState }) => .then(({ elements, appState }) =>
this.syncActionResult({ this.syncActionResult({
elements, elements,
@ -3349,11 +3387,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
private getTextWysiwygSnappedToCenterPosition( private getTextWysiwygSnappedToCenterPosition(
x: number, x: number,
y: number, y: number,
state: { appState: AppState,
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
scale: number, scale: number,
) { ) {
@ -3378,7 +3412,7 @@ class App extends React.Component<ExcalidrawProps, AppState> {
if (isSnappedToCenter) { if (isSnappedToCenter) {
const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords( const { x: viewportX, y: viewportY } = sceneCoordsToViewportCoords(
{ sceneX: elementCenterX, sceneY: elementCenterY }, { sceneX: elementCenterX, sceneY: elementCenterY },
state, appState,
canvas, canvas,
scale, scale,
); );
@ -3421,6 +3455,21 @@ class App extends React.Component<ExcalidrawProps, AppState> {
this.state, this.state,
); );
}, 300); }, 300);
private getCanvasOffsets() {
if (this.excalidrawRef?.current) {
const parentElement = this.excalidrawRef.current.parentElement;
const { left, top } = parentElement.getBoundingClientRect();
return {
offsetLeft: left,
offsetTop: top,
};
}
return {
offsetLeft: 0,
offsetTop: 0,
};
}
} }
// ----------------------------------------------------------------------------- // -----------------------------------------------------------------------------

View File

@ -2,28 +2,13 @@ import { getDefaultAppState, cleanAppStateForExport } from "../appState";
import { restore } from "./restore"; import { restore } from "./restore";
import { t } from "../i18n"; import { t } from "../i18n";
import { AppState } from "../types"; import { AppState } from "../types";
import { calculateScrollCenter } from "../scene";
export const loadFromBlob = async (blob: any) => { /**
const updateAppState = (contents: string) => { * @param blob
const defaultAppState = getDefaultAppState(); * @param appState if provided, used for centering scroll to restored scene
let elements = []; */
let appState = defaultAppState; export const loadFromBlob = async (blob: any, appState?: AppState) => {
try {
const data = JSON.parse(contents);
if (data.type !== "excalidraw") {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
elements = data.elements || [];
appState = {
...defaultAppState,
...cleanAppStateForExport(data.appState as Partial<AppState>),
};
} catch {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
return { elements, appState };
};
if (blob.handle) { if (blob.handle) {
(window as any).handle = blob.handle; (window as any).handle = blob.handle;
} }
@ -42,6 +27,23 @@ export const loadFromBlob = async (blob: any) => {
}); });
} }
const { elements, appState } = updateAppState(contents); const defaultAppState = getDefaultAppState();
return restore(elements, appState, { scrollToContent: true }); let elements = [];
let _appState = appState || defaultAppState;
try {
const data = JSON.parse(contents);
if (data.type !== "excalidraw") {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
elements = data.elements || [];
_appState = {
...defaultAppState,
...cleanAppStateForExport(data.appState as Partial<AppState>),
...(appState ? calculateScrollCenter(elements, appState, null) : {}),
};
} catch {
throw new Error(t("alerts.couldNotLoadInvalidFile"));
}
return restore(elements, _appState);
}; };

View File

@ -237,7 +237,7 @@ export const importFromBackend = async (
privateKey: string | undefined, privateKey: string | undefined,
) => { ) => {
let elements: readonly ExcalidrawElement[] = []; let elements: readonly ExcalidrawElement[] = [];
let appState: AppState = getDefaultAppState(); let appState = getDefaultAppState();
try { try {
const response = await fetch( const response = await fetch(
@ -245,7 +245,7 @@ export const importFromBackend = async (
); );
if (!response.ok) { if (!response.ok) {
window.alert(t("alerts.importBackendFailed")); window.alert(t("alerts.importBackendFailed"));
return restore(elements, appState, { scrollToContent: true }); return restore(elements, appState);
} }
let data; let data;
if (privateKey) { if (privateKey) {
@ -276,7 +276,7 @@ export const importFromBackend = async (
window.alert(t("alerts.importBackendFailed")); window.alert(t("alerts.importBackendFailed"));
console.error(error); console.error(error);
} finally { } finally {
return restore(elements, appState, { scrollToContent: true }); return restore(elements, appState);
} }
}; };

View File

@ -42,11 +42,11 @@ export const saveAsJSON = async (
); );
}; };
export const loadFromJSON = async () => { export const loadFromJSON = async (appState: AppState) => {
const blob = await fileOpen({ const blob = await fileOpen({
description: "Excalidraw files", description: "Excalidraw files",
extensions: ["json", "excalidraw"], extensions: ["json", "excalidraw"],
mimeTypes: ["application/json"], mimeTypes: ["application/json"],
}); });
return loadFromBlob(blob); return loadFromBlob(blob, appState);
}; };

View File

@ -6,7 +6,6 @@ import {
import { AppState } from "../types"; import { AppState } from "../types";
import { DataState } from "./types"; import { DataState } from "./types";
import { isInvisiblySmallElement, getNormalizedDimensions } from "../element"; import { isInvisiblySmallElement, getNormalizedDimensions } from "../element";
import { calculateScrollCenter } from "../scene";
import { randomId } from "../random"; import { randomId } from "../random";
import { import {
FONT_FAMILY, FONT_FAMILY,
@ -110,8 +109,7 @@ const migrateElement = (
export const restore = ( export const restore = (
savedElements: readonly ExcalidrawElement[], savedElements: readonly ExcalidrawElement[],
savedState: AppState | null, savedState: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null,
opts?: { scrollToContent: boolean },
): DataState => { ): DataState => {
const elements = savedElements.reduce((elements, element) => { const elements = savedElements.reduce((elements, element) => {
// filtering out selection, which is legacy, no longer kept in elements, // filtering out selection, which is legacy, no longer kept in elements,
@ -125,13 +123,6 @@ export const restore = (
return elements; return elements;
}, [] as ExcalidrawElement[]); }, [] as ExcalidrawElement[]);
if (opts?.scrollToContent && savedState) {
savedState = {
...savedState,
...calculateScrollCenter(elements, savedState, null),
};
}
return { return {
elements: elements, elements: elements,
appState: savedState, appState: savedState,

View File

@ -6,5 +6,5 @@ export interface DataState {
version?: string; version?: string;
source?: string; source?: string;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: AppState | null; appState: MarkOptional<AppState, "offsetTop" | "offsetLeft"> | null;
} }

View File

@ -4,6 +4,7 @@ import { globalSceneState } from "../scene";
import { isTextElement } from "./typeChecks"; import { isTextElement } from "./typeChecks";
import { CLASSES } from "../constants"; import { CLASSES } from "../constants";
import { ExcalidrawElement } from "./types"; import { ExcalidrawElement } from "./types";
import { AppState } from "../types";
const normalizeText = (text: string) => { const normalizeText = (text: string) => {
return ( return (
@ -19,23 +20,26 @@ const getTransform = (
width: number, width: number,
height: number, height: number,
angle: number, angle: number,
zoom: number, appState: AppState,
) => { ) => {
const { zoom, offsetTop, offsetLeft } = appState;
const degree = (180 * angle) / Math.PI; const degree = (180 * angle) / Math.PI;
return `translate(${(width * (zoom - 1)) / 2}px, ${ // offsets must be multiplied by 2 to account for the division by 2 of
(height * (zoom - 1)) / 2 // the whole expression afterwards
return `translate(${((width - offsetLeft * 2) * (zoom - 1)) / 2}px, ${
((height - offsetTop * 2) * (zoom - 1)) / 2
}px) scale(${zoom}) rotate(${degree}deg)`; }px) scale(${zoom}) rotate(${degree}deg)`;
}; };
export const textWysiwyg = ({ export const textWysiwyg = ({
id, id,
zoom, appState,
onChange, onChange,
onSubmit, onSubmit,
getViewportCoords, getViewportCoords,
}: { }: {
id: ExcalidrawElement["id"]; id: ExcalidrawElement["id"];
zoom: number; appState: AppState;
onChange?: (text: string) => void; onChange?: (text: string) => void;
onSubmit: (text: string) => void; onSubmit: (text: string) => void;
getViewportCoords: (x: number, y: number) => [number, number]; getViewportCoords: (x: number, y: number) => [number, number];
@ -66,7 +70,7 @@ export const textWysiwyg = ({
updatedElement.width, updatedElement.width,
updatedElement.height, updatedElement.height,
angle, angle,
zoom, appState,
), ),
textAlign: textAlign, textAlign: textAlign,
color: updatedElement.strokeColor, color: updatedElement.strokeColor,

View File

@ -59,7 +59,11 @@ registerFont("./public/Cascadia.ttf", { family: "Cascadia" });
const canvas = exportToCanvas( const canvas = exportToCanvas(
elements as any, elements as any,
getDefaultAppState(), {
...getDefaultAppState(),
offsetTop: 0,
offsetLeft: 0,
},
{ {
exportBackground: true, exportBackground: true,
viewBackgroundColor: "#ffffff", viewBackgroundColor: "#ffffff",

View File

@ -47,6 +47,7 @@ export const calculateScrollCenter = (
} }
const scale = window.devicePixelRatio; const scale = window.devicePixelRatio;
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, canvas, [x1, y1, x2, y2])) {
[x1, y1, x2, y2] = getClosestElementBounds( [x1, y1, x2, y2] = getClosestElementBounds(
elements, elements,

File diff suppressed because it is too large Load Diff

View File

@ -37,7 +37,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -68,7 +68,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -99,7 +99,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -130,7 +130,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -165,7 +165,7 @@ describe("add element to the scene when pointer dragging long enough", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(1); expect(h.elements.length).toEqual(1);
@ -198,7 +198,7 @@ describe("do not add element to the scene if size is too small", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@ -217,7 +217,7 @@ describe("do not add element to the scene if size is too small", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@ -236,7 +236,7 @@ describe("do not add element to the scene if size is too small", () => {
// finish (position does not matter) // finish (position does not matter)
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@ -258,7 +258,7 @@ describe("do not add element to the scene if size is too small", () => {
// we need to finalize it because arrows and lines enter multi-mode // we need to finalize it because arrows and lines enter multi-mode
fireEvent.keyDown(document, { key: KEYS.ENTER }); fireEvent.keyDown(document, { key: KEYS.ENTER });
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@ -280,7 +280,7 @@ describe("do not add element to the scene if size is too small", () => {
// we need to finalize it because arrows and lines enter multi-mode // we need to finalize it because arrows and lines enter multi-mode
fireEvent.keyDown(document, { key: KEYS.ENTER }); fireEvent.keyDown(document, { key: KEYS.ENTER });
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).toHaveBeenCalledTimes(5);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });

View File

@ -30,7 +30,7 @@ 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(4); expect(renderScene).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();
@ -65,7 +65,7 @@ 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(4); expect(renderScene).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();

View File

@ -30,7 +30,7 @@ describe("remove shape in non linear elements", () => {
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(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@ -44,7 +44,7 @@ describe("remove shape in non linear elements", () => {
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(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
@ -58,7 +58,7 @@ describe("remove shape in non linear elements", () => {
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(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.elements.length).toEqual(0); expect(h.elements.length).toEqual(0);
}); });
}); });
@ -88,7 +88,7 @@ describe("multi point mode in linear elements", () => {
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
fireEvent.keyDown(document, { key: KEYS.ENTER }); fireEvent.keyDown(document, { key: KEYS.ENTER });
expect(renderScene).toHaveBeenCalledTimes(10); expect(renderScene).toHaveBeenCalledTimes(11);
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;
@ -129,7 +129,7 @@ describe("multi point mode in linear elements", () => {
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
fireEvent.keyDown(document, { key: KEYS.ENTER }); fireEvent.keyDown(document, { key: KEYS.ENTER });
expect(renderScene).toHaveBeenCalledTimes(10); expect(renderScene).toHaveBeenCalledTimes(11);
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;

View File

@ -30,7 +30,7 @@ describe("resize element", () => {
fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 }); fireEvent.pointerMove(canvas, { clientX: 60, clientY: 70 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(4); expect(renderScene).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();
@ -73,7 +73,7 @@ describe("resize element with aspect ratio when SHIFT 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(4); expect(renderScene).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();

View File

@ -28,7 +28,7 @@ describe("selection element", () => {
const canvas = container.querySelector("canvas")!; const canvas = container.querySelector("canvas")!;
fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 }); fireEvent.pointerDown(canvas, { clientX: 60, clientY: 100 });
expect(renderScene).toHaveBeenCalledTimes(1); expect(renderScene).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");
@ -49,7 +49,7 @@ describe("selection element", () => {
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(2); expect(renderScene).toHaveBeenCalledTimes(3);
const selectionElement = h.state.selectionElement!; const selectionElement = h.state.selectionElement!;
expect(selectionElement).not.toBeNull(); expect(selectionElement).not.toBeNull();
expect(selectionElement.type).toEqual("selection"); expect(selectionElement.type).toEqual("selection");
@ -71,7 +71,7 @@ describe("selection element", () => {
fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 }); fireEvent.pointerMove(canvas, { clientX: 150, clientY: 30 });
fireEvent.pointerUp(canvas); fireEvent.pointerUp(canvas);
expect(renderScene).toHaveBeenCalledTimes(3); expect(renderScene).toHaveBeenCalledTimes(4);
expect(h.state.selectionElement).toBeNull(); expect(h.state.selectionElement).toBeNull();
}); });
}); });
@ -96,7 +96,7 @@ 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(7); expect(renderScene).toHaveBeenCalledTimes(8);
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();
@ -123,7 +123,7 @@ 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(7); expect(renderScene).toHaveBeenCalledTimes(8);
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();
@ -150,7 +150,7 @@ 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(7); expect(renderScene).toHaveBeenCalledTimes(8);
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();
@ -190,7 +190,7 @@ 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(7); expect(renderScene).toHaveBeenCalledTimes(8);
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();
@ -229,7 +229,7 @@ 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(7); expect(renderScene).toHaveBeenCalledTimes(8);
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();

View File

@ -81,6 +81,8 @@ export type AppState = {
editingGroupId: GroupId | null; editingGroupId: GroupId | null;
width: number; width: number;
height: number; height: number;
offsetTop: number;
offsetLeft: number;
isLibraryOpen: boolean; isLibraryOpen: boolean;
}; };

View File

@ -1,4 +1,4 @@
import { FlooredNumber } from "./types"; import { AppState } from "./types";
import { getZoomOrigin } from "./scene"; import { getZoomOrigin } from "./scene";
import { import {
CURSOR_TYPE, CURSOR_TYPE,
@ -185,45 +185,39 @@ export const getShortcutKey = (shortcut: string): string => {
}; };
export const viewportCoordsToSceneCoords = ( export const viewportCoordsToSceneCoords = (
{ clientX, clientY }: { clientX: number; clientY: number }, { clientX, clientY }: { clientX: number; clientY: number },
{ appState: AppState,
scrollX,
scrollY,
zoom,
}: {
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
scale: number, scale: number,
) => { ) => {
const zoomOrigin = getZoomOrigin(canvas, scale); const zoomOrigin = getZoomOrigin(canvas, scale);
const clientXWithZoom = zoomOrigin.x + (clientX - zoomOrigin.x) / zoom; const clientXWithZoom =
const clientYWithZoom = zoomOrigin.y + (clientY - zoomOrigin.y) / zoom; zoomOrigin.x +
(clientX - zoomOrigin.x - appState.offsetLeft) / appState.zoom;
const clientYWithZoom =
zoomOrigin.y +
(clientY - zoomOrigin.y - appState.offsetTop) / appState.zoom;
const x = clientXWithZoom - scrollX; const x = clientXWithZoom - appState.scrollX;
const y = clientYWithZoom - scrollY; const y = clientYWithZoom - appState.scrollY;
return { x, y }; return { x, y };
}; };
export const sceneCoordsToViewportCoords = ( export const sceneCoordsToViewportCoords = (
{ sceneX, sceneY }: { sceneX: number; sceneY: number }, { sceneX, sceneY }: { sceneX: number; sceneY: number },
{ appState: AppState,
scrollX,
scrollY,
zoom,
}: {
scrollX: FlooredNumber;
scrollY: FlooredNumber;
zoom: number;
},
canvas: HTMLCanvasElement | null, canvas: HTMLCanvasElement | null,
scale: number, scale: number,
) => { ) => {
const zoomOrigin = getZoomOrigin(canvas, scale); const zoomOrigin = getZoomOrigin(canvas, scale);
const x = zoomOrigin.x - (zoomOrigin.x - sceneX - scrollX) * zoom; const x =
const y = zoomOrigin.y - (zoomOrigin.y - sceneY - scrollY) * zoom; zoomOrigin.x -
(zoomOrigin.x - sceneX - appState.scrollX - appState.offsetLeft) *
appState.zoom;
const y =
zoomOrigin.y -
(zoomOrigin.y - sceneY - appState.scrollY - appState.offsetTop) *
appState.zoom;
return { x, y }; return { x, y };
}; };