fix: emitted visible scene bounds not accounting for offsets (#7450)

This commit is contained in:
David Luzar 2023-12-16 17:32:54 +01:00 committed by GitHub
parent 561e919a2e
commit 6dfa89e846
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 82 additions and 54 deletions

View File

@ -20,9 +20,12 @@ export const WS_EVENTS = {
} as const; } as const;
export enum WS_SUBTYPES { export enum WS_SUBTYPES {
INVALID_RESPONSE = "INVALID_RESPONSE",
INIT = "SCENE_INIT", INIT = "SCENE_INIT",
UPDATE = "SCENE_UPDATE", UPDATE = "SCENE_UPDATE",
USER_VIEWPORT_BOUNDS = "USER_VIEWPORT_BOUNDS", MOUSE_LOCATION = "MOUSE_LOCATION",
IDLE_STATUS = "IDLE_STATUS",
USER_VISIBLE_SCENE_BOUNDS = "USER_VISIBLE_SCENE_BOUNDS",
} }
export const FIREBASE_STORAGE_PREFIXES = { export const FIREBASE_STORAGE_PREFIXES = {

View File

@ -18,10 +18,10 @@ import {
} from "../../packages/excalidraw/index"; } from "../../packages/excalidraw/index";
import { Collaborator, Gesture } from "../../packages/excalidraw/types"; import { Collaborator, Gesture } from "../../packages/excalidraw/types";
import { import {
assertNever,
preventUnload, preventUnload,
resolvablePromise, resolvablePromise,
throttleRAF, throttleRAF,
viewportCoordsToSceneCoords,
withBatchedUpdates, withBatchedUpdates,
} from "../../packages/excalidraw/utils"; } from "../../packages/excalidraw/utils";
import { import {
@ -81,7 +81,8 @@ import { resetBrowserStateVersions } from "../data/tabSync";
import { LocalData } from "../data/LocalData"; import { LocalData } from "../data/LocalData";
import { atom, useAtom } from "jotai"; import { atom, useAtom } from "jotai";
import { appJotaiStore } from "../app-jotai"; import { appJotaiStore } from "../app-jotai";
import { Mutable } from "../../packages/excalidraw/utility-types"; import { Mutable, ValueOf } from "../../packages/excalidraw/utility-types";
import { getVisibleSceneBounds } from "../../packages/excalidraw/element/bounds";
export const collabAPIAtom = atom<CollabAPI | null>(null); export const collabAPIAtom = atom<CollabAPI | null>(null);
export const collabDialogShownAtom = atom(false); export const collabDialogShownAtom = atom(false);
@ -174,7 +175,7 @@ class Collab extends PureComponent<Props, CollabState> {
this.portal.socket && this.portal.broadcastUserFollowed(payload); this.portal.socket && this.portal.broadcastUserFollowed(payload);
}); });
const throttledRelayUserViewportBounds = throttleRAF( const throttledRelayUserViewportBounds = throttleRAF(
this.relayUserViewportBounds, this.relayVisibleSceneBounds,
); );
const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() => const unsubOnScrollChange = this.excalidrawAPI.onScrollChange(() =>
throttledRelayUserViewportBounds(), throttledRelayUserViewportBounds(),
@ -384,7 +385,7 @@ class Collab extends PureComponent<Props, CollabState> {
iv: Uint8Array, iv: Uint8Array,
encryptedData: ArrayBuffer, encryptedData: ArrayBuffer,
decryptionKey: string, decryptionKey: string,
) => { ): Promise<ValueOf<SocketUpdateDataSource>> => {
try { try {
const decrypted = await decryptData(iv, encryptedData, decryptionKey); const decrypted = await decryptData(iv, encryptedData, decryptionKey);
@ -396,7 +397,7 @@ class Collab extends PureComponent<Props, CollabState> {
window.alert(t("alerts.decryptFailed")); window.alert(t("alerts.decryptFailed"));
console.error(error); console.error(error);
return { return {
type: "INVALID_RESPONSE", type: WS_SUBTYPES.INVALID_RESPONSE,
}; };
} }
}; };
@ -512,7 +513,7 @@ class Collab extends PureComponent<Props, CollabState> {
); );
switch (decryptedData.type) { switch (decryptedData.type) {
case "INVALID_RESPONSE": case WS_SUBTYPES.INVALID_RESPONSE:
return; return;
case WS_SUBTYPES.INIT: { case WS_SUBTYPES.INIT: {
if (!this.portal.socketInitialized) { if (!this.portal.socketInitialized) {
@ -535,7 +536,7 @@ class Collab extends PureComponent<Props, CollabState> {
this.reconcileElements(decryptedData.payload.elements), this.reconcileElements(decryptedData.payload.elements),
); );
break; break;
case "MOUSE_LOCATION": { case WS_SUBTYPES.MOUSE_LOCATION: {
const { pointer, button, username, selectedElementIds } = const { pointer, button, username, selectedElementIds } =
decryptedData.payload; decryptedData.payload;
@ -554,8 +555,8 @@ class Collab extends PureComponent<Props, CollabState> {
break; break;
} }
case WS_SUBTYPES.USER_VIEWPORT_BOUNDS: { case WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS: {
const { bounds, socketId } = decryptedData.payload; const { sceneBounds, socketId } = decryptedData.payload;
const appState = this.excalidrawAPI.getAppState(); const appState = this.excalidrawAPI.getAppState();
@ -579,7 +580,7 @@ class Collab extends PureComponent<Props, CollabState> {
this.excalidrawAPI.updateScene({ this.excalidrawAPI.updateScene({
appState: zoomToFitBounds({ appState: zoomToFitBounds({
appState, appState,
bounds, bounds: sceneBounds,
fitToViewport: true, fitToViewport: true,
viewportZoomFactor: 1, viewportZoomFactor: 1,
}).appState, }).appState,
@ -588,7 +589,7 @@ class Collab extends PureComponent<Props, CollabState> {
break; break;
} }
case "IDLE_STATUS": { case WS_SUBTYPES.IDLE_STATUS: {
const { userState, socketId, username } = decryptedData.payload; const { userState, socketId, username } = decryptedData.payload;
this.updateCollaborator(socketId, { this.updateCollaborator(socketId, {
userState, userState,
@ -596,6 +597,10 @@ class Collab extends PureComponent<Props, CollabState> {
}); });
break; break;
} }
default: {
assertNever(decryptedData, null);
}
} }
}, },
); );
@ -618,7 +623,7 @@ class Collab extends PureComponent<Props, CollabState> {
appState: { followedBy: new Set(followedBy) }, appState: { followedBy: new Set(followedBy) },
}); });
this.relayUserViewportBounds({ shouldPerform: true }); this.relayVisibleSceneBounds({ force: true });
}, },
); );
@ -848,25 +853,14 @@ class Collab extends PureComponent<Props, CollabState> {
CURSOR_SYNC_TIMEOUT, CURSOR_SYNC_TIMEOUT,
); );
relayUserViewportBounds = (props?: { shouldPerform: boolean }) => { relayVisibleSceneBounds = (props?: { force: boolean }) => {
const appState = this.excalidrawAPI.getAppState(); const appState = this.excalidrawAPI.getAppState();
if ( if (this.portal.socket && (appState.followedBy.size > 0 || props?.force)) {
this.portal.socket && this.portal.broadcastVisibleSceneBounds(
(appState.followedBy.size > 0 || props?.shouldPerform) {
) { sceneBounds: getVisibleSceneBounds(appState),
const { x: x1, y: y1 } = viewportCoordsToSceneCoords( },
{ clientX: 0, clientY: 0 },
appState,
);
const { x: x2, y: y2 } = viewportCoordsToSceneCoords(
{ clientX: appState.width, clientY: appState.height },
appState,
);
this.portal.broadcastUserViewportBounds(
{ bounds: [x1, y1, x2, y2] },
`follow@${this.portal.socket.id}`, `follow@${this.portal.socket.id}`,
); );
} }

View File

@ -184,7 +184,7 @@ class Portal {
broadcastIdleChange = (userState: UserIdleState) => { broadcastIdleChange = (userState: UserIdleState) => {
if (this.socket?.id) { if (this.socket?.id) {
const data: SocketUpdateDataSource["IDLE_STATUS"] = { const data: SocketUpdateDataSource["IDLE_STATUS"] = {
type: "IDLE_STATUS", type: WS_SUBTYPES.IDLE_STATUS,
payload: { payload: {
socketId: this.socket.id, socketId: this.socket.id,
userState, userState,
@ -204,7 +204,7 @@ class Portal {
}) => { }) => {
if (this.socket?.id) { if (this.socket?.id) {
const data: SocketUpdateDataSource["MOUSE_LOCATION"] = { const data: SocketUpdateDataSource["MOUSE_LOCATION"] = {
type: "MOUSE_LOCATION", type: WS_SUBTYPES.MOUSE_LOCATION,
payload: { payload: {
socketId: this.socket.id, socketId: this.socket.id,
pointer: payload.pointer, pointer: payload.pointer,
@ -222,19 +222,19 @@ class Portal {
} }
}; };
broadcastUserViewportBounds = ( broadcastVisibleSceneBounds = (
payload: { payload: {
bounds: [number, number, number, number]; sceneBounds: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"]["payload"]["sceneBounds"];
}, },
roomId: string, roomId: string,
) => { ) => {
if (this.socket?.id) { if (this.socket?.id) {
const data: SocketUpdateDataSource["USER_VIEWPORT_BOUNDS"] = { const data: SocketUpdateDataSource["USER_VISIBLE_SCENE_BOUNDS"] = {
type: WS_SUBTYPES.USER_VIEWPORT_BOUNDS, type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS,
payload: { payload: {
socketId: this.socket.id, socketId: this.socket.id,
username: this.collab.state.username, username: this.collab.state.username,
bounds: payload.bounds, sceneBounds: payload.sceneBounds,
}, },
}; };

View File

@ -10,6 +10,7 @@ import {
import { serializeAsJSON } from "../../packages/excalidraw/data/json"; import { serializeAsJSON } from "../../packages/excalidraw/data/json";
import { restore } from "../../packages/excalidraw/data/restore"; import { restore } from "../../packages/excalidraw/data/restore";
import { ImportedDataState } from "../../packages/excalidraw/data/types"; import { ImportedDataState } from "../../packages/excalidraw/data/types";
import { SceneBounds } from "../../packages/excalidraw/element/bounds";
import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers"; import { isInvisiblySmallElement } from "../../packages/excalidraw/element/sizeHelpers";
import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks"; import { isInitializedImageElement } from "../../packages/excalidraw/element/typeChecks";
import { import {
@ -28,6 +29,7 @@ import {
DELETED_ELEMENT_TIMEOUT, DELETED_ELEMENT_TIMEOUT,
FILE_UPLOAD_MAX_BYTES, FILE_UPLOAD_MAX_BYTES,
ROOM_ID_BYTES, ROOM_ID_BYTES,
WS_SUBTYPES,
} from "../app_constants"; } from "../app_constants";
import { encodeFilesForUpload } from "./FileManager"; import { encodeFilesForUpload } from "./FileManager";
import { saveFilesToFirebase } from "./firebase"; import { saveFilesToFirebase } from "./firebase";
@ -97,20 +99,23 @@ export type EncryptedData = {
}; };
export type SocketUpdateDataSource = { export type SocketUpdateDataSource = {
INVALID_RESPONSE: {
type: WS_SUBTYPES.INVALID_RESPONSE;
};
SCENE_INIT: { SCENE_INIT: {
type: "SCENE_INIT"; type: WS_SUBTYPES.INIT;
payload: { payload: {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
}; };
}; };
SCENE_UPDATE: { SCENE_UPDATE: {
type: "SCENE_UPDATE"; type: WS_SUBTYPES.UPDATE;
payload: { payload: {
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
}; };
}; };
MOUSE_LOCATION: { MOUSE_LOCATION: {
type: "MOUSE_LOCATION"; type: WS_SUBTYPES.MOUSE_LOCATION;
payload: { payload: {
socketId: string; socketId: string;
pointer: { x: number; y: number; tool: "pointer" | "laser" }; pointer: { x: number; y: number; tool: "pointer" | "laser" };
@ -119,16 +124,16 @@ export type SocketUpdateDataSource = {
username: string; username: string;
}; };
}; };
USER_VIEWPORT_BOUNDS: { USER_VISIBLE_SCENE_BOUNDS: {
type: "USER_VIEWPORT_BOUNDS"; type: WS_SUBTYPES.USER_VISIBLE_SCENE_BOUNDS;
payload: { payload: {
socketId: string; socketId: string;
username: string; username: string;
bounds: [number, number, number, number]; sceneBounds: SceneBounds;
}; };
}; };
IDLE_STATUS: { IDLE_STATUS: {
type: "IDLE_STATUS"; type: WS_SUBTYPES.IDLE_STATUS;
payload: { payload: {
socketId: string; socketId: string;
userState: UserIdleState; userState: UserIdleState;
@ -138,10 +143,7 @@ export type SocketUpdateDataSource = {
}; };
export type SocketUpdateDataIncoming = export type SocketUpdateDataIncoming =
| SocketUpdateDataSource[keyof SocketUpdateDataSource] SocketUpdateDataSource[keyof SocketUpdateDataSource];
| {
type: "INVALID_RESPONSE";
};
export type SocketUpdateData = export type SocketUpdateData =
SocketUpdateDataSource[keyof SocketUpdateDataSource] & { SocketUpdateDataSource[keyof SocketUpdateDataSource] & {

View File

@ -13,6 +13,8 @@ Please add the latest change on the top under the correct section.
## Unreleased ## Unreleased
- Expose `getVisibleSceneBounds` helper to get scene bounds of visible canvas area. [#7450](https://github.com/excalidraw/excalidraw/pull/7450)
### Breaking Changes ### Breaking Changes
- `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336) - `appState.openDialog` type was changed from `null | string` to `null | { name: string }`. [#7336](https://github.com/excalidraw/excalidraw/pull/7336)

View File

@ -20,7 +20,7 @@ import {
isHandToolActive, isHandToolActive,
} from "../appState"; } from "../appState";
import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors"; import { DEFAULT_CANVAS_BACKGROUND_PICKS } from "../colors";
import { Bounds } from "../element/bounds"; import { SceneBounds } from "../element/bounds";
import { setCursor } from "../cursor"; import { setCursor } from "../cursor";
export const actionChangeViewBackgroundColor = register({ export const actionChangeViewBackgroundColor = register({
@ -211,7 +211,7 @@ export const actionResetZoom = register({
}); });
const zoomValueToFitBoundsOnViewport = ( const zoomValueToFitBoundsOnViewport = (
bounds: Bounds, bounds: SceneBounds,
viewportDimensions: { width: number; height: number }, viewportDimensions: { width: number; height: number },
) => { ) => {
const [x1, y1, x2, y2] = bounds; const [x1, y1, x2, y2] = bounds;
@ -235,7 +235,7 @@ export const zoomToFitBounds = ({
fitToViewport = false, fitToViewport = false,
viewportZoomFactor = 0.7, viewportZoomFactor = 0.7,
}: { }: {
bounds: readonly [number, number, number, number]; bounds: SceneBounds;
appState: Readonly<AppState>; appState: Readonly<AppState>;
/** whether to fit content to viewport (beyond >100%) */ /** whether to fit content to viewport (beyond >100%) */
fitToViewport: boolean; fitToViewport: boolean;

View File

@ -9,7 +9,7 @@ import {
import { distance2d, rotate, rotatePoint } from "../math"; import { distance2d, rotate, rotatePoint } from "../math";
import rough from "roughjs/bin/rough"; import rough from "roughjs/bin/rough";
import { Drawable, Op } from "roughjs/bin/core"; import { Drawable, Op } from "roughjs/bin/core";
import { Point } from "../types"; import { AppState, Point } from "../types";
import { generateRoughOptions } from "../scene/Shape"; import { generateRoughOptions } from "../scene/Shape";
import { import {
isArrowElement, isArrowElement,
@ -35,7 +35,9 @@ export type RectangleBox = {
type MaybeQuadraticSolution = [number | null, number | null] | false; type MaybeQuadraticSolution = [number | null, number | null] | false;
// x and y position of top left corner, x and y position of bottom right corner /**
* x and y position of top left corner, x and y position of bottom right corner
*/
export type Bounds = readonly [ export type Bounds = readonly [
minX: number, minX: number,
minY: number, minY: number,
@ -43,6 +45,13 @@ export type Bounds = readonly [
maxY: number, maxY: number,
]; ];
export type SceneBounds = readonly [
sceneX: number,
sceneY: number,
sceneX2: number,
sceneY2: number,
];
export class ElementBounds { export class ElementBounds {
private static boundsCache = new WeakMap< private static boundsCache = new WeakMap<
ExcalidrawElement, ExcalidrawElement,
@ -879,3 +888,21 @@ export const getCommonBoundingBox = (
midY: (minY + maxY) / 2, midY: (minY + maxY) / 2,
}; };
}; };
/**
* returns scene coords of user's editor viewport (visible canvas area) bounds
*/
export const getVisibleSceneBounds = ({
scrollX,
scrollY,
width,
height,
zoom,
}: AppState): SceneBounds => {
return [
-scrollX,
-scrollY,
-scrollX + width / zoom.value,
-scrollY + height / zoom.value,
];
};

View File

@ -249,7 +249,7 @@ export { TTDDialogTrigger } from "./components/TTDDialog/TTDDialogTrigger";
export { normalizeLink } from "./data/url"; export { normalizeLink } from "./data/url";
export { zoomToFitBounds } from "./actions/actionCanvas"; export { zoomToFitBounds } from "./actions/actionCanvas";
export { convertToExcalidrawElements } from "./data/transform"; export { convertToExcalidrawElements } from "./data/transform";
export { getCommonBounds } from "./element/bounds"; export { getCommonBounds, getVisibleSceneBounds } from "./element/bounds";
export { export {
elementsOverlappingBBox, elementsOverlappingBBox,