diff --git a/src/actions/actionCanvas.tsx b/src/actions/actionCanvas.tsx index bc268544b..8dc72f015 100644 --- a/src/actions/actionCanvas.tsx +++ b/src/actions/actionCanvas.tsx @@ -225,22 +225,20 @@ const zoomValueToFitBoundsOnViewport = ( return clampedZoomValueToFitElements as NormalizedZoomValue; }; -export const zoomToFit = ({ - targetElements, +export const zoomToFitBounds = ({ + bounds, appState, fitToViewport = false, viewportZoomFactor = 0.7, }: { - targetElements: readonly ExcalidrawElement[]; + bounds: readonly [number, number, number, number]; appState: Readonly; /** whether to fit content to viewport (beyond >100%) */ fitToViewport: boolean; /** zoom content to cover X of the viewport, when fitToViewport=true */ viewportZoomFactor?: number; }) => { - const commonBounds = getCommonBounds(getNonDeletedElements(targetElements)); - - const [x1, y1, x2, y2] = commonBounds; + const [x1, y1, x2, y2] = bounds; const centerX = (x1 + x2) / 2; const centerY = (y1 + y2) / 2; @@ -267,7 +265,7 @@ export const zoomToFit = ({ scrollX = (appState.width / 2) * (1 / newZoomValue) - centerX; scrollY = (appState.height / 2) * (1 / newZoomValue) - centerY; } else { - newZoomValue = zoomValueToFitBoundsOnViewport(commonBounds, { + newZoomValue = zoomValueToFitBoundsOnViewport(bounds, { width: appState.width, height: appState.height, }); @@ -296,6 +294,29 @@ export const zoomToFit = ({ }; }; +export const zoomToFit = ({ + targetElements, + appState, + fitToViewport, + viewportZoomFactor, +}: { + targetElements: readonly ExcalidrawElement[]; + appState: Readonly; + /** whether to fit content to viewport (beyond >100%) */ + fitToViewport: boolean; + /** zoom content to cover X of the viewport, when fitToViewport=true */ + viewportZoomFactor?: number; +}) => { + const commonBounds = getCommonBounds(getNonDeletedElements(targetElements)); + + return zoomToFitBounds({ + bounds: commonBounds, + appState, + fitToViewport, + viewportZoomFactor, + }); +}; + // Note, this action differs from actionZoomToFitSelection in that it doesn't // zoom beyond 100%. In other words, if the content is smaller than viewport // size, it won't be zoomed in. diff --git a/src/components/App.tsx b/src/components/App.tsx index e8e2b8a5b..67425f753 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1500,18 +1500,21 @@ class App extends React.Component { this.refreshDeviceState(this.excalidrawContainerRef.current); } - // TODO follow-participant - // add zoom change - if (prevState.zoom.value !== this.state.zoom.value) { - this.props?.onZoomChange?.(this.state.zoom); + this.props?.onScrollAndZoomChange?.({ + zoom: this.state.zoom, + scroll: { x: this.state.scrollX, y: this.state.scrollY }, + }); } if ( prevState.scrollX !== this.state.scrollX || prevState.scrollY !== this.state.scrollY ) { - this.props?.onScrollChange?.(this.state.scrollX, this.state.scrollY); + this.props?.onScrollAndZoomChange?.({ + zoom: this.state.zoom, + scroll: { x: this.state.scrollX, y: this.state.scrollY }, + }); } if ( diff --git a/src/excalidraw-app/collab/Collab.tsx b/src/excalidraw-app/collab/Collab.tsx index 2a1554f14..99e0d4d04 100644 --- a/src/excalidraw-app/collab/Collab.tsx +++ b/src/excalidraw-app/collab/Collab.tsx @@ -1,6 +1,6 @@ import throttle from "lodash.throttle"; import { PureComponent } from "react"; -import { ExcalidrawImperativeAPI } from "../../types"; +import { AppState, ExcalidrawImperativeAPI } from "../../types"; import { ErrorDialog } from "../../components/ErrorDialog"; import { APP_NAME, ENV, EVENT } from "../../constants"; import { ImportedDataState } from "../../data/types"; @@ -16,6 +16,7 @@ import { Collaborator, Gesture } from "../../types"; import { preventUnload, resolvablePromise, + viewportCoordsToSceneCoords, withBatchedUpdates, } from "../../utils"; import { @@ -71,6 +72,7 @@ import { resetBrowserStateVersions } from "../data/tabSync"; import { LocalData } from "../data/LocalData"; import { atom, useAtom } from "jotai"; import { appJotaiStore } from "../app-jotai"; +import { zoomToFitBounds } from "../../actions/actionCanvas"; export const collabAPIAtom = atom(null); export const collabDialogShownAtom = atom(false); @@ -89,8 +91,7 @@ export interface CollabAPI { /** function so that we can access the latest value from stale callbacks */ isCollaborating: () => boolean; onPointerUpdate: CollabInstance["onPointerUpdate"]; - onScrollChange: CollabInstance["onScrollChange"]; - onZoomChange: CollabInstance["onZoomChange"]; + onScrollAndZoomChange: CollabInstance["onScrollAndZoomChange"]; startCollaboration: CollabInstance["startCollaboration"]; stopCollaboration: CollabInstance["stopCollaboration"]; syncElements: CollabInstance["syncElements"]; @@ -164,8 +165,7 @@ class Collab extends PureComponent { const collabAPI: CollabAPI = { isCollaborating: this.isCollaborating, onPointerUpdate: this.onPointerUpdate, - onScrollChange: this.onScrollChange, - onZoomChange: this.onZoomChange, + onScrollAndZoomChange: this.onScrollAndZoomChange, startCollaboration: this.startCollaboration, syncElements: this.syncElements, fetchImageFilesFromFirebase: this.fetchImageFilesFromFirebase, @@ -510,8 +510,6 @@ class Collab extends PureComponent { break; } case WS_SCENE_EVENT_TYPES.UPDATE: - console.log("received update", decryptedData); - console.log(this.excalidrawAPI.getAppState()); this.handleRemoteSceneUpdate( this.reconcileElements(decryptedData.payload.elements), ); @@ -520,8 +518,6 @@ class Collab extends PureComponent { const { pointer, button, username, selectedElementIds } = decryptedData.payload; - // console.log({ decryptedData }); - const socketId: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["socketId"] = decryptedData.payload.socketId || // @ts-ignore legacy, see #2094 (#2097) @@ -539,50 +535,22 @@ class Collab extends PureComponent { }); break; } - // TODO follow-participant - // case "SCROLL_LOCATION" - // case "ZOOM_VALUE" - // if following someone, update scroll and zoom - case "SCROLL_LOCATION": { - const { - scroll: { x, y }, - } = decryptedData.payload; + case "SCROLL_AND_ZOOM": { + const { bounds } = decryptedData.payload; - const socketId: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["socketId"] = + const socketId: SocketUpdateDataSource["SCROLL_AND_ZOOM"]["payload"]["socketId"] = decryptedData.payload.socketId; - console.log({ decryptedData }); - - const appState = this.excalidrawAPI.getAppState(); - console.log({ appState }); - - if (appState.userToFollow === socketId) { - this.excalidrawAPI.updateScene({ - appState: { - scrollX: x, - scrollY: y, - }, - }); - } - - break; - } - - case "ZOOM_VALUE": { - const { zoom } = decryptedData.payload; - - console.log({ decryptedData }); - - const socketId: SocketUpdateDataSource["ZOOM_VALUE"]["payload"]["socketId"] = - decryptedData.payload.socketId; - - const appState = this.excalidrawAPI.getAppState(); - - if (appState.userToFollow === socketId) { - this.excalidrawAPI.updateScene({ - appState: { zoom }, + const _appState = this.excalidrawAPI.getAppState(); + if (_appState.userToFollow === socketId) { + const { appState } = zoomToFitBounds({ + appState: _appState, + bounds, + fitToViewport: true, + viewportZoomFactor: 1, }); + this.excalidrawAPI.updateScene({ appState }); } break; @@ -814,7 +782,6 @@ class Collab extends PureComponent { button: SocketUpdateDataSource["MOUSE_LOCATION"]["payload"]["button"]; pointersMap: Gesture["pointers"]; }) => { - // console.log({ payload }); payload.pointersMap.size < 2 && this.portal.socket && this.portal.broadcastMouseLocation(payload); @@ -822,26 +789,34 @@ class Collab extends PureComponent { CURSOR_SYNC_TIMEOUT, ); - // TODO follow-participant - // - onScrollChange - // -- broadCastScrollLocation - // - onZoomChange - // -- broadCastZoomValue + onScrollAndZoomChange = throttle( + (payload: { zoom: AppState["zoom"]; scroll: { x: number; y: number } }) => { + const appState = this.excalidrawAPI.getAppState(); - onZoomChange = throttle( - (payload: { - zoom: SocketUpdateDataSource["ZOOM_VALUE"]["payload"]["zoom"]; - }) => { - this.portal.socket && this.portal.broadcastZoomValue(payload); - }, - ); + const { x: x1, y: y1 } = viewportCoordsToSceneCoords( + { clientX: 0, clientY: 0 }, + { + offsetLeft: appState.offsetLeft, + offsetTop: appState.offsetTop, + scrollX: payload.scroll.x, + scrollY: payload.scroll.y, + zoom: payload.zoom, + }, + ); - onScrollChange = throttle( - (payload: { - scrollX: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["x"]; - scrollY: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["y"]; - }) => { - this.portal.socket && this.portal.broadcastScrollLocation(payload); + const { x: x2, y: y2 } = viewportCoordsToSceneCoords( + { clientX: appState.width, clientY: appState.height }, + { + offsetLeft: appState.offsetLeft, + offsetTop: appState.offsetTop, + scrollX: payload.scroll.x, + scrollY: payload.scroll.y, + zoom: payload.zoom, + }, + ); + + this.portal.socket && + this.portal.broadcastScrollAndZoom({ bounds: [x1, y1, x2, y2] }); }, ); diff --git a/src/excalidraw-app/collab/Portal.tsx b/src/excalidraw-app/collab/Portal.tsx index 5c5ffbcf6..03e975273 100644 --- a/src/excalidraw-app/collab/Portal.tsx +++ b/src/excalidraw-app/collab/Portal.tsx @@ -214,8 +214,6 @@ class Portal { }, }; - // console.log("broadcastMouseLocation data", data); - return this._broadcastSocketData( data as SocketUpdateData, true, // volatile @@ -223,48 +221,19 @@ class Portal { } }; - // TODO follow-participant - // - broadCastScrollLocation - // - broadCastZoomValue - - broadcastScrollLocation = (payload: { - scrollX: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["x"]; - scrollY: SocketUpdateDataSource["SCROLL_LOCATION"]["payload"]["scroll"]["y"]; + broadcastScrollAndZoom = (payload: { + bounds: [number, number, number, number]; }) => { if (this.socket?.id) { - const data: SocketUpdateDataSource["SCROLL_LOCATION"] = { - type: "SCROLL_LOCATION", + const data: SocketUpdateDataSource["SCROLL_AND_ZOOM"] = { + type: "SCROLL_AND_ZOOM", payload: { socketId: this.socket.id, - scroll: { x: payload.scrollX, y: payload.scrollY }, username: this.collab.state.username, + bounds: payload.bounds, }, }; - console.log("broadcastScrollLocation data", data); - - return this._broadcastSocketData( - data as SocketUpdateData, - true, // volatile - ); - } - }; - - broadcastZoomValue = (payload: { - zoom: SocketUpdateDataSource["ZOOM_VALUE"]["payload"]["zoom"]; - }) => { - if (this.socket?.id) { - const data: SocketUpdateDataSource["ZOOM_VALUE"] = { - type: "ZOOM_VALUE", - payload: { - socketId: this.socket.id, - zoom: payload.zoom, - username: this.collab.state.username, - }, - }; - - console.log("broadcastZoomValue data", data); - return this._broadcastSocketData( data as SocketUpdateData, true, // volatile diff --git a/src/excalidraw-app/data/index.ts b/src/excalidraw-app/data/index.ts index d036a6696..dde2c8847 100644 --- a/src/excalidraw-app/data/index.ts +++ b/src/excalidraw-app/data/index.ts @@ -113,20 +113,12 @@ export type SocketUpdateDataSource = { username: string; }; }; - SCROLL_LOCATION: { - type: "SCROLL_LOCATION"; + SCROLL_AND_ZOOM: { + type: "SCROLL_AND_ZOOM"; payload: { socketId: string; - scroll: { x: number; y: number }; - username: string; - }; - }; - ZOOM_VALUE: { - type: "ZOOM_VALUE"; - payload: { - socketId: string; - zoom: AppState["zoom"]; username: string; + bounds: [number, number, number, number]; }; }; IDLE_STATUS: { diff --git a/src/excalidraw-app/index.tsx b/src/excalidraw-app/index.tsx index c1304996f..16464536e 100644 --- a/src/excalidraw-app/index.tsx +++ b/src/excalidraw-app/index.tsx @@ -649,16 +649,8 @@ const ExcalidrawWrapper = () => { })} > { - console.log({ zoom }); - collabAPI?.onZoomChange({ zoom }); - }} - onScrollChange={(x, y) => { - // console.log({ x, y }); - - collabAPI?.onScrollChange({ scrollX: x, scrollY: y }); + onScrollAndZoomChange={({ zoom, scroll }) => { + collabAPI?.onScrollAndZoomChange({ zoom, scroll }); }} ref={excalidrawRefCallback} onChange={onChange} diff --git a/src/packages/excalidraw/index.tsx b/src/packages/excalidraw/index.tsx index 034cfd393..d6c3e92a5 100644 --- a/src/packages/excalidraw/index.tsx +++ b/src/packages/excalidraw/index.tsx @@ -41,7 +41,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { onLinkOpen, onPointerDown, onScrollChange, - onZoomChange, + onScrollAndZoomChange, children, } = props; @@ -116,7 +116,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => { onLinkOpen={onLinkOpen} onPointerDown={onPointerDown} onScrollChange={onScrollChange} - onZoomChange={onZoomChange} + onScrollAndZoomChange={onScrollAndZoomChange} > {children} diff --git a/src/types.ts b/src/types.ts index a6439de71..a41b7cbce 100644 --- a/src/types.ts +++ b/src/types.ts @@ -362,6 +362,13 @@ export interface ExcalidrawProps { ) => void; onScrollChange?: (scrollX: number, scrollY: number) => void; onZoomChange?: (zoom: Zoom) => void; + onScrollAndZoomChange?: ({ + zoom, + scroll, + }: { + zoom: Zoom; + scroll: { x: number; y: number }; + }) => void; children?: React.ReactNode; }