diff --git a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx index 61911c883c..df021db87f 100644 --- a/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx +++ b/bigbluebutton-html5/imports/ui/components/whiteboard/component.jsx @@ -1,386 +1,388 @@ -import * as React from "react"; -import _ from "lodash"; -import Cursors from "./cursors/container"; -import { TldrawApp, Tldraw } from "@tldraw/tldraw"; -import logger from '/imports/startup/client/logger'; -import { - ColorStyle, - DashStyle, - SizeStyle, - TDDocument, - TDShapeType, -} from "@tldraw/tldraw"; -import { Renderer, Utils } from "@tldraw/core"; - -function usePrevious(value) { - const ref = React.useRef(); - React.useEffect(() => { - ref.current = value; - }, [value]); - return ref.current; -} - -const findRemoved = (A, B) => { - return A.filter((a) => { - return !B.includes(a); - }); -}; - -export default function Whiteboard(props) { - const { - isPresenter, - removeShapes, - initDefaultPages, - meetingId, - persistShape, - persistAsset, - shapes, - assets, - currentUser, - curPres, - whiteboardId, - podId, - zoomSlide, - skipToSlide, - slidePosition, - curPageId, - svgUri, - presentationBounds, - isViewersCursorLocked, - } = props; - - const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1); - const rDocument = React.useRef({ - name: "test", - version: TldrawApp.version, - id: whiteboardId, - pages, - pageStates, - bindings: {}, - assets, - }); - //const [doc, setDoc] = React.useState(rDocument.current); - const [_assets, setAssets] = React.useState(assets); - const [command, setCommand] = React.useState(""); - const [wbAccess, setWBAccess] = React.useState(props?.hasMultiUserAccess(props.whiteboardId, props.currentUser.userId)); - const [selectedIds, setSelectedIds] = React.useState([]); - const [tldrawAPI, setTLDrawAPI] = React.useState(null); - const [cameraFitSlide, setCameraFitSlide] = React.useState({point: [0, 0], zoom: 0}); - const [zoomedIn, setZoomedIn] = React.useState(false); - const prevShapes = usePrevious(shapes); - const prevSlidePosition = usePrevious(slidePosition); - const prevPageId = usePrevious(curPageId); - - const calculateCameraFitSlide = () => { - let zoom = - Math.min( - (presentationBounds.width) / slidePosition.width, - (presentationBounds.height) / slidePosition.height - ); - - zoom = Utils.clamp(zoom, 0.1, 5); - - let point = [0, 0]; - if ((presentationBounds.width / presentationBounds.height) > - (slidePosition.width / slidePosition.height)) - { - point[0] = (presentationBounds.width - (slidePosition.width * zoom)) / 2 / zoom - } else { - point[1] = (presentationBounds.height - (slidePosition.height * zoom)) / 2 / zoom - } - - return {point, zoom} - } - - const doc = React.useMemo(() => { - const currentDoc = rDocument.current; - - let next = { ...currentDoc }; - - let pageBindings = null; - let history = null; - let stack = null; - let changed = false; - - if (next.pageStates[curPageId] && !_.isEqual(prevShapes, shapes)) { - // mergeDocument loses bindings and history, save it - pageBindings = tldrawAPI?.getPage(curPageId)?.bindings; - history = tldrawAPI?.history - stack = tldrawAPI?.stack - - next.pages[curPageId].shapes = shapes; - - changed = true; - } - - if (next.pages[curPageId] && !next.pages[curPageId].shapes["slide-background-shape"]) { - next.assets[`slide-background-asset-${curPageId}`] = { - id: `slide-background-asset-${curPageId}`, - size: [slidePosition?.width || 0, slidePosition?.height || 0], - src: svgUri, - type: "image", - }; - - next.pages[curPageId].shapes["slide-background-shape"] = { - assetId: `slide-background-asset-${curPageId}`, - childIndex: 0.5, - id: "slide-background-shape", - name: "Image", - type: TDShapeType.Image, - parentId: `${curPageId}`, - point: [0, 0], - isLocked: true, - size: [slidePosition?.width || 0, slidePosition?.height || 0], - style: { - dash: DashStyle.Draw, - size: SizeStyle.Medium, - color: ColorStyle.Blue, - }, - }; - - changed = true; - } - - if (changed) { - if (pageBindings) next.pages[curPageId].bindings = pageBindings; - tldrawAPI?.mergeDocument(next); - if (tldrawAPI && history) tldrawAPI.history = history; - if (tldrawAPI && stack) tldrawAPI.stack = stack; - } - - // move poll result text to bottom right - if (next.pages[curPageId]) { - const pollResults = Object.entries(next.pages[curPageId].shapes) - .filter(([id, shape]) => shape.name.includes("poll-result")) - for (const [id, shape] of pollResults) { - if (_.isEqual(shape.point, [0, 0])) { - const shapeBounds = tldrawAPI?.getShapeBounds(id); - if (shapeBounds) { - shape.point = [ - slidePosition.width - shapeBounds.width, - slidePosition.height - shapeBounds.height - ] - shape.size = [shapeBounds.width, shapeBounds.height] - isPresenter && persistShape(shape, whiteboardId); - } - } - }; - } - - return currentDoc; - }, [assets, shapes, tldrawAPI, curPageId, slidePosition]); - - // when presentationBounds change, update tldraw camera - // to fit slide on center if zoomed out - React.useEffect(() => { - if (curPageId && slidePosition) { - const camera = calculateCameraFitSlide(); - setCameraFitSlide(camera); - if (!zoomedIn) { - tldrawAPI?.setCamera(camera.point, camera.zoom); - } - } - }, [presentationBounds, curPageId]); - - // change tldraw page when presentation page changes - React.useEffect(() => { - if (tldrawAPI && curPageId) { - const previousPageZoom = tldrawAPI.getPageState()?.camera?.zoom; - tldrawAPI.changePage(curPageId); - //change zoom of the new page to follow the previous one - if (!zoomedIn && cameraFitSlide.zoom !== 0) { - tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom, "zoomed"); - } else { - previousPageZoom && - slidePosition && - tldrawAPI.setCamera([slidePosition.xCamera, slidePosition.yCamera], previousPageZoom, "zoomed"); - } - } - }, [curPageId]); - - // change tldraw camera when slidePosition changes - React.useEffect(() => { - if (tldrawAPI && !isPresenter && curPageId && slidePosition) { - if (slidePosition.zoom === 0 && slidePosition.xCamera === 0 && slidePosition.yCamera === 0) { - tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); - setZoomedIn(false); - } else { - tldrawAPI?.setCamera([slidePosition.xCamera, slidePosition.yCamera], slidePosition.zoom); - setZoomedIn(true); - } - } - }, [curPageId, slidePosition]); - - const hasWBAccess = props?.hasMultiUserAccess(props.whiteboardId, props.currentUser.userId); - - return ( - <> - - { - if (!hasWBAccess && !isPresenter) app.onPan = () => {}; - setTLDrawAPI(app); - props.setTldrawAPI(app); - if (curPageId) { - app.changePage(curPageId); - if (slidePosition.zoom === 0) { - // first load, center the view to fit slide - const cameraFitSlide = calculateCameraFitSlide(); - app.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); - setCameraFitSlide(cameraFitSlide); - } else { - app.setCamera([slidePosition.xCamera, slidePosition.yCamera], slidePosition.zoom) - setZoomedIn(true); - } - } - }} - showPages={false} - showZoom={false} - showUI={curPres ? (isPresenter || hasWBAccess) : true} - showMenu={curPres ? false : true} - showMultiplayerMenu={false} - readOnly={!isPresenter && !hasWBAccess} - onUndo={(e, s) => { - e?.selectedIds?.map(id => { - persistShape(e.getShape(id), whiteboardId); - }) - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - removeShapes(shapesIdsToRemove, whiteboardId) - }} - - onRedo={(e, s) => { - e?.selectedIds?.map(id => { - persistShape(e.getShape(id), whiteboardId); - }); - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - removeShapes(shapesIdsToRemove, whiteboardId) - }} - - onChangePage={(app, s, b, a) => { - if (app.getPage()?.id !== curPageId) { - skipToSlide(Number.parseInt(app.getPage()?.id), podId) - } - }} - onCommand={(e, s, g) => { - if (s.includes("session:complete:DrawSession")) { - Object.entries(e.state.document.pages[e.getPage()?.id]?.shapes) - .filter(([k, s]) => s?.type === 'draw') - .forEach(([k, s]) => { - if (!e.prevShapes[k] && !k.includes('slide-background')) { - const shapeBounds = e.getShapeBounds(k); - s.size = [shapeBounds.width, shapeBounds.height]; - persistShape(s, whiteboardId); - } - }); - } - - if (s.includes("style") - || s?.includes("session:complete:ArrowSession")) { - e.selectedIds.forEach(id => { - const shape = e.getShape(id); - const shapeBounds = e.getShapeBounds(id); - shape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(shape, whiteboardId); - }); - } - - if (s.includes('move_to_page')) { - const movedShapes = e.selectedIds.map(id => { - return e.getShape(id); - }); - //remove shapes on origin page - removeShapes(e.selectedIds, whiteboardId); - //persist shapes for destination page - const newWhiteboardId = curPres.pages.find(page => page.num === Number.parseInt(e.getPage()?.id)).id; - movedShapes.forEach(s => { - persistShape(s, newWhiteboardId); - }); - return; - } - - const conditions = [ - "session:complete:TransformSingleSession", "session:complete:TranslateSession", - "session:complete:RotateSession", "session:complete:HandleSession", - "updated_shapes", "duplicate", "stretch", "align", "move", - "create", "flip", "toggle", "group", "translate" - ] - if (conditions.some(el => s?.includes(el))) { - e.selectedIds.forEach(id => { - const shape = e.getShape(id); - const shapeBounds = e.getShapeBounds(id); - shape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(shape, whiteboardId); - //checks to find any bindings assosiated with the selected shapes. - //If any, they need to be updated as well. - const pageBindings = e.state.document.pages[e.getPage()?.id]?.bindings; - const boundShapes = []; - if (pageBindings) { - Object.entries(pageBindings).map(([k,b]) => { - if (b.toId.includes(id)) { - boundShapes.push(e.state.document.pages[e.getPage()?.id]?.shapes[b.fromId]) - } - }) - } - //persist shape(s) that was updated by the client and any shapes bound to it. - boundShapes.forEach(bs => { - const shapeBounds = e.getShapeBounds(bs.id); - bs.size = [shapeBounds.width, shapeBounds.height]; - persistShape(bs, whiteboardId) - }) - const children = e.getShape(id).children - //also persist children of the selected shape (grouped shapes) - children && children.forEach(c => { - const shape = e.getShape(c); - const shapeBounds = e.getShapeBounds(c); - shape.size = [shapeBounds.width, shapeBounds.height]; - persistShape(shape, whiteboardId) - }) - }); - } - - if (s?.includes("session:complete:EraseSession") || s?.includes("delete")) { - const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; - let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) - removeShapes(shapesIdsToRemove, whiteboardId) - } - }} - - onPatch={(s, reason) => { - if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) { - const camera = tldrawAPI.getPageState().camera; - //don't allow zoom out more than fit - if (camera.zoom <= cameraFitSlide.zoom) { - tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); - setZoomedIn(false); - zoomSlide(parseInt(curPageId), podId, 0, 0, 0); - } else { - zoomSlide(parseInt(curPageId), podId, camera.zoom, camera.point[0], camera.point[1]); - setZoomedIn(true); - } - } - //don't allow non-presenters to pan&zoom - if (slidePosition && reason && !isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) { - if (slidePosition.zoom === 0 && slidePosition.xCamera === 0 && slidePosition.yCamera === 0) { - tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); - setZoomedIn(false); - } else { - tldrawAPI?.setCamera([slidePosition.xCamera, slidePosition.yCamera], slidePosition.zoom); - setZoomedIn(true); - } - } - }} - /> - - - ); -} +import * as React from "react"; +import _ from "lodash"; +import Cursors from "./cursors/container"; +import { TldrawApp, Tldraw } from "@tldraw/tldraw"; +import logger from '/imports/startup/client/logger'; +import { + ColorStyle, + DashStyle, + SizeStyle, + TDDocument, + TDShapeType, +} from "@tldraw/tldraw"; +import { Renderer, Utils } from "@tldraw/core"; + +function usePrevious(value) { + const ref = React.useRef(); + React.useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} + +const findRemoved = (A, B) => { + return A.filter((a) => { + return !B.includes(a); + }); +}; + +export default function Whiteboard(props) { + const { + isPresenter, + removeShapes, + initDefaultPages, + meetingId, + persistShape, + persistAsset, + shapes, + assets, + currentUser, + curPres, + whiteboardId, + podId, + zoomSlide, + skipToSlide, + slidePosition, + curPageId, + svgUri, + presentationBounds, + isViewersCursorLocked, + } = props; + + const { pages, pageStates } = initDefaultPages(curPres?.pages.length || 1); + const rDocument = React.useRef({ + name: "test", + version: TldrawApp.version, + id: whiteboardId, + pages, + pageStates, + bindings: {}, + assets, + }); + //const [doc, setDoc] = React.useState(rDocument.current); + const [_assets, setAssets] = React.useState(assets); + const [command, setCommand] = React.useState(""); + const [wbAccess, setWBAccess] = React.useState(props?.hasMultiUserAccess(props.whiteboardId, props.currentUser.userId)); + const [selectedIds, setSelectedIds] = React.useState([]); + const [tldrawAPI, setTLDrawAPI] = React.useState(null); + const [cameraFitSlide, setCameraFitSlide] = React.useState({point: [0, 0], zoom: 0}); + const [zoomedIn, setZoomedIn] = React.useState(false); + const prevShapes = usePrevious(shapes); + const prevSlidePosition = usePrevious(slidePosition); + const prevPageId = usePrevious(curPageId); + + const calculateCameraFitSlide = () => { + let zoom = + Math.min( + (presentationBounds.width) / slidePosition.width, + (presentationBounds.height) / slidePosition.height + ); + + zoom = Utils.clamp(zoom, 0.1, 5); + + let point = [0, 0]; + if ((presentationBounds.width / presentationBounds.height) > + (slidePosition.width / slidePosition.height)) + { + point[0] = (presentationBounds.width - (slidePosition.width * zoom)) / 2 / zoom + } else { + point[1] = (presentationBounds.height - (slidePosition.height * zoom)) / 2 / zoom + } + + return {point, zoom} + } + + const doc = React.useMemo(() => { + const currentDoc = rDocument.current; + + let next = { ...currentDoc }; + + let pageBindings = null; + let history = null; + let stack = null; + let changed = false; + + if (next.pageStates[curPageId] && !_.isEqual(prevShapes, shapes)) { + // mergeDocument loses bindings and history, save it + pageBindings = tldrawAPI?.getPage(curPageId)?.bindings; + history = tldrawAPI?.history + stack = tldrawAPI?.stack + + next.pages[curPageId].shapes = shapes; + + changed = true; + } + + if (next.pages[curPageId] && !next.pages[curPageId].shapes["slide-background-shape"]) { + next.assets[`slide-background-asset-${curPageId}`] = { + id: `slide-background-asset-${curPageId}`, + size: [slidePosition?.width || 0, slidePosition?.height || 0], + src: svgUri, + type: "image", + }; + + next.pages[curPageId].shapes["slide-background-shape"] = { + assetId: `slide-background-asset-${curPageId}`, + childIndex: 0.5, + id: "slide-background-shape", + name: "Image", + type: TDShapeType.Image, + parentId: `${curPageId}`, + point: [0, 0], + isLocked: true, + size: [slidePosition?.width || 0, slidePosition?.height || 0], + style: { + dash: DashStyle.Draw, + size: SizeStyle.Medium, + color: ColorStyle.Blue, + }, + }; + + changed = true; + } + + if (changed) { + if (pageBindings) next.pages[curPageId].bindings = pageBindings; + tldrawAPI?.mergeDocument(next); + if (tldrawAPI && history) tldrawAPI.history = history; + if (tldrawAPI && stack) tldrawAPI.stack = stack; + } + + // move poll result text to bottom right + if (next.pages[curPageId]) { + const pollResults = Object.entries(next.pages[curPageId].shapes) + .filter(([id, shape]) => shape.name.includes("poll-result")) + for (const [id, shape] of pollResults) { + if (_.isEqual(shape.point, [0, 0])) { + const shapeBounds = tldrawAPI?.getShapeBounds(id); + if (shapeBounds) { + shape.point = [ + slidePosition.width - shapeBounds.width, + slidePosition.height - shapeBounds.height + ] + shape.size = [shapeBounds.width, shapeBounds.height] + isPresenter && persistShape(shape, whiteboardId); + } + } + }; + } + + return currentDoc; + }, [assets, shapes, tldrawAPI, curPageId, slidePosition]); + + // when presentationBounds change, update tldraw camera + // to fit slide on center if zoomed out + React.useEffect(() => { + if (curPageId && slidePosition) { + const camera = calculateCameraFitSlide(); + setCameraFitSlide(camera); + if (!zoomedIn) { + tldrawAPI?.setCamera(camera.point, camera.zoom); + } + } + }, [presentationBounds, curPageId]); + + // change tldraw page when presentation page changes + React.useEffect(() => { + if (tldrawAPI && curPageId) { + const previousPageZoom = tldrawAPI.getPageState()?.camera?.zoom; + tldrawAPI.changePage(curPageId); + //change zoom of the new page to follow the previous one + if (!zoomedIn && cameraFitSlide.zoom !== 0) { + tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom, "zoomed"); + } else { + previousPageZoom && + slidePosition && + tldrawAPI.setCamera([slidePosition.xCamera, slidePosition.yCamera], previousPageZoom, "zoomed"); + } + } + }, [curPageId]); + + // change tldraw camera when slidePosition changes + React.useEffect(() => { + if (tldrawAPI && !isPresenter && curPageId && slidePosition) { + if (slidePosition.zoom === 0 && slidePosition.xCamera === 0 && slidePosition.yCamera === 0) { + tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); + setZoomedIn(false); + } else { + tldrawAPI?.setCamera([slidePosition.xCamera, slidePosition.yCamera], slidePosition.zoom); + setZoomedIn(true); + } + } + }, [curPageId, slidePosition]); + + const hasWBAccess = props?.hasMultiUserAccess(props.whiteboardId, props.currentUser.userId); + + return ( + <> + + { + if (!hasWBAccess && !isPresenter) app.onPan = () => {}; + setTLDrawAPI(app); + props.setTldrawAPI(app); + if (curPageId) { + app.changePage(curPageId); + if (slidePosition.zoom === 0) { + // first load, center the view to fit slide + const cameraFitSlide = calculateCameraFitSlide(); + app.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); + setCameraFitSlide(cameraFitSlide); + } else { + app.setCamera([slidePosition.xCamera, slidePosition.yCamera], slidePosition.zoom) + setZoomedIn(true); + } + } + }} + showPages={false} + showZoom={false} + showUI={curPres ? (isPresenter || hasWBAccess) : true} + showMenu={curPres ? false : true} + showMultiplayerMenu={false} + readOnly={!isPresenter && !hasWBAccess} + onUndo={(e, s) => { + e?.selectedIds?.map(id => { + persistShape(e.getShape(id), whiteboardId); + }) + const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; + let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) + removeShapes(shapesIdsToRemove, whiteboardId) + }} + + onRedo={(e, s) => { + e?.selectedIds?.map(id => { + persistShape(e.getShape(id), whiteboardId); + }); + const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; + let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) + removeShapes(shapesIdsToRemove, whiteboardId) + }} + + onChangePage={(app, s, b, a) => { + if (app.getPage()?.id !== curPageId) { + skipToSlide(Number.parseInt(app.getPage()?.id), podId) + } + }} + onCommand={(e, s, g) => { + if (s.includes("session:complete:DrawSession")) { + Object.entries(e.state.document.pages[e.getPage()?.id]?.shapes) + .filter(([k, s]) => s?.type === 'draw') + .forEach(([k, s]) => { + if (!e.prevShapes[k] && !k.includes('slide-background')) { + const shapeBounds = e.getShapeBounds(k); + s.size = [shapeBounds.width, shapeBounds.height]; + persistShape(s, whiteboardId); + } + }); + } + + if (s.includes("style") + || s?.includes("session:complete:ArrowSession")) { + e.selectedIds.forEach(id => { + const shape = e.getShape(id); + const shapeBounds = e.getShapeBounds(id); + shape.size = [shapeBounds.width, shapeBounds.height]; + persistShape(shape, whiteboardId); + }); + } + + if (s.includes('move_to_page')) { + const movedShapes = e.selectedIds.map(id => { + return e.getShape(id); + }); + //remove shapes on origin page + removeShapes(e.selectedIds, whiteboardId); + //persist shapes for destination page + const newWhiteboardId = curPres.pages.find(page => page.num === Number.parseInt(e.getPage()?.id)).id; + movedShapes.forEach(s => { + persistShape(s, newWhiteboardId); + }); + return; + } + + const conditions = [ + "session:complete:TransformSingleSession", "session:complete:TranslateSession", + "session:complete:RotateSession", "session:complete:HandleSession", + "updated_shapes", "duplicate", "stretch", "align", "move", + "create", "flip", "toggle", "group", "translate" + ] + if (conditions.some(el => s?.includes(el))) { + e.selectedIds.forEach(id => { + const shape = e.getShape(id); + const shapeBounds = e.getShapeBounds(id); + shape.size = [shapeBounds.width, shapeBounds.height]; + persistShape(shape, whiteboardId); + //checks to find any bindings assosiated with the selected shapes. + //If any, they need to be updated as well. + const pageBindings = e.state.document.pages[e.getPage()?.id]?.bindings; + const boundShapes = []; + if (pageBindings) { + Object.entries(pageBindings).map(([k,b]) => { + if (b.toId.includes(id)) { + boundShapes.push(e.state.document.pages[e.getPage()?.id]?.shapes[b.fromId]) + } + }) + } + //persist shape(s) that was updated by the client and any shapes bound to it. + boundShapes.forEach(bs => { + const shapeBounds = e.getShapeBounds(bs.id); + bs.size = [shapeBounds.width, shapeBounds.height]; + persistShape(bs, whiteboardId) + }) + const children = e.getShape(id).children + //also persist children of the selected shape (grouped shapes) + children && children.forEach(c => { + const shape = e.getShape(c); + const shapeBounds = e.getShapeBounds(c); + shape.size = [shapeBounds.width, shapeBounds.height]; + persistShape(shape, whiteboardId) + }) + }); + } + + if (s?.includes("session:complete:EraseSession") || s?.includes("delete")) { + const pageShapes = e.state.document.pages[e.getPage()?.id]?.shapes; + let shapesIdsToRemove = findRemoved(Object.keys(shapes), Object.keys(pageShapes)) + removeShapes(shapesIdsToRemove, whiteboardId) + } + }} + + onPatch={(s, reason) => { + if (reason && isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) { + const camera = tldrawAPI.getPageState().camera; + //don't allow zoom out more than fit + if (camera.zoom <= cameraFitSlide.zoom) { + tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); + setZoomedIn(false); + zoomSlide(parseInt(curPageId), podId, 0, 0, 0); + } else { + zoomSlide(parseInt(curPageId), podId, camera.zoom, camera.point[0], camera.point[1]); + setZoomedIn(true); + } + } + //don't allow non-presenters to pan&zoom + if (slidePosition && reason && !isPresenter && (reason.includes("zoomed") || reason.includes("panned"))) { + if (slidePosition.zoom === 0 && slidePosition.xCamera === 0 && slidePosition.yCamera === 0) { + tldrawAPI?.setCamera(cameraFitSlide.point, cameraFitSlide.zoom); + setZoomedIn(false); + } else { + tldrawAPI?.setCamera([slidePosition.xCamera, slidePosition.yCamera], slidePosition.zoom); + setZoomedIn(true); + } + } + }} + /> + + + ); +}