You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
237 lines
7.8 KiB
TypeScript
237 lines
7.8 KiB
TypeScript
import React, { useEffect, useRef } from "react";
|
|
import { isShallowEqual, sceneCoordsToViewportCoords } from "../../utils";
|
|
import { CURSOR_TYPE } from "../../constants";
|
|
import { t } from "../../i18n";
|
|
import type { DOMAttributes } from "react";
|
|
import type { AppState, Device, InteractiveCanvasAppState } from "../../types";
|
|
import type {
|
|
InteractiveCanvasRenderConfig,
|
|
RenderableElementsMap,
|
|
RenderInteractiveSceneCallback,
|
|
} from "../../scene/types";
|
|
import type {
|
|
NonDeletedExcalidrawElement,
|
|
NonDeletedSceneElementsMap,
|
|
} from "../../element/types";
|
|
import { isRenderThrottlingEnabled } from "../../reactUtils";
|
|
import { renderInteractiveScene } from "../../renderer/interactiveScene";
|
|
|
|
type InteractiveCanvasProps = {
|
|
containerRef: React.RefObject<HTMLDivElement>;
|
|
canvas: HTMLCanvasElement | null;
|
|
elementsMap: RenderableElementsMap;
|
|
visibleElements: readonly NonDeletedExcalidrawElement[];
|
|
selectedElements: readonly NonDeletedExcalidrawElement[];
|
|
allElementsMap: NonDeletedSceneElementsMap;
|
|
sceneNonce: number | undefined;
|
|
selectionNonce: number | undefined;
|
|
scale: number;
|
|
appState: InteractiveCanvasAppState;
|
|
device: Device;
|
|
renderInteractiveSceneCallback: (
|
|
data: RenderInteractiveSceneCallback,
|
|
) => void;
|
|
handleCanvasRef: (canvas: HTMLCanvasElement | null) => void;
|
|
onContextMenu: Exclude<
|
|
DOMAttributes<HTMLCanvasElement | HTMLDivElement>["onContextMenu"],
|
|
undefined
|
|
>;
|
|
onPointerMove: Exclude<
|
|
DOMAttributes<HTMLCanvasElement>["onPointerMove"],
|
|
undefined
|
|
>;
|
|
onPointerUp: Exclude<
|
|
DOMAttributes<HTMLCanvasElement>["onPointerUp"],
|
|
undefined
|
|
>;
|
|
onPointerCancel: Exclude<
|
|
DOMAttributes<HTMLCanvasElement>["onPointerCancel"],
|
|
undefined
|
|
>;
|
|
onTouchMove: Exclude<
|
|
DOMAttributes<HTMLCanvasElement>["onTouchMove"],
|
|
undefined
|
|
>;
|
|
onPointerDown: Exclude<
|
|
DOMAttributes<HTMLCanvasElement>["onPointerDown"],
|
|
undefined
|
|
>;
|
|
onDoubleClick: Exclude<
|
|
DOMAttributes<HTMLCanvasElement>["onDoubleClick"],
|
|
undefined
|
|
>;
|
|
};
|
|
|
|
const InteractiveCanvas = (props: InteractiveCanvasProps) => {
|
|
const isComponentMounted = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (!isComponentMounted.current) {
|
|
isComponentMounted.current = true;
|
|
return;
|
|
}
|
|
|
|
const remotePointerButton: InteractiveCanvasRenderConfig["remotePointerButton"] =
|
|
new Map();
|
|
const remotePointerViewportCoords: InteractiveCanvasRenderConfig["remotePointerViewportCoords"] =
|
|
new Map();
|
|
const remoteSelectedElementIds: InteractiveCanvasRenderConfig["remoteSelectedElementIds"] =
|
|
new Map();
|
|
const remotePointerUsernames: InteractiveCanvasRenderConfig["remotePointerUsernames"] =
|
|
new Map();
|
|
const remotePointerUserStates: InteractiveCanvasRenderConfig["remotePointerUserStates"] =
|
|
new Map();
|
|
|
|
props.appState.collaborators.forEach((user, socketId) => {
|
|
if (user.selectedElementIds) {
|
|
for (const id of Object.keys(user.selectedElementIds)) {
|
|
if (!remoteSelectedElementIds.has(id)) {
|
|
remoteSelectedElementIds.set(id, []);
|
|
}
|
|
remoteSelectedElementIds.get(id)!.push(socketId);
|
|
}
|
|
}
|
|
if (!user.pointer || user.pointer.renderCursor === false) {
|
|
return;
|
|
}
|
|
if (user.username) {
|
|
remotePointerUsernames.set(socketId, user.username);
|
|
}
|
|
if (user.userState) {
|
|
remotePointerUserStates.set(socketId, user.userState);
|
|
}
|
|
remotePointerViewportCoords.set(
|
|
socketId,
|
|
sceneCoordsToViewportCoords(
|
|
{
|
|
sceneX: user.pointer.x,
|
|
sceneY: user.pointer.y,
|
|
},
|
|
props.appState,
|
|
),
|
|
);
|
|
remotePointerButton.set(socketId, user.button);
|
|
});
|
|
|
|
const selectionColor =
|
|
(props.containerRef?.current &&
|
|
getComputedStyle(props.containerRef.current).getPropertyValue(
|
|
"--color-selection",
|
|
)) ||
|
|
"#6965db";
|
|
|
|
renderInteractiveScene(
|
|
{
|
|
canvas: props.canvas,
|
|
elementsMap: props.elementsMap,
|
|
visibleElements: props.visibleElements,
|
|
selectedElements: props.selectedElements,
|
|
allElementsMap: props.allElementsMap,
|
|
scale: window.devicePixelRatio,
|
|
appState: props.appState,
|
|
renderConfig: {
|
|
remotePointerViewportCoords,
|
|
remotePointerButton,
|
|
remoteSelectedElementIds,
|
|
remotePointerUsernames,
|
|
remotePointerUserStates,
|
|
selectionColor,
|
|
renderScrollbars: false,
|
|
},
|
|
device: props.device,
|
|
callback: props.renderInteractiveSceneCallback,
|
|
},
|
|
isRenderThrottlingEnabled(),
|
|
);
|
|
});
|
|
|
|
return (
|
|
<canvas
|
|
className="excalidraw__canvas interactive"
|
|
style={{
|
|
width: props.appState.width,
|
|
height: props.appState.height,
|
|
cursor: props.appState.viewModeEnabled
|
|
? CURSOR_TYPE.GRAB
|
|
: CURSOR_TYPE.AUTO,
|
|
}}
|
|
width={props.appState.width * props.scale}
|
|
height={props.appState.height * props.scale}
|
|
ref={props.handleCanvasRef}
|
|
onContextMenu={props.onContextMenu}
|
|
onPointerMove={props.onPointerMove}
|
|
onPointerUp={props.onPointerUp}
|
|
onPointerCancel={props.onPointerCancel}
|
|
onTouchMove={props.onTouchMove}
|
|
onPointerDown={props.onPointerDown}
|
|
onDoubleClick={
|
|
props.appState.viewModeEnabled ? undefined : props.onDoubleClick
|
|
}
|
|
>
|
|
{t("labels.drawingCanvas")}
|
|
</canvas>
|
|
);
|
|
};
|
|
|
|
const getRelevantAppStateProps = (
|
|
appState: AppState,
|
|
): InteractiveCanvasAppState => ({
|
|
zoom: appState.zoom,
|
|
scrollX: appState.scrollX,
|
|
scrollY: appState.scrollY,
|
|
width: appState.width,
|
|
height: appState.height,
|
|
viewModeEnabled: appState.viewModeEnabled,
|
|
editingGroupId: appState.editingGroupId,
|
|
editingLinearElement: appState.editingLinearElement,
|
|
selectedElementIds: appState.selectedElementIds,
|
|
frameToHighlight: appState.frameToHighlight,
|
|
offsetLeft: appState.offsetLeft,
|
|
offsetTop: appState.offsetTop,
|
|
theme: appState.theme,
|
|
pendingImageElementId: appState.pendingImageElementId,
|
|
selectionElement: appState.selectionElement,
|
|
selectedGroupIds: appState.selectedGroupIds,
|
|
selectedLinearElement: appState.selectedLinearElement,
|
|
multiElement: appState.multiElement,
|
|
isBindingEnabled: appState.isBindingEnabled,
|
|
suggestedBindings: appState.suggestedBindings,
|
|
isRotating: appState.isRotating,
|
|
elementsToHighlight: appState.elementsToHighlight,
|
|
collaborators: appState.collaborators, // Necessary for collab. sessions
|
|
activeEmbeddable: appState.activeEmbeddable,
|
|
snapLines: appState.snapLines,
|
|
zenModeEnabled: appState.zenModeEnabled,
|
|
editingElement: appState.editingElement,
|
|
});
|
|
|
|
const areEqual = (
|
|
prevProps: InteractiveCanvasProps,
|
|
nextProps: InteractiveCanvasProps,
|
|
) => {
|
|
// This could be further optimised if needed, as we don't have to render interactive canvas on each scene mutation
|
|
if (
|
|
prevProps.selectionNonce !== nextProps.selectionNonce ||
|
|
prevProps.sceneNonce !== nextProps.sceneNonce ||
|
|
prevProps.scale !== nextProps.scale ||
|
|
// we need to memoize on elementsMap because they may have renewed
|
|
// even if sceneNonce didn't change (e.g. we filter elements out based
|
|
// on appState)
|
|
prevProps.elementsMap !== nextProps.elementsMap ||
|
|
prevProps.visibleElements !== nextProps.visibleElements ||
|
|
prevProps.selectedElements !== nextProps.selectedElements
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
// Comparing the interactive appState for changes in case of some edge cases
|
|
return isShallowEqual(
|
|
// asserting AppState because we're being passed the whole AppState
|
|
// but resolve to only the InteractiveCanvas-relevant props
|
|
getRelevantAppStateProps(prevProps.appState as AppState),
|
|
getRelevantAppStateProps(nextProps.appState as AppState),
|
|
);
|
|
};
|
|
|
|
export default React.memo(InteractiveCanvas, areEqual);
|