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:
parent
63edbb9517
commit
7eff6893c5
@ -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 });
|
||||||
})
|
})
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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">(
|
||||||
|
@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
|
@ -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
@ -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);
|
||||||
});
|
});
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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();
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
44
src/utils.ts
44
src/utils.ts
@ -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 };
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user